From ef1923baa647b3eb2c12535c45910860aaedb2e8 Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 30 Apr 2025 23:25:41 +0200 Subject: [PATCH] courrier preview --- components/email/ComposeEmail.tsx | 412 ++++++++++++++--------------- lib/utils/email-utils.ts | 422 ++++++++++++++++-------------- 2 files changed, 421 insertions(+), 413 deletions(-) diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 84e9a13d..5c5e26d6 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -2,37 +2,23 @@ import { useState, useRef, useEffect } from 'react'; import { - X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2 + X, Paperclip, SendHorizontal, Loader2, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; import DOMPurify from 'isomorphic-dompurify'; -import { Label } from '@/components/ui/label'; - -// Import sub-components -import ComposeEmailHeader from './ComposeEmailHeader'; -import RichEmailEditor from './RichEmailEditor'; // Import from the centralized utils import { formatReplyEmail, - formatForwardedEmail, - formatEmailAddresses + formatForwardedEmail } from '@/lib/utils/email-utils'; -import { EmailMessage, EmailAddress } from '@/types/email'; +import { EmailMessage } from '@/types/email'; /** - * CENTRAL EMAIL COMPOSER COMPONENT - * - * This is the unified, centralized email composer component used throughout the application. - * It handles new emails, replies, and forwards with proper text direction. - * - * All code that needs to compose emails should import this component from: - * @/components/email/ComposeEmail + * Email composer component + * Handles new emails, replies, and forwards with a clean UI */ - -// Define interface for the modern props interface ComposeEmailProps { initialEmail?: EmailMessage | null; type?: 'new' | 'reply' | 'reply-all' | 'forward'; @@ -60,7 +46,6 @@ export default function ComposeEmail(props: ComposeEmailProps) { const [bcc, setBcc] = useState(''); const [subject, setSubject] = useState(''); const [emailContent, setEmailContent] = useState(''); - const [quotedContent, setQuotedContent] = useState(''); const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [sending, setSending] = useState(false); @@ -91,11 +76,9 @@ export default function ComposeEmail(props: ComposeEmailProps) { // Set subject setSubject(formatted.subject); - // Set the quoted content (original email) - setQuotedContent(formatted.content.html || formatted.content.text); - - // Start with empty content for the reply - setEmailContent(''); + // Set content with original email + const content = formatted.content.html || formatted.content.text; + setEmailContent(content); } else if (type === 'forward') { // Get formatted data for forward @@ -104,13 +87,11 @@ export default function ComposeEmail(props: ComposeEmailProps) { // Set subject setSubject(formatted.subject); - // Set the quoted content (original email) - setQuotedContent(formatted.content.html || formatted.content.text); + // Set content with original email + const content = formatted.content.html || formatted.content.text; + setEmailContent(content); - // Start with empty content for the forward - setEmailContent(''); - - // If the original email has attachments, we should include them + // If the original email has attachments, include them if (initialEmail.attachments && initialEmail.attachments.length > 0) { const formattedAttachments = initialEmail.attachments.map(att => ({ name: att.filename || 'attachment', @@ -126,6 +107,39 @@ export default function ComposeEmail(props: ComposeEmailProps) { } }, [initialEmail, type]); + // Place cursor at beginning and ensure content is scrolled to top + useEffect(() => { + if (editorRef.current && type !== 'new') { + // Small delay to ensure DOM is ready + setTimeout(() => { + if (editorRef.current) { + // Focus the editor + editorRef.current.focus(); + + // Put cursor at the beginning + const selection = window.getSelection(); + const range = document.createRange(); + range.setStart(editorRef.current, 0); + range.collapse(true); + selection?.removeAllRanges(); + selection?.addRange(range); + + // Also make sure editor container is scrolled to top + editorRef.current.scrollTop = 0; + + // Find parent scrollable containers and scroll them to top + let parent = editorRef.current.parentElement; + while (parent) { + if (parent.classList.contains('overflow-y-auto')) { + parent.scrollTop = 0; + } + parent = parent.parentElement; + } + } + }, 100); + } + }, [emailContent, type]); + // Handle file attachments const handleAttachmentAdd = async (files: FileList) => { const newAttachments = Array.from(files).map(file => ({ @@ -151,17 +165,12 @@ export default function ComposeEmail(props: ComposeEmailProps) { setSending(true); try { - // Combine the new content with the quoted content - const fullContent = type !== 'new' - ? `${emailContent}
${quotedContent}
` - : emailContent; - await onSend({ to, cc: cc || undefined, bcc: bcc || undefined, subject, - body: fullContent, + body: emailContent, attachments }); @@ -175,158 +184,136 @@ export default function ComposeEmail(props: ComposeEmailProps) { } }; - // Focus and scroll to top when opened - useEffect(() => { - setTimeout(() => { - if (editorRef.current) { - editorRef.current.focus(); - - // Scroll to top - const scrollElements = [ - editorRef.current, - document.querySelector('.overflow-y-auto'), - document.querySelector('.compose-email-body') - ]; - - scrollElements.forEach(el => { - if (el instanceof HTMLElement) { - el.scrollTop = 0; - } - }); - } - }, 100); - }, []); + // Get compose title based on type + const getComposeTitle = () => { + switch(type) { + case 'reply': return 'Reply'; + case 'reply-all': return 'Reply All'; + case 'forward': return 'Forward'; + default: return 'New Message'; + } + }; return (
- -
-
- {/* To Field */} -
- - setTo(e.target.value)} - placeholder="recipient@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* CC/BCC Toggle Buttons */} -
- - -
- - {/* CC Field */} - {showCc && ( -
- - setCc(e.target.value)} - placeholder="cc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" + {/* Header */} +
+

{getComposeTitle()}

+ +
+ + {/* Email Form */} +
+
+ {/* Recipients */} +
+
+ To: + setTo(e.target.value)} + placeholder="recipient@example.com" + className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0" />
- )} - - {/* BCC Field */} - {showBcc && ( -
- - setBcc(e.target.value)} - placeholder="bcc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* Subject Field */} -
- - setSubject(e.target.value)} - placeholder="Enter subject" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* Message Body - Simplified Editor */} -
- -
-
- {/* Simple editor for new content */} -
setEmailContent(e.currentTarget.innerHTML)} + + {showCc && ( +
+ Cc: + setCc(e.target.value)} + placeholder="cc@example.com" + className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0" /> - - {/* Quoted content from original email */} - {quotedContent && ( -
-
-
- )}
+ )} + + {showBcc && ( +
+ Bcc: + setBcc(e.target.value)} + placeholder="bcc@example.com" + className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0" + /> +
+ )} + + {/* CC/BCC Toggle Links */} +
+ {!showCc && ( + + )} + + {!showBcc && ( + + )}
+ {/* Subject */} +
+
+ Subject: + setSubject(e.target.value)} + placeholder="Subject" + className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0" + /> +
+
+ + {/* Message Body */} +
setEmailContent(e.currentTarget.innerHTML)} + /> + {/* Attachments */} {attachments.length > 0 && ( -
-

Attachments

-
- {attachments.map((file, index) => ( -
- {file.name} - -
- ))} -
+
+ {attachments.map((file, index) => ( +
+ {file.name} + +
+ ))}
)}
- {/* Modal Footer - now inside the main modal container and visually attached */} -
+ + {/* Footer */} +
{/* File Input for Attachments */} - {sending && Preparing attachment...}
-
+ +
+
- {/* Styles for email display */} + {/* Styles for email content */}
); diff --git a/lib/utils/email-utils.ts b/lib/utils/email-utils.ts index 94bd5408..57bc7248 100644 --- a/lib/utils/email-utils.ts +++ b/lib/utils/email-utils.ts @@ -225,216 +225,214 @@ export function renderEmailContent(content: EmailContent): string { } /** - * Format an email for forwarding + * Format email for reply */ -export function formatForwardedEmail(email: EmailMessage): { - subject: string; - content: EmailContent; -} { - // Format subject with Fwd: prefix if needed - const subjectBase = email.subject || '(No subject)'; - const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) - ? subjectBase - : `Fwd: ${subjectBase}`; +export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all' = 'reply') { + if (!originalEmail) { + return { + to: '', + cc: '', + subject: '', + content: { + text: '', + html: '', + isHtml: false, + direction: 'ltr' as const + } + }; + } + + // Format the recipients + const to = Array.isArray(originalEmail.from) + ? originalEmail.from.map(addr => { + if (typeof addr === 'string') return addr; + return addr.address ? addr.address : ''; + }).filter(Boolean).join(', ') + : typeof originalEmail.from === 'string' + ? originalEmail.from + : ''; + + // For reply-all, include other recipients in CC + let cc = ''; + if (type === 'reply-all') { + const toRecipients = Array.isArray(originalEmail.to) + ? originalEmail.to.map(addr => { + if (typeof addr === 'string') return addr; + return addr.address ? addr.address : ''; + }).filter(Boolean) + : typeof originalEmail.to === 'string' + ? [originalEmail.to] + : []; + + const ccRecipients = Array.isArray(originalEmail.cc) + ? originalEmail.cc.map(addr => { + if (typeof addr === 'string') return addr; + return addr.address ? addr.address : ''; + }).filter(Boolean) + : typeof originalEmail.cc === 'string' + ? [originalEmail.cc] + : []; + + cc = [...toRecipients, ...ccRecipients].join(', '); + } + + // Format the subject + const subject = originalEmail.subject && !originalEmail.subject.startsWith('Re:') + ? `Re: ${originalEmail.subject}` + : originalEmail.subject || ''; + + // Format the content + const originalDate = originalEmail.date ? new Date(originalEmail.date) : new Date(); + const dateStr = originalDate.toLocaleString(); - // Get sender and recipient information - const fromString = formatEmailAddresses(email.from || []); - const toString = formatEmailAddresses(email.to || []); - const dateString = formatEmailDate(email.date); + const fromStr = Array.isArray(originalEmail.from) + ? originalEmail.from.map(addr => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof originalEmail.from === 'string' + ? originalEmail.from + : 'Unknown Sender'; - // Get original content - use the raw content if possible to preserve formatting - let originalContent = ''; - - if (email.content) { - if (email.content.isHtml && email.content.html) { - originalContent = email.content.html; - } else if (email.content.text) { - // Format plain text with basic HTML formatting - originalContent = formatPlainTextToHtml(email.content.text); + const toStr = Array.isArray(originalEmail.to) + ? originalEmail.to.map(addr => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof originalEmail.to === 'string' + ? originalEmail.to + : ''; + + // Create HTML content + const htmlContent = ` +
+
+ + `; + + // Create plain text content + const plainText = originalEmail.content?.text || ''; + const textContent = ` + +On ${dateStr}, ${fromStr} wrote: +> ${plainText.split('\n').join('\n> ')} + `; + + return { + to, + cc, + subject, + content: { + text: textContent, + html: htmlContent, + isHtml: true, + direction: 'ltr' as const } - } else if (email.html) { - originalContent = email.html; - } else if (email.text) { - originalContent = formatPlainTextToHtml(email.text); - } else { - originalContent = '

No content

'; - } - - // Check if the content already has a forwarded message header - const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------'); - - // If there's already a forwarded message header, don't add another one - let htmlContent = ''; - if (hasExistingHeader) { - // Just wrap the content without additional formatting - htmlContent = ` -
- - `; - } else { - // Create formatted content for forwarded email - htmlContent = ` -
-
-
-
-
---------- Forwarded message ---------
-
From: ${fromString}
-
Date: ${dateString}
-
Subject: ${email.subject || ''}
-
To: ${toString}
-
- -
-
- `; - } - - // Ensure we have clean HTML - const cleanedHtml = DOMPurify.sanitize(htmlContent, { - ADD_TAGS: ['style'], - ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'], - ALLOW_DATA_ATTR: true - }); - - // Create normalized content with HTML and extracted text - const content: EmailContent = { - html: cleanedHtml, - text: '', // Will be extracted when composing - isHtml: true, - direction: email.content?.direction || 'ltr' }; - - // Extract text from HTML if in browser environment - if (typeof document !== 'undefined') { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = htmlContent; - content.text = tempDiv.textContent || tempDiv.innerText || ''; - } else { - // Simple text extraction in server environment - content.text = htmlContent - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .trim(); +} + +/** + * Format email for forwarding + */ +export function formatForwardedEmail(originalEmail: any) { + if (!originalEmail) { + return { + to: '', + subject: '', + content: { + text: '', + html: '', + isHtml: false, + direction: 'ltr' as const + } + }; } + + // Format the subject + const subject = originalEmail.subject && !originalEmail.subject.startsWith('Fwd:') + ? `Fwd: ${originalEmail.subject}` + : originalEmail.subject || ''; + + // Format from, to, cc for the header + const fromStr = Array.isArray(originalEmail.from) + ? originalEmail.from.map(addr => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof originalEmail.from === 'string' + ? originalEmail.from + : 'Unknown Sender'; - return { subject, content }; + const toStr = Array.isArray(originalEmail.to) + ? originalEmail.to.map(addr => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof originalEmail.to === 'string' + ? originalEmail.to + : ''; + + const ccStr = Array.isArray(originalEmail.cc) + ? originalEmail.cc.map(addr => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof originalEmail.cc === 'string' + ? originalEmail.cc + : ''; + + const dateStr = originalEmail.date ? new Date(originalEmail.date).toLocaleString() : 'Unknown Date'; + + // Create HTML content + const htmlContent = ` +
+
+ + `; + + // Create plain text content + const textContent = ` + +---------- Forwarded message --------- +From: ${fromStr} +Date: ${dateStr} +Subject: ${originalEmail.subject || ''} +To: ${toStr} +${ccStr ? `Cc: ${ccStr}\n` : ''} + +${originalEmail.content?.text || ''} + `; + + return { + to: '', + subject, + content: { + text: textContent, + html: htmlContent, + isHtml: true, + direction: 'ltr' as const + } + }; } /** * Format an email for reply or reply-all */ -export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all'): { - to: string; - cc?: string; - subject: string; - content: EmailContent; -} { - // Format email addresses - const to = formatEmailAddresses(email.from || []); - - // For reply-all, include all recipients in CC except our own address - let cc = undefined; - if (type === 'reply-all' && (email.to || email.cc)) { - const allRecipients = [ - ...(typeof email.to === 'string' ? [{name: '', address: email.to}] : (email.to || [])), - ...(typeof email.cc === 'string' ? [{name: '', address: email.cc}] : (email.cc || [])) - ]; - - // Remove duplicates, then convert to string - const uniqueRecipients = [...new Map(allRecipients.map(addr => - [typeof addr === 'string' ? addr : addr.address, addr] - )).values()]; - - cc = formatEmailAddresses(uniqueRecipients); - } - - // Format subject with Re: prefix if needed - const subjectBase = email.subject || '(No subject)'; - const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; - - // Get original content - use the raw content if possible to preserve formatting - let originalContent = ''; - - if (email.content) { - if (email.content.isHtml && email.content.html) { - originalContent = email.content.html; - } else if (email.content.text) { - // Format plain text with basic HTML formatting - originalContent = formatPlainTextToHtml(email.content.text); - } - } else if (email.html) { - originalContent = email.html; - } else if (email.text) { - originalContent = formatPlainTextToHtml(email.text); - } else { - originalContent = '

No content

'; - } - - // Format sender info - let senderName = 'Unknown Sender'; - if (email.from) { - if (Array.isArray(email.from) && email.from.length > 0) { - const sender = email.from[0]; - senderName = typeof sender === 'string' ? sender : (sender.name || sender.address); - } else if (typeof email.from === 'string') { - senderName = email.from; - } - } - - const formattedDate = formatEmailDate(email.date); - - // Create the reply content with attribution line - const htmlContent = ` -
-
-
- On ${formattedDate}, ${senderName} wrote: -
-
- ${originalContent} -
-
- `; - - // Ensure we have clean HTML - const cleanedHtml = DOMPurify.sanitize(htmlContent, { - ADD_TAGS: ['style'], - ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'], - ALLOW_DATA_ATTR: true - }); - - // Create normalized content with HTML and extracted text - const content: EmailContent = { - html: cleanedHtml, - text: '', // Will be extracted when composing - isHtml: true, - direction: email.content?.direction || 'ltr' - }; - - // Extract text from HTML if in browser environment - if (typeof document !== 'undefined') { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = htmlContent; - content.text = tempDiv.textContent || tempDiv.innerText || ''; - } else { - // Simple text extraction in server environment - content.text = htmlContent - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .trim(); - } - - return { to, cc, subject, content }; -} - -/** - * Format an email for reply or forward - Unified API - */ export function formatEmailForReplyOrForward( email: EmailMessage, type: 'reply' | 'reply-all' | 'forward' @@ -450,4 +448,32 @@ export function formatEmailForReplyOrForward( } else { return formatReplyEmail(email, type); } +} + +/** + * Helper to properly format email addresses + */ +export function formatEmailAddress(addr: any) { + // ... existing code ... +} + +/** + * Helper to format multiple email addresses + */ +export function formatEmailAddresses(addrs: any) { + // ... existing code ... +} + +/** + * Helper to format recipient + */ +export function formatRecipient(addr: any) { + // ... existing code ... +} + +/** + * Helper to format multiple recipients + */ +export function formatRecipients(addrs: any) { + // ... existing code ... } \ No newline at end of file