'use client'; import { useState, useRef, useEffect } from 'react'; import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import DOMPurify from 'isomorphic-dompurify'; import { Label } from '@/components/ui/label'; // Import ONLY from the centralized formatter import { formatForwardedEmail, formatReplyEmail, formatEmailForReplyOrForward, EmailMessage as FormatterEmailMessage, sanitizeHtml } from '@/lib/utils/email-formatter'; /** * 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 * * It uses the centralized email formatter from @/lib/utils/email-formatter.ts * for consistent handling of email content and text direction. */ // Define EmailMessage interface locally instead of importing from server-only file interface EmailAddress { name: string; address: string; } interface EmailMessage { id: string; messageId?: string; subject: string; from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; date: Date | string; flags?: { seen: boolean; flagged: boolean; answered: boolean; deleted: boolean; draft: boolean; }; preview?: string; content?: string; html?: string; text?: string; hasAttachments?: boolean; attachments?: any[]; folder?: string; size?: number; contentFetched?: boolean; } // Legacy interface for backward compatibility with old ComposeEmail component interface LegacyComposeEmailProps { showCompose: boolean; setShowCompose: (show: boolean) => void; composeTo: string; setComposeTo: (to: string) => void; composeCc: string; setComposeCc: (cc: string) => void; composeBcc: string; setComposeBcc: (bcc: string) => void; composeSubject: string; setComposeSubject: (subject: string) => void; composeBody: string; setComposeBody: (body: string) => void; showCc: boolean; setShowCc: (show: boolean) => void; showBcc: boolean; setShowBcc: (show: boolean) => void; attachments: any[]; setAttachments: (attachments: any[]) => void; handleSend: () => Promise; originalEmail?: { content: string; type: 'reply' | 'reply-all' | 'forward'; }; onSend: (email: any) => Promise; onCancel: () => void; replyTo?: any | null; forwardFrom?: any | null; } // New interface for the modern ComposeEmail component interface ComposeEmailProps { initialEmail?: EmailMessage | null; type?: 'new' | 'reply' | 'reply-all' | 'forward'; onClose: () => void; onSend: (emailData: { to: string; cc?: string; bcc?: string; subject: string; body: string; attachments?: Array<{ name: string; content: string; type: string; }>; }) => Promise; } // Union type to handle both new and legacy props type ComposeEmailAllProps = ComposeEmailProps | LegacyComposeEmailProps; // Type guard to check if props are legacy function isLegacyProps(props: ComposeEmailAllProps): props is LegacyComposeEmailProps { return 'showCompose' in props && 'setShowCompose' in props; } export default function ComposeEmail(props: ComposeEmailAllProps) { // Handle legacy props by adapting them to new component if (isLegacyProps(props)) { return ; } // Continue with modern implementation for new props const { initialEmail, type = 'new', onClose, onSend } = props; // Email form state const [to, setTo] = useState(''); const [cc, setCc] = useState(''); const [bcc, setBcc] = useState(''); const [subject, setSubject] = useState(''); const [emailContent, setEmailContent] = useState(''); const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [sending, setSending] = useState(false); const [attachments, setAttachments] = useState>([]); // Refs const editorRef = useRef(null); const attachmentInputRef = useRef(null); // Initialize the form when replying to or forwarding an email useEffect(() => { if (initialEmail && type !== 'new') { try { const formatterEmail: FormatterEmailMessage = { id: initialEmail.id, messageId: initialEmail.messageId, subject: initialEmail.subject, from: initialEmail.from || [], to: initialEmail.to || [], cc: initialEmail.cc || [], bcc: initialEmail.bcc || [], date: initialEmail.date, content: initialEmail.content, html: initialEmail.html, text: initialEmail.text, hasAttachments: initialEmail.hasAttachments || false }; if (type === 'forward') { // For forwarding, use the dedicated formatter const { subject, content } = formatForwardedEmail(formatterEmail); setSubject(subject); setEmailContent(content); } else { // For reply/reply-all, use the reply formatter const { to, cc, subject, content } = formatReplyEmail(formatterEmail, type as 'reply' | 'reply-all'); setTo(to); if (cc) { setCc(cc); setShowCc(true); } setSubject(subject); setEmailContent(content); } // For type safety const isReplyType = (t: 'new' | 'reply' | 'reply-all' | 'forward'): t is 'reply' | 'reply-all' | 'forward' => t === 'reply' || t === 'reply-all' || t === 'forward'; // Focus editor after initializing content setTimeout(() => { if (isReplyType(type) && editorRef.current) { // For replies/forwards, focus contentEditable editorRef.current.focus(); try { // Place cursor at the beginning const selection = window.getSelection(); if (selection) { const range = document.createRange(); if (editorRef.current.firstChild) { range.setStart(editorRef.current.firstChild, 0); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } } catch (e) { console.error('Error positioning cursor:', e); } } else { // For new emails, focus the textarea const textarea = document.querySelector('textarea'); textarea?.focus(); } }, 100); } catch (error) { console.error('Error formatting email:', error); } } }, [initialEmail, type]); // Handle attachment selection const handleAttachmentClick = () => { attachmentInputRef.current?.click(); }; // Process selected files const handleFileSelection = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; // Read files as data URLs for (const file of files) { const reader = new FileReader(); reader.onload = (event) => { const content = event.target?.result as string; setAttachments(current => [ ...current, { name: file.name, content: content.split(',')[1], // Remove data:mime/type;base64, prefix type: file.type } ]); }; reader.readAsDataURL(file); } // Reset file input if (e.target) { e.target.value = ''; } }; // Remove attachment const removeAttachment = (index: number) => { setAttachments(current => current.filter((_, i) => i !== index)); }; // Handle editor input const handleEditorInput = () => { if (!editorRef.current) return; // Store the current selection/cursor position const selection = window.getSelection(); const range = selection?.getRangeAt(0); const offset = range?.startOffset || 0; const container = range?.startContainer; // Capture the content setEmailContent(editorRef.current.innerHTML); // Try to restore the cursor position after React updates setTimeout(() => { if (!selection || !range || !container || !editorRef.current) return; try { if (editorRef.current.contains(container)) { const newRange = document.createRange(); newRange.setStart(container, offset); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); } } catch (e) { console.error('Error restoring cursor position:', e); } }, 0); }; // Add a handler for textarea changes const handleTextareaChange = (e: React.ChangeEvent) => { setEmailContent(e.target.value); }; // Send email const handleSend = async () => { if (!to) { alert('Please specify at least one recipient'); return; } try { setSending(true); // For new emails, emailContent is already set via onChange // For replies/forwards, we need to get content from editorRef const finalContent = type === 'new' ? emailContent : editorRef.current?.innerHTML || emailContent; await onSend({ to, cc: cc || undefined, bcc: bcc || undefined, subject, body: finalContent, attachments }); onClose(); } catch (error) { console.error('Error sending email:', error); alert('Failed to send email. Please try again.'); } finally { setSending(false); } }; return ( {type === 'new' ? 'New Message' : type === 'forward' ? 'Forward Email' : 'Reply to Email'} {/* Recipients, Subject fields */}
To: setTo(e.target.value)} className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0" placeholder="recipient@example.com" />
{showCc && (
Cc: setCc(e.target.value)} className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0" placeholder="cc@example.com" />
)} {showBcc && (
Bcc: setBcc(e.target.value)} className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0" placeholder="bcc@example.com" />
)} {/* CC/BCC controls */}
Subject: setSubject(e.target.value)} className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0" placeholder="Subject" />
{/* Email Body Editor - different approach based on type */}
{/* For new emails, use textarea for better text direction */} {type === 'new' ? (