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

View File

@ -2,7 +2,10 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { formatEmailForReplyOrForward, EmailMessage, EmailAddress } from '@/lib/services/email-service'; 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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
@ -38,6 +41,8 @@ export default function ComposeEmail({
const [bcc, setBcc] = useState<string>(''); const [bcc, setBcc] = useState<string>('');
const [subject, setSubject] = useState<string>(''); const [subject, setSubject] = useState<string>('');
const [body, setBody] = useState<string>(''); const [body, setBody] = useState<string>('');
const [userMessage, setUserMessage] = useState<string>('');
const [originalContent, setOriginalContent] = useState<string>('');
const [showCc, setShowCc] = useState<boolean>(false); const [showCc, setShowCc] = useState<boolean>(false);
const [showBcc, setShowBcc] = useState<boolean>(false); const [showBcc, setShowBcc] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false); const [sending, setSending] = useState<boolean>(false);
@ -69,7 +74,16 @@ export default function ComposeEmail({
} }
setSubject(formattedEmail.subject); 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 // Focus editor after initializing
@ -198,92 +212,23 @@ export default function ComposeEmail({
// Check if we have content to forward // Check if we have content to forward
if (initialEmail.content || initialEmail.html || initialEmail.text) { if (initialEmail.content || initialEmail.html || initialEmail.text) {
try { // Use the most complete version of content available
// Use the parse-email API endpoint which centralizes our email parsing logic contentHtml = DOMPurify.sanitize(
const response = await fetch('/api/parse-email', { initialEmail.content || initialEmail.html ||
method: 'POST', `<pre style="white-space: pre-wrap;">${initialEmail.text || ''}</pre>`
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');
} }
// Combine the header and content - using containment // Store the complete forwarded content in state
const forwardedContent = ` const forwardedContent = `${headerHtml}<div class="forwarded-content">${contentHtml}</div>`;
${headerHtml} setOriginalContent(forwardedContent);
<div style="margin-top: 10px; border-left: 2px solid #e1e1e1; padding-left: 15px;">
<div style="isolation: isolate; contain: content; overflow: auto;"> // Set the complete body
${contentHtml} setBody(`<div class="user-message"></div>${forwardedContent}`);
</div>
</div>
`;
console.log('Setting body with forwarded content');
setBody(forwardedContent);
} catch (error) { } catch (error) {
console.error('Error initializing forwarded email:', error); console.error('Error formatting forwarded email:', error);
setBody('<div style="color: #666; font-style: italic;">Error formatting forwarded email content</div>');
// 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>
`);
} }
}; };
@ -364,8 +309,14 @@ export default function ComposeEmail({
// Handle editor input // Handle editor input
const handleEditorInput = (e: React.FormEvent<HTMLDivElement>) => { const handleEditorInput = (e: React.FormEvent<HTMLDivElement>) => {
// Store the HTML content for use in the send function if (editorRef.current) {
setBody(e.currentTarget.innerHTML); 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 // Toggle text direction
@ -377,23 +328,17 @@ export default function ComposeEmail({
}; };
return ( return (
<Card className="w-full h-full flex flex-col overflow-hidden shadow-lg"> <Card className="w-full max-w-4xl mx-auto">
<CardHeader className="border-b py-2 px-4"> <CardHeader>
<div className="flex justify-between items-center"> <CardTitle className="flex items-center justify-between">
<CardTitle className="text-lg"> <span>{type === 'new' ? 'New Message' : type === 'forward' ? 'Forward Email' : 'Reply to Email'}</span>
{type === 'new' ? 'New Message' :
type === 'reply' ? 'Reply' :
type === 'reply-all' ? 'Reply All' :
'Forward'}
</CardTitle>
<Button variant="ghost" size="icon" onClick={onClose}> <Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-4 w-4" /> <X className="h-5 w-5" />
</Button> </Button>
</div> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4">
<CardContent className="p-0 flex-1 flex flex-col overflow-hidden"> {/* Recipients, Subject fields remain the same */}
{/* Email header fields */}
<div className="p-3 border-b space-y-2"> <div className="p-3 border-b space-y-2">
<div className="flex items-center"> <div className="flex items-center">
<span className="w-16 text-sm font-medium">To:</span> <span className="w-16 text-sm font-medium">To:</span>
@ -477,31 +422,52 @@ export default function ComposeEmail({
</div> </div>
</div> </div>
{/* Editor toolbar */} {/* Updated Email Body Editor */}
<div className="border-b p-1 flex items-center"> <div className="space-y-2">
<Button <div className="flex items-center justify-between">
variant="ghost" <label htmlFor="body" className="text-sm font-medium">Message</label>
size="sm" <Button
onClick={toggleTextDirection} variant="ghost"
className="hover:bg-muted" size="sm"
title={isRTL ? "Switch to Left-to-Right" : "Switch to Right-to-Left"} onClick={toggleTextDirection}
> className="h-8 px-2 text-xs"
{isRTL ? <TextAlignRight className="h-4 w-4" /> : <TextAlignLeft className="h-4 w-4" />} >
</Button> {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> </div>
{/* Email body editor */} {/* Attachments section remains the same */}
<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.length > 0 && ( {attachments.length > 0 && (
<div className="border-t p-2"> <div className="border-t p-2">
<div className="text-sm font-medium mb-1">Attachments:</div> <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 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(', '); 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 { return {
to: '', to: '',
subject, subject,
body: ` body: `
<div class="forwarded-message"> <div class="forwarded-message" style="margin-top: 20px;">
<p>---------- Forwarded message ---------</p> <div style="background-color: #f5f5f5; border-left: 3px solid #ddd; padding: 12px; margin-bottom: 15px; border-radius: 4px;">
<p>From: ${fromText}</p> <p style="margin: 0 0 8px 0; font-weight: 500; color: #555; font-size: 14px;">---------- Forwarded message ---------</p>
<p>Date: ${formattedDate}</p> <p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">From:</span> ${fromText}</p>
<p>Subject: ${email.subject || ''}</p> <p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">Date:</span> ${formattedDate}</p>
<p>To: ${toText}</p> <p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">Subject:</span> ${email.subject || ''}</p>
<br> <p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">To:</span> ${toText}</p>
${email.html || email.text ? (email.html || `<pre>${email.text || ''}</pre>`) : '<p>No content available</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> </div>
` `
}; };
} }
// Format the email body with quote // Format the email body with improved quote styling for replies
const body = ` const body = `
<div> <div class="reply-body">
<br/> <br/>
<br/> <div class="quote-header" style="color: #555; font-size: 13px; margin: 20px 0 10px 0;">${quoteHeader}</div>
<div>${quoteHeader}</div> <blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 3px solid #ddd; color: #555; background-color: #f8f8f8; border-radius: 4px;">
<blockquote style="border-left: 2px solid #ccc; padding-left: 10px; margin-left: 10px; color: #777;"> <div class="quoted-content" style="font-size: 13px;">
${quotedContent} ${quotedContent}
</div>
</blockquote> </blockquote>
</div>`; </div>`;
@ -754,5 +758,5 @@ function createQuoteHeader(email: EmailMessage): string {
? `${sender.name} <${sender.address}>` ? `${sender.name} <${sender.address}>`
: sender?.address || 'Unknown sender'; : sender?.address || 'Unknown sender';
return `<div>On ${formattedDate}, ${fromText} wrote:</div>`; return `<div style="font-weight: 500;">On ${formattedDate}, ${fromText} wrote:</div>`;
} }