diff --git a/.DS_Store b/.DS_Store index c3c20e74..bafcc976 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index dbc4bdd0..1c77745c 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -1,13 +1,77 @@ 'use client'; import { useState, useRef, useEffect } from 'react'; -import { formatEmailForReplyOrForward, EmailMessage, EmailAddress } from '@/lib/services/email-service'; -import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2 } from 'lucide-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 sub-components +import ComposeEmailHeader from './ComposeEmailHeader'; +import ComposeEmailForm from './ComposeEmailForm'; +import ComposeEmailFooter from './ComposeEmailFooter'; +import RichEmailEditor from './RichEmailEditor'; +import QuotedEmailContent from './QuotedEmailContent'; + +// Import ONLY from the centralized formatter +import { + formatReplyEmail, + formatForwardedEmail, + formatEmailAddresses, + type EmailMessage, + type EmailAddress +} 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 interface for the legacy props +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; +} + +// Define interface for the modern props interface ComposeEmailProps { initialEmail?: EmailMessage | null; type?: 'new' | 'reply' | 'reply-all' | 'forward'; @@ -26,18 +90,63 @@ interface ComposeEmailProps { }) => Promise; } -export default function ComposeEmail({ - initialEmail, - type = 'new', - onClose, - onSend -}: ComposeEmailProps) { +// Union type for handling both types of props +type ComposeEmailAllProps = ComposeEmailProps | LegacyComposeEmailProps; + +// Type guard to check if props are legacy +function isLegacyProps( + props: ComposeEmailAllProps +): props is LegacyComposeEmailProps { + return 'showCompose' in props; +} + +// Helper function to adapt EmailMessage to QuotedEmailContent props format +function EmailMessageToQuotedContentAdapter({ + email, + type +}: { + email: EmailMessage, + type: 'reply' | 'reply-all' | 'forward' +}) { + // Get the email content + const content = email.content || email.html || email.text || ''; + + // Get the sender + const sender = email.from && email.from.length > 0 + ? { + name: email.from[0].name, + email: email.from[0].address + } + : { email: 'unknown@example.com' }; + + // Map the type to what QuotedEmailContent expects + const mappedType = type === 'reply-all' ? 'reply' : type; + + return ( + + ); +} + +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 [body, setBody] = useState(''); + const [emailContent, setEmailContent] = useState(''); const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [sending, setSending] = useState(false); @@ -47,320 +156,170 @@ export default function ComposeEmail({ type: string; }>>([]); - // Refs - const editorRef = useRef(null); - const attachmentInputRef = useRef(null); - // Initialize the form when replying to or forwarding an email useEffect(() => { if (initialEmail && type !== 'new') { - if (type === 'forward') { - initializeForwardedEmail(); - } else { - // For reply/reply-all, continue using formatEmailForReplyOrForward - const formattedEmail = formatEmailForReplyOrForward(initialEmail, type as 'reply' | 'reply-all'); - - setTo(formattedEmail.to); - - if (formattedEmail.cc) { - setCc(formattedEmail.cc); - setShowCc(true); + try { + // Set recipients based on type + if (type === 'reply' || type === 'reply-all') { + // Reply goes to the original sender + setTo(formatEmailAddresses(initialEmail.from || [])); + + // For reply-all, include all original recipients in CC + if (type === 'reply-all') { + const allRecipients = [ + ...(initialEmail.to || []), + ...(initialEmail.cc || []) + ]; + // Filter out the current user if they were a recipient + // This would need some user context to properly implement + setCc(formatEmailAddresses(allRecipients)); + } + + // Set subject with Re: prefix + const subjectBase = initialEmail.subject || '(No subject)'; + const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; + setSubject(subject); + + // Format the reply content with the quoted message included directly + const content = initialEmail.content || initialEmail.html || initialEmail.text || ''; + const sender = initialEmail.from && initialEmail.from.length > 0 + ? initialEmail.from[0].name || initialEmail.from[0].address + : 'Unknown sender'; + const date = initialEmail.date ? + (typeof initialEmail.date === 'string' ? new Date(initialEmail.date) : initialEmail.date) : + new Date(); + + // Format date for display + const formattedDate = date.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // Create reply content with quote + const replyContent = ` +

+

+

+

+
On ${formattedDate}, ${sender} wrote:
+
+
+ ${content} +
+
+ `; + + setEmailContent(replyContent); + + // Show CC field if there are CC recipients + if (initialEmail.cc && initialEmail.cc.length > 0) { + setShowCc(true); + } + } + else if (type === 'forward') { + // Set subject with Fwd: prefix + const subjectBase = initialEmail.subject || '(No subject)'; + const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) ? subjectBase : `Fwd: ${subjectBase}`; + setSubject(subject); + + // Format the forward content with the original email included directly + const content = initialEmail.content || initialEmail.html || initialEmail.text || ''; + const fromString = formatEmailAddresses(initialEmail.from || []); + const toString = formatEmailAddresses(initialEmail.to || []); + const date = initialEmail.date ? + (typeof initialEmail.date === 'string' ? new Date(initialEmail.date) : initialEmail.date) : + new Date(); + + // Format date for display + const formattedDate = date.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // Create forwarded content + const forwardContent = ` +

+

+

+

+
+
+
+
---------- Forwarded message ---------
+
From: ${fromString}
+
Date: ${formattedDate}
+
Subject: ${initialEmail.subject || ''}
+
To: ${toString}
+
+ +
+
+ `; + + setEmailContent(forwardContent); + + // If the original email has attachments, we should include them + if (initialEmail.attachments && initialEmail.attachments.length > 0) { + const formattedAttachments = initialEmail.attachments.map(att => ({ + name: att.filename || 'attachment', + type: att.contentType || 'application/octet-stream', + content: att.content || '' + })); + setAttachments(formattedAttachments); + } } - - setSubject(formattedEmail.subject); - setBody(formattedEmail.body); + } catch (error) { + console.error('Error initializing compose form:', error); } - - // Focus editor after initializing - setTimeout(() => { - if (editorRef.current) { - editorRef.current.focus(); - - // Place cursor at the beginning of the content - const selection = window.getSelection(); - const range = document.createRange(); - - range.setStart(editorRef.current, 0); - range.collapse(true); - - selection?.removeAllRanges(); - selection?.addRange(range); - } - }, 100); } }, [initialEmail, type]); - - // Format date for the forwarded message header - const formatDate = (date: Date | null): string => { - if (!date) return ''; - try { - return date.toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } catch (e) { - return date.toString(); - } - }; - // Format sender address in a readable format - const formatSender = (from: Array<{name?: string, address: string}> | undefined): string => { - if (!from || from.length === 0) return 'Unknown'; - return from.map(sender => - sender.name && sender.name !== sender.address - ? `${sender.name} <${sender.address}>` - : sender.address - ).join(', '); - }; - - // Format recipient addresses in a readable format - const formatRecipients = (recipients: Array<{name?: string, address: string}> | undefined): string => { - if (!recipients || recipients.length === 0) return ''; - return recipients.map(recipient => - recipient.name && recipient.name !== recipient.address - ? `${recipient.name} <${recipient.address}>` - : recipient.address - ).join(', '); - }; - - // Initialize forwarded email with clear structure and style preservation - const initializeForwardedEmail = async () => { - console.log('Starting initializeForwardedEmail'); - if (!initialEmail) { - console.error('No email available for forwarding'); - setBody('
No email available for forwarding
'); - return; - } - - // Debug the email object structure - console.log('Forwarding email object:', { - id: initialEmail.id, - subject: initialEmail.subject, - fromLength: initialEmail.from?.length, - from: initialEmail.from, - to: initialEmail.to, - date: initialEmail.date, - hasContent: Boolean(initialEmail.content), - contentLength: initialEmail.content?.length, - hasHtml: Boolean(initialEmail.html), - htmlLength: initialEmail.html?.length - }); - - try { - // Format subject with Fwd: prefix if needed - const subjectBase = initialEmail.subject || '(No subject)'; - const subjectRegex = /^(Fwd|FW|Forward):\s*/i; - const subject = subjectRegex.test(subjectBase) - ? subjectBase - : `Fwd: ${subjectBase}`; - - setSubject(subject); - - // Format the forwarded message with a well-structured header - const fromString = Array.isArray(initialEmail.from) && initialEmail.from.length > 0 - ? initialEmail.from.map(addr => addr.name - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ') - : 'Unknown'; - - const toString = Array.isArray(initialEmail.to) && initialEmail.to.length > 0 - ? initialEmail.to.map(addr => addr.name - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ') - : ''; - - const dateString = initialEmail.date - ? typeof initialEmail.date === 'string' - ? new Date(initialEmail.date).toLocaleString() - : initialEmail.date.toLocaleString() - : new Date().toLocaleString(); - - // Create a clean wrapper that won't interfere with the original email's styling - // Use inline styles for the header to avoid CSS conflicts - const headerHtml = ` -
-
-
---------- Forwarded message ---------
-
From: ${fromString}
-
Date: ${dateString}
-
Subject: ${subjectBase}
-
To: ${toString}
-
-
- `; - - // Process the original content - let originalContent = ''; - - // First try to use the API to parse and sanitize the email content - try { - // Use server-side parsing via fetch API to properly handle complex emails - 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(); - - if (parsedEmail.html && parsedEmail.html.trim()) { - console.log('Using parsed HTML content for forward'); - - // Create an iframe-like containment for the email content - // This prevents CSS from the original email leaking into our compose view - originalContent = ` - - `; - } else if (parsedEmail.text && parsedEmail.text.trim()) { - console.log('Using parsed text content for forward'); - originalContent = `
${parsedEmail.text}
`; - } else { - console.log('No content available from parser'); - originalContent = '
No content available
'; - } - } else { - throw new Error('Failed to parse email content'); - } - } catch (parseError) { - console.error('Error parsing email content:', parseError); - - // Fall back to direct content handling if API parsing fails - if (initialEmail.html && initialEmail.html.trim()) { - console.log('Falling back to HTML content for forward'); - // Use DOMPurify to sanitize HTML and remove dangerous elements - originalContent = DOMPurify.sanitize(initialEmail.html, { - ADD_TAGS: ['style', 'div', 'span', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'img', 'table', 'tr', 'td', 'th'], - ADD_ATTR: ['style', 'class', 'id', 'src', 'alt', 'href', 'target'], - FORBID_TAGS: ['script', 'iframe', 'object', 'embed'], - FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'] - }); - } else if (initialEmail.content && initialEmail.content.trim()) { - console.log('Falling back to content field for forward'); - originalContent = DOMPurify.sanitize(initialEmail.content); - } else if (initialEmail.text && initialEmail.text.trim()) { - console.log('Falling back to text content for forward'); - originalContent = `
${initialEmail.text}
`; - } else { - console.log('No content available for forward'); - originalContent = '
No content available
'; - } - } - - // Preserve all original structure by wrapping, not modifying the original content - // Important: We add a style scope to prevent CSS leakage - const forwardedContent = ` - ${headerHtml} - - - - `; - - console.log('Setting body with forwarded content'); - setBody(forwardedContent); - - } catch (error) { - console.error('Error initializing forwarded email:', error); - - // Even in error case, provide a usable template with empty values - setBody(` -
-
-
---------- Forwarded message ---------
-
From: ${initialEmail.from ? formatSender(initialEmail.from) : 'Unknown'}
-
Date: ${new Date().toLocaleString()}
-
Subject: ${initialEmail.subject || '(No subject)'}
-
To: ${initialEmail.to ? formatRecipients(initialEmail.to) : ''}
-
-
-
- Error loading original message content. The original message may still be viewable in your inbox. -
- `); - } - }; - - // 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; - - // Convert selected files to attachments + // Handle file attachments + const handleAttachmentAdd = async (files: FileList) => { const newAttachments = Array.from(files).map(file => ({ - file, - uploading: true + name: file.name, + type: file.type, + content: URL.createObjectURL(file) })); - - // 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 = ''; - } + setAttachments(prev => [...prev, ...newAttachments]); }; - - // Remove attachment - const removeAttachment = (index: number) => { - setAttachments(current => current.filter((_, i) => i !== index)); + + const handleAttachmentRemove = (index: number) => { + setAttachments(prev => prev.filter((_, i) => i !== index)); }; - - // Send the email + + // Handle sending email const handleSend = async () => { if (!to) { alert('Please specify at least one recipient'); return; } - + + setSending(true); + try { - setSending(true); - await onSend({ to, cc: cc || undefined, bcc: bcc || undefined, subject, - body: editorRef.current?.innerHTML || body, + body: emailContent, attachments }); + // Reset form and close onClose(); } catch (error) { console.error('Error sending email:', error); @@ -369,183 +328,591 @@ export default function ComposeEmail({ setSending(false); } }; - - // Handle editor input - const handleEditorInput = (e: React.FormEvent) => { - // Store the HTML content for use in the send function - setBody(e.currentTarget.innerHTML); - }; - + + // Additional effect to ensure we scroll to the top and focus the editor + useEffect(() => { + // Focus the editor and ensure it's scrolled to the top + const editorContainer = document.querySelector('.ql-editor') as HTMLElement; + if (editorContainer) { + // Set timeout to ensure DOM is fully rendered + setTimeout(() => { + // Focus the editor + editorContainer.focus(); + + // Make sure all scroll containers are at the top + editorContainer.scrollTop = 0; + + // Find all possible scrollable parent containers + const scrollContainers = [ + document.querySelector('.ql-container') as HTMLElement, + document.querySelector('.rich-email-editor-container') as HTMLElement, + document.querySelector('.h-full.flex.flex-col.p-6') as HTMLElement + ]; + + // Scroll all containers to top + scrollContainers.forEach(container => { + if (container) { + container.scrollTop = 0; + } + }); + }, 100); + } + }, []); + return ( - - -
- - {type === 'new' ? 'New Message' : - type === 'reply' ? 'Reply' : - type === 'reply-all' ? 'Reply All' : - 'Forward'} - - -
-
- - - {/* Email header 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 */} -
- - {/* Attachments list */} - {attachments.length > 0 && ( -
-
Attachments:
-
- {attachments.map((attachment, index) => ( -
- - {attachment.name} - -
- ))} -
-
- )} - - - -
- +
+
+ {/* Modal Header */} +
+

+ {type === 'reply' ? 'Reply' : type === 'forward' ? 'Forward' : type === 'reply-all' ? 'Reply All' : 'New Message'} +

+ + {/* Modal Body */} +
+
+ {/* 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" + /> +
+ )} + + {/* 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 */} +
+ +
+ +
+
+ + {/* Attachments */} + {attachments.length > 0 && ( +
+

Attachments

+
+ {attachments.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+
+ )} +
+
+ + {/* Modal Footer */} +
+
+ {/* File Input for Attachments */} + { + if (e.target.files && e.target.files.length > 0) { + handleAttachmentAdd(e.target.files); + } + }} + /> + + {sending && Preparing attachment...} +
+
+ + +
+
+
+
+ ); +} + +// Legacy adapter to maintain backward compatibility +function LegacyAdapter({ + showCompose, + setShowCompose, + composeTo, + setComposeTo, + composeCc, + setComposeCc, + composeBcc, + setComposeBcc, + composeSubject, + setComposeSubject, + composeBody, + setComposeBody, + showCc, + setShowCc, + showBcc, + setShowBcc, + attachments, + setAttachments, + handleSend, + originalEmail, + onSend, + onCancel, + replyTo, + forwardFrom +}: LegacyComposeEmailProps) { + const [sending, setSending] = useState(false); + + // Determine the type based on legacy props + const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => { + if (originalEmail?.type === 'forward') return 'forward'; + if (originalEmail?.type === 'reply-all') return 'reply-all'; + if (originalEmail?.type === 'reply') return 'reply'; + if (replyTo) return 'reply'; + if (forwardFrom) return 'forward'; + return 'new'; + }; + + // Format legacy content on mount if necessary + useEffect(() => { + // Only format if we have original email and no content was set yet + if ((originalEmail || replyTo || forwardFrom) && + (!composeBody || composeBody === '

' || composeBody === '
')) { + + const type = determineType(); + + if (type === 'reply' || type === 'reply-all') { + // For reply, format with sender info and original content + const content = originalEmail?.content || ''; + const sender = replyTo?.name || replyTo?.email || 'Unknown sender'; + const date = new Date().toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); - - - + const replyContent = ` +

+

+

+

+
On ${date}, ${sender} wrote:
+
+
+ ${content} +
+
+ `; + + setComposeBody(replyContent); + } + else if (type === 'forward') { + // For forward, format with original message details + const content = originalEmail?.content || ''; + const fromString = forwardFrom?.name || forwardFrom?.email || 'Unknown'; + const toString = 'Recipients'; + const date = new Date().toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + const forwardContent = ` +

+

+

+

+
+
+
+
---------- Forwarded message ---------
+
From: ${fromString}
+
Date: ${date}
+
Subject: ${composeSubject || ''}
+
To: ${toString}
+
+ +
+
+ `; + + setComposeBody(forwardContent); + } + } + }, [originalEmail, replyTo, forwardFrom, composeBody, determineType, composeSubject]); + + // Converts attachments to the expected format + const convertAttachments = () => { + return attachments.map(att => ({ + name: att.name || att.filename || 'attachment', + content: att.content || '', + type: att.type || att.contentType || 'application/octet-stream' + })); + }; + + // Handle sending in the legacy format + const handleLegacySend = async () => { + setSending(true); + + try { + if (onSend) { + // New API + await onSend({ + to: composeTo, + cc: composeCc, + bcc: composeBcc, + subject: composeSubject, + body: composeBody, + attachments: convertAttachments() + }); + } else if (handleSend) { + // Old API + await handleSend(); + } + + // Close compose window + setShowCompose(false); + } catch (error) { + console.error('Error sending email:', error); + alert('Failed to send email. Please try again.'); + } finally { + setSending(false); + } + }; + + // Handle file selection for legacy interface + const handleFileSelection = (files: FileList) => { + const newAttachments = Array.from(files).map(file => ({ + name: file.name, + type: file.type, + content: URL.createObjectURL(file), + size: file.size + })); + + setAttachments([...attachments, ...newAttachments]); + }; + + if (!showCompose) return null; + + return ( +
+
+ {/* Modal Header */} +
+

+ {determineType() === 'reply' ? 'Reply' : determineType() === 'forward' ? 'Forward' : determineType() === 'reply-all' ? 'Reply All' : 'New Message'} +

+ +
+ + {/* Modal Body */} +
+
+ {/* To Field */} +
+ + setComposeTo(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 && ( +
+ + setComposeCc(e.target.value)} + placeholder="cc@example.com" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" + /> +
+ )} + + {/* BCC Field */} + {showBcc && ( +
+ + setComposeBcc(e.target.value)} + placeholder="bcc@example.com" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" + /> +
+ )} + + {/* Subject Field */} +
+ + setComposeSubject(e.target.value)} + placeholder="Enter subject" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" + /> +
+ + {/* Message Body */} +
+ +
+ +
+
+ + {/* Attachments */} + {attachments.length > 0 && ( +
+

Attachments

+
+ {attachments.map((file, index) => ( +
+ {file.name || file.filename} + +
+ ))} +
+
+ )} +
+
+ + {/* Modal Footer */} +
+
+ {/* File Input for Attachments */} + { + if (e.target.files && e.target.files.length > 0) { + handleFileSelection(e.target.files); + } + }} + /> + + {sending && Preparing attachment...} +
+
+ + +
+
+
+
); } \ No newline at end of file diff --git a/components/email/EmailList.tsx b/components/email/EmailList.tsx index 94480b2c..eac71c6a 100644 --- a/components/email/EmailList.tsx +++ b/components/email/EmailList.tsx @@ -1,11 +1,11 @@ 'use client'; -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useState } from 'react'; import { Loader2, Mail, Search, X } from 'lucide-react'; import { Email } from '@/hooks/use-courrier'; -import EmailListItem from '@/components/email/EmailListItem'; -import EmailListHeader from '@/components/email/EmailListHeader'; -import BulkActionsToolbar from '@/components/email/BulkActionsToolbar'; +import EmailListItem from './EmailListItem'; +import EmailListHeader from './EmailListHeader'; +import BulkActionsToolbar from './BulkActionsToolbar'; import { Input } from '@/components/ui/input'; interface EmailListProps { @@ -43,92 +43,24 @@ export default function EmailList({ }: EmailListProps) { const [scrollPosition, setScrollPosition] = useState(0); const [searchQuery, setSearchQuery] = useState(''); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const scrollRef = useRef(null); - const scrollTimeoutRef = useRef(null); - const prevEmailsLengthRef = useRef(emails.length); - // Debounced scroll handler for better performance - const handleScroll = useCallback((event: React.UIEvent) => { + // Handle scroll to detect when user reaches the bottom + const handleScroll = (event: React.UIEvent) => { const target = event.target as HTMLDivElement; const { scrollTop, scrollHeight, clientHeight } = target; - // Save scroll position for restoration setScrollPosition(scrollTop); - // Clear any existing timeout to prevent rapid firing - if (scrollTimeoutRef.current) { - clearTimeout(scrollTimeoutRef.current); + // If user scrolls near the bottom and we have more emails, load more + if (scrollHeight - scrollTop - clientHeight < 200 && hasMoreEmails && !isLoading) { + onLoadMore(); } - - // If near bottom (within 200px) and more emails are available, load more - // Added additional checks to prevent loading loop - const isNearBottom = scrollHeight - scrollTop - clientHeight < 200; - if (isNearBottom && hasMoreEmails && !isLoading && !isLoadingMore) { - setIsLoadingMore(true); - - // Use timeout to debounce load requests - scrollTimeoutRef.current = setTimeout(() => { - // Clear the timeout reference before loading - scrollTimeoutRef.current = null; - onLoadMore(); - - // Reset loading state after a delay - setTimeout(() => { - setIsLoadingMore(false); - }, 1500); // Increased from 1000ms to 1500ms to prevent quick re-triggering - }, 200); // Increased from 100ms to 200ms for better debouncing - } - }, [hasMoreEmails, isLoading, isLoadingMore, onLoadMore]); - - // Restore scroll position when emails are loaded - useEffect(() => { - // Only attempt to restore position if: - // 1. We have more emails than before - // 2. We have a scroll reference - // 3. We have a saved scroll position - // 4. We're not in the middle of a loading operation - if (emails.length > prevEmailsLengthRef.current && - scrollRef.current && - scrollPosition > 0 && - !isLoading) { - // Use requestAnimationFrame to ensure the DOM has updated - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollPosition; - console.log(`Restored scroll position to ${scrollPosition}`); - } - }); - } - - // Always update the reference for next comparison - prevEmailsLengthRef.current = emails.length; - }, [emails.length, scrollPosition, isLoading]); - - // Add listener for custom reset scroll event - useEffect(() => { - const handleResetScroll = () => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - setScrollPosition(0); - } - }; - - window.addEventListener('reset-email-scroll', handleResetScroll); - - return () => { - window.removeEventListener('reset-email-scroll', handleResetScroll); - }; - }, []); + }; + // Handle search const handleSearch = (e: React.FormEvent) => { e.preventDefault(); onSearch?.(searchQuery); - - // Reset scroll to top when searching - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } }; const clearSearch = () => { @@ -136,21 +68,11 @@ export default function EmailList({ onSearch?.(''); }; - const scrollToTop = () => { - if (scrollRef.current) { - // Use smooth scrolling for better UX - scrollRef.current.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - }; - // Render loading state if (isLoading && emails.length === 0) { return (
- +
); } @@ -221,22 +143,10 @@ export default function EmailList({ )}
- {/* Back to top button */} - {scrollPosition > 300 && ( - - )} - - {/* Email list */} {emails.map((email) => ( ))} - {/* Loading indicator */} - {(isLoading || isLoadingMore) && ( + {isLoading && emails.length > 0 && (
- +
)} - - {/* Load more button - only show when near bottom but not auto-loading */} - {hasMoreEmails && !isLoading && !isLoadingMore && ( - - )}