courrier clean 2

This commit is contained in:
alma 2025-04-26 14:17:44 +02:00
parent e3791fa583
commit 0c5437a24f
3 changed files with 141 additions and 177 deletions

View File

@ -174,7 +174,7 @@ export default function ComposeEmail({
setUserMessage(content);
// Combine user message with quoted content for the full email body
const combined = `${content}${quotedContent ? quotedContent : ''}`;
const combined = `${content}${quotedContent ? `<div class="original-content">${quotedContent}</div>` : ''}`;
setComposeBody(combined);
}
};
@ -189,8 +189,8 @@ export default function ComposeEmail({
// For rich editor, combine user message with quoted content
if (useRichEditor) {
// Wrap the content with proper direction styles
const userContent = userMessage ? `<div class="force-ltr">${userMessage}</div>` : '';
const quotedWithStyles = quotedContent ? `<div class="force-ltr">${quotedContent}</div>` : '';
const userContent = userMessage ? `<div class="force-ltr user-message">${userMessage}</div>` : '';
const quotedWithStyles = quotedContent ? `<div class="force-ltr quoted-content" style="margin-top: 20px; border-top: 1px solid #e2e2e2; padding-top: 10px;">${quotedContent}</div>` : '';
const combinedContent = `${userContent}${quotedWithStyles}`;
setComposeBody(combinedContent);
@ -341,12 +341,12 @@ export default function ComposeEmail({
<Label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</Label>
{useRichEditor ? (
<div className="mt-1 border border-gray-300 rounded-md overflow-hidden">
{/* User input area - completely separate from quoted content */}
<div className="mt-1 border border-gray-300 rounded-md overflow-hidden flex flex-col">
{/* User input area - clearly separated from quoted content */}
<div
ref={contentEditableRef}
contentEditable="true"
className="w-full p-3 bg-white min-h-[100px] text-gray-900 email-editor"
className="w-full p-3 bg-white min-h-[150px] text-gray-900 email-editor"
onInput={handleUserMessageChange}
onFocus={() => {
// Clear placeholder text when user focuses if they haven't started typing
@ -364,39 +364,33 @@ export default function ComposeEmail({
style={{ direction: 'ltr', textAlign: 'left' }}
/>
{/* Original email content - also editable */}
{/* Original email content with clear visual separation */}
{quotedContent && (
<div
className="w-full bg-gray-50 border-t border-gray-300 email-content-wrapper"
style={{ direction: 'ltr' }}
>
<>
<div className="px-3 py-2 bg-gray-100 border-t border-gray-300 text-xs text-gray-500 font-medium">
{composeSubject.startsWith('Re:') ? 'Original message' : 'Forwarded message'}
</div>
<div
className="p-3 opacity-90 text-sm email-content force-ltr"
contentEditable="true"
dangerouslySetInnerHTML={{ __html: quotedContent }}
onInput={(e) => {
const target = e.currentTarget;
// Force LTR on any content added/edited using setAttribute
target.querySelectorAll('*').forEach(el => {
el.setAttribute('style', 'direction: ltr !important; text-align: left !important;');
// Also add the class to ensure styles are applied
el.classList.add('force-ltr');
});
setQuotedContent(target.innerHTML);
// Update the combined content
if (contentEditableRef.current) {
const combined = `${contentEditableRef.current.innerHTML}${target.innerHTML}`;
setComposeBody(combined);
}
}}
style={{
direction: 'ltr',
textAlign: 'left',
unicodeBidi: 'isolate',
writingMode: 'horizontal-tb'
}}
/>
</div>
className="w-full bg-gray-50 border-t border-gray-200 email-content-wrapper"
style={{ direction: 'ltr' }}
>
<div
className="p-3 text-sm email-content force-ltr"
dangerouslySetInnerHTML={{ __html: quotedContent }}
contentEditable="false"
style={{
direction: 'ltr',
textAlign: 'left',
unicodeBidi: 'isolate',
writingMode: 'horizontal-tb',
color: '#555',
borderLeft: '3px solid #ccc',
paddingLeft: '10px',
margin: '0 10px',
}}
/>
</div>
</>
)}
</div>
) : (
@ -409,7 +403,7 @@ export default function ComposeEmail({
style={{ direction: 'ltr', textAlign: 'left' }}
/>
)}
</div>
</div>
</div>
</div>

View File

@ -2,7 +2,10 @@
import { useState, useRef, useEffect } from 'react';
import { formatEmailForReplyOrForward, EmailMessage, EmailAddress } from '@/lib/services/email-service';
import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2, TextAlignLeft, TextAlignRight } from 'lucide-react';
import {
X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2,
AlignLeft, AlignRight
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
@ -38,6 +41,8 @@ export default function ComposeEmail({
const [bcc, setBcc] = useState<string>('');
const [subject, setSubject] = useState<string>('');
const [body, setBody] = useState<string>('');
const [userMessage, setUserMessage] = useState<string>('');
const [originalContent, setOriginalContent] = useState<string>('');
const [showCc, setShowCc] = useState<boolean>(false);
const [showBcc, setShowBcc] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false);
@ -69,7 +74,16 @@ export default function ComposeEmail({
}
setSubject(formattedEmail.subject);
setBody(formattedEmail.body);
// Split the formatted body to separate user's message area from quoted content
const bodyContent = formattedEmail.body;
setBody(bodyContent);
// Set the original content for the quoted part
const quoteMatch = bodyContent.match(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/i);
if (quoteMatch && quoteMatch[0]) {
setOriginalContent(quoteMatch[0]);
}
}
// Focus editor after initializing
@ -198,92 +212,23 @@ export default function ComposeEmail({
// Check if we have content to forward
if (initialEmail.content || initialEmail.html || initialEmail.text) {
try {
// Use the parse-email API endpoint which centralizes our email parsing logic
const response = await fetch('/api/parse-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: initialEmail.content || initialEmail.html || initialEmail.text || ''
}),
});
if (response.ok) {
const parsedEmail = await response.json();
// Use the parsed HTML content if available
if (parsedEmail.html) {
contentHtml = parsedEmail.html;
console.log('Successfully parsed HTML content');
} else if (parsedEmail.text) {
// Text-only content is wrapped in pre-formatted styling
contentHtml = `<div style="white-space: pre-wrap; font-family: monospace; padding: 10px; background-color: #f9f9f9; border-radius: 4px;">${parsedEmail.text}</div>`;
console.log('Using text content');
} else {
console.warn('API returned success but no content');
}
} else {
console.error('API returned error:', await response.text());
throw new Error('API call failed');
}
} catch (error) {
console.error('Error parsing email:', error);
// Fallback processing - using our cleanHtml utility directly
if (initialEmail.html) {
// Import the cleanHtml function dynamically if needed
const { cleanHtml } = await import('@/lib/mail-parser-wrapper');
contentHtml = cleanHtml(initialEmail.html, {
preserveStyles: true,
scopeStyles: true,
addWrapper: true
});
console.log('Using direct HTML cleaning fallback');
} else if (initialEmail.content) {
contentHtml = DOMPurify.sanitize(initialEmail.content, {
ADD_TAGS: ['style'],
FORBID_TAGS: ['script', 'iframe']
});
console.log('Using DOMPurify sanitized content');
} else if (initialEmail.text) {
contentHtml = `<div style="white-space: pre-wrap; font-family: monospace; padding: 10px; background-color: #f9f9f9; border-radius: 4px;">${initialEmail.text}</div>`;
console.log('Using plain text fallback');
}
}
} else {
console.warn('No email content available for forwarding');
// Use the most complete version of content available
contentHtml = DOMPurify.sanitize(
initialEmail.content || initialEmail.html ||
`<pre style="white-space: pre-wrap;">${initialEmail.text || ''}</pre>`
);
}
// Combine the header and content - using containment
const forwardedContent = `
${headerHtml}
<div style="margin-top: 10px; border-left: 2px solid #e1e1e1; padding-left: 15px;">
<div style="isolation: isolate; contain: content; overflow: auto;">
${contentHtml}
</div>
</div>
`;
// Store the complete forwarded content in state
const forwardedContent = `${headerHtml}<div class="forwarded-content">${contentHtml}</div>`;
setOriginalContent(forwardedContent);
// Set the complete body
setBody(`<div class="user-message"></div>${forwardedContent}`);
console.log('Setting body with forwarded content');
setBody(forwardedContent);
} catch (error) {
console.error('Error initializing forwarded email:', error);
// Provide a minimal template even in error case
setBody(`
<div style="border-top: 1px solid #e1e1e1; margin-top: 20px; padding-top: 15px; font-family: Arial, sans-serif; color: #333;">
<div style="margin-bottom: 15px;">
<div style="font-weight: normal; margin-bottom: 10px;">---------- Forwarded message ---------</div>
<div><b>From:</b> ${initialEmail.from ? formatSender(initialEmail.from) : 'Unknown'}</div>
<div><b>Date:</b> ${new Date().toLocaleString()}</div>
<div><b>Subject:</b> ${initialEmail.subject || '(No subject)'}</div>
<div><b>To:</b> ${initialEmail.to ? formatRecipients(initialEmail.to) : ''}</div>
</div>
</div>
<div style="margin-top: 10px; padding: 15px; color: #d32f2f; font-style: italic; border: 1px dashed #d32f2f; margin: 15px 0; text-align: center; background-color: #fff8f8; border-radius: 4px;">
Error loading original message content. The original message may still be viewable in your inbox.
</div>
`);
console.error('Error formatting forwarded email:', error);
setBody('<div style="color: #666; font-style: italic;">Error formatting forwarded email content</div>');
}
};
@ -364,8 +309,14 @@ export default function ComposeEmail({
// Handle editor input
const handleEditorInput = (e: React.FormEvent<HTMLDivElement>) => {
// Store the HTML content for use in the send function
setBody(e.currentTarget.innerHTML);
if (editorRef.current) {
const content = editorRef.current.innerHTML;
setUserMessage(content);
// Combine user message with original content for the full email
const combined = `<div class="user-message" style="direction: ${isRTL ? 'rtl' : 'ltr'}; text-align: ${isRTL ? 'right' : 'left'};">${content}</div>${originalContent}`;
setBody(combined);
}
};
// Toggle text direction
@ -377,23 +328,17 @@ export default function ComposeEmail({
};
return (
<Card className="w-full h-full flex flex-col overflow-hidden shadow-lg">
<CardHeader className="border-b py-2 px-4">
<div className="flex justify-between items-center">
<CardTitle className="text-lg">
{type === 'new' ? 'New Message' :
type === 'reply' ? 'Reply' :
type === 'reply-all' ? 'Reply All' :
'Forward'}
</CardTitle>
<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>{type === 'new' ? 'New Message' : type === 'forward' ? 'Forward Email' : 'Reply to Email'}</span>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-4 w-4" />
<X className="h-5 w-5" />
</Button>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0 flex-1 flex flex-col overflow-hidden">
{/* Email header fields */}
<CardContent className="space-y-4">
{/* Recipients, Subject fields remain the same */}
<div className="p-3 border-b space-y-2">
<div className="flex items-center">
<span className="w-16 text-sm font-medium">To:</span>
@ -477,31 +422,52 @@ export default function ComposeEmail({
</div>
</div>
{/* Editor toolbar */}
<div className="border-b p-1 flex items-center">
<Button
variant="ghost"
size="sm"
onClick={toggleTextDirection}
className="hover:bg-muted"
title={isRTL ? "Switch to Left-to-Right" : "Switch to Right-to-Left"}
>
{isRTL ? <TextAlignRight className="h-4 w-4" /> : <TextAlignLeft className="h-4 w-4" />}
</Button>
{/* Updated Email Body Editor */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="body" className="text-sm font-medium">Message</label>
<Button
variant="ghost"
size="sm"
onClick={toggleTextDirection}
className="h-8 px-2 text-xs"
>
{isRTL ? <AlignLeft className="h-4 w-4 mr-1" /> : <AlignRight className="h-4 w-4 mr-1" />}
{isRTL ? 'LTR' : 'RTL'}
</Button>
</div>
{/* Email editor with clear separation between user message and original content */}
<div className="border rounded-md overflow-hidden">
{/* User's message input area */}
<div
ref={editorRef}
contentEditable={!sending}
className="w-full p-4 min-h-[200px] focus:outline-none"
onInput={handleEditorInput}
style={{
direction: isRTL ? 'rtl' : 'ltr',
textAlign: isRTL ? 'right' : 'left'
}}
/>
{/* Original content display with visual separation - only shown for replies/forwards */}
{type !== 'new' && originalContent && (
<div className="border-t pt-2">
<div className="px-4 py-2 bg-gray-50 text-xs font-medium text-gray-500">
{type === 'forward' ? 'Forwarded content' : 'Original message'}
</div>
<div
className="p-4 bg-gray-50 text-sm original-content"
dangerouslySetInnerHTML={{ __html: originalContent }}
style={{ opacity: 0.85 }}
/>
</div>
)}
</div>
</div>
{/* Email body editor */}
<div
ref={editorRef}
className="flex-1 overflow-auto p-4 focus:outline-none email-content"
contentEditable={true}
onInput={handleEditorInput}
dangerouslySetInnerHTML={{ __html: body }}
dir={isRTL ? "rtl" : "ltr"}
style={{ minHeight: '200px' }}
/>
{/* Attachments list */}
{/* Attachments section remains the same */}
{attachments.length > 0 && (
<div className="border-t p-2">
<div className="text-sm font-medium mb-1">Attachments:</div>

View File

@ -673,32 +673,36 @@ export function formatEmailForReplyOrForward(
const fromText = email.from.map(f => f.name ? `${f.name} <${f.address}>` : f.address).join(', ');
const toText = email.to.map(t => t.name ? `${t.name} <${t.address}>` : t.address).join(', ');
// Return specialized body for forward
// Return specialized body for forward with improved styling
return {
to: '',
subject,
body: `
<div class="forwarded-message">
<p>---------- Forwarded message ---------</p>
<p>From: ${fromText}</p>
<p>Date: ${formattedDate}</p>
<p>Subject: ${email.subject || ''}</p>
<p>To: ${toText}</p>
<br>
${email.html || email.text ? (email.html || `<pre>${email.text || ''}</pre>`) : '<p>No content available</p>'}
<div class="forwarded-message" style="margin-top: 20px;">
<div style="background-color: #f5f5f5; border-left: 3px solid #ddd; padding: 12px; margin-bottom: 15px; border-radius: 4px;">
<p style="margin: 0 0 8px 0; font-weight: 500; color: #555; font-size: 14px;">---------- Forwarded message ---------</p>
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">From:</span> ${fromText}</p>
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">Date:</span> ${formattedDate}</p>
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">Subject:</span> ${email.subject || ''}</p>
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">To:</span> ${toText}</p>
</div>
<div class="forwarded-content" style="border-left: 2px solid #ddd; padding-left: 15px; color: #333;">
${email.html || email.text ? (email.html || `<pre style="white-space: pre-wrap; font-family: inherit; margin: 0;">${email.text || ''}</pre>`) : '<p>No content available</p>'}
</div>
</div>
`
};
}
// Format the email body with quote
// Format the email body with improved quote styling for replies
const body = `
<div>
<div class="reply-body">
<br/>
<br/>
<div>${quoteHeader}</div>
<blockquote style="border-left: 2px solid #ccc; padding-left: 10px; margin-left: 10px; color: #777;">
${quotedContent}
<div class="quote-header" style="color: #555; font-size: 13px; margin: 20px 0 10px 0;">${quoteHeader}</div>
<blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 3px solid #ddd; color: #555; background-color: #f8f8f8; border-radius: 4px;">
<div class="quoted-content" style="font-size: 13px;">
${quotedContent}
</div>
</blockquote>
</div>`;
@ -754,5 +758,5 @@ function createQuoteHeader(email: EmailMessage): string {
? `${sender.name} <${sender.address}>`
: sender?.address || 'Unknown sender';
return `<div>On ${formattedDate}, ${fromText} wrote:</div>`;
return `<div style="font-weight: 500;">On ${formattedDate}, ${fromText} wrote:</div>`;
}