diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index b5b367b1..28004908 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -136,6 +136,20 @@ export default function ComposeEmail(props: ComposeEmailProps) { } } + // Safety timeout to prevent endless loading + const safetyTimeoutId = setTimeout(() => { + const contentState = emailContent; + if (!contentState || contentState === "") { + console.warn('Email content initialization timed out after 5 seconds, using fallback template'); + // Create a basic fallback template + const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail); + const fallbackContent = type === 'forward' + ? `
{ + if (el instanceof HTMLElement) { + el.scrollTop = 0; + } + }); + } + } catch (err) { + console.error('Error setting initial content:', err); + + // Enhanced fallback mechanism for complex content try { + // First try to extract text from HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = initialContent; - const textContent = tempDiv.textContent || tempDiv.innerText || 'Empty content'; + const textContent = tempDiv.textContent || tempDiv.innerText || ''; - // Set text directly to ensure something displays - quillRef.current.setText(textContent); + if (textContent.trim()) { + console.log('Using extracted text fallback, length:', textContent.length); + quillRef.current.setText(textContent); + } else { + // If text extraction fails or returns empty, provide a message + console.log('Using empty content fallback'); + quillRef.current.setText('Unable to load original content'); + } } catch (e) { - console.error('Text extraction fallback failed:', e); + console.error('All fallbacks failed:', e); quillRef.current.setText('Error loading content'); } - } else { - // Special handling for reply or forwarded content - if (contentIsReplyOrForward) { - console.log('Using special handling for reply/forward content'); - - // For reply/forward content, convert ALL tables to divs - const cleanedContent = cleanupTableStructures(sanitizedContent, true); - - // Use direct innerHTML setting with minimal processing for reply/forward content - quillRef.current.root.innerHTML = cleanedContent; - } else { - // For regular content, use normal processing - const cleanedContent = cleanupTableStructures(sanitizedContent, false); - - // Use direct innerHTML setting for regular content - quillRef.current.root.innerHTML = cleanedContent; - } - - // Set the direction for the content - if (quillRef.current && quillRef.current.format) { - quillRef.current.format('direction', direction); - if (direction === 'rtl') { - quillRef.current.format('align', 'right'); - } - } else { - console.warn('Cannot format content: editor not fully initialized'); - } - } - - // Set cursor at the beginning - quillRef.current.setSelection(0, 0); - - // Ensure the cursor and scroll position is at the top of the editor - if (editorRef.current) { - editorRef.current.scrollTop = 0; - - // Find and scroll parent containers that might have scroll - const scrollable = [ - editorRef.current.closest('.ql-container'), - editorRef.current.closest('.rich-email-editor-container'), - editorRef.current.closest('.overflow-y-auto'), - document.querySelector('.overflow-y-auto') - ]; - - scrollable.forEach(el => { - if (el instanceof HTMLElement) { - el.scrollTop = 0; - } - }); - } - } catch (err) { - console.error('Error setting initial content:', err); - - // Enhanced fallback mechanism for complex content - try { - // First try to extract text from HTML - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = initialContent; - const textContent = tempDiv.textContent || tempDiv.innerText || ''; - - if (textContent.trim()) { - console.log('Using extracted text fallback, length:', textContent.length); - quillRef.current.setText(textContent); - } else { - // If text extraction fails or returns empty, provide a message - console.log('Using empty content fallback'); - quillRef.current.setText('Unable to load original content'); - } - } catch (e) { - console.error('All fallbacks failed:', e); - quillRef.current.setText('Error loading content'); } } + + // Add change listener + quillRef.current.on('text-change', () => { + const html = quillRef.current.root.innerHTML; + onChange(html); + }); + + // Improve editor layout + const editorContainer = editorElement.closest('.ql-container'); + if (editorContainer) { + editorContainer.classList.add('email-editor-container'); + } + + setIsReady(true); + } catch (initError) { + console.error('Critical error initializing editor:', initError); + // Provide fallback in UI + if (editorRef.current) { + editorRef.current.innerHTML = 'Error loading editor. Please try again or use plain text mode.'; + } } - - // Add change listener - quillRef.current.on('text-change', () => { - const html = quillRef.current.root.innerHTML; - onChange(html); - }); - - // Improve editor layout - const editorContainer = editorElement.closest('.ql-container'); - if (editorContainer) { - editorContainer.classList.add('email-editor-container'); - } - - setIsReady(true); }; initializeQuill().catch(err => { @@ -382,10 +409,50 @@ const RichEmailEditor: React.FC= ({ if (quillRef.current) { // Clean up any event listeners or resources quillRef.current.off('text-change'); + quillRef.current = null; } }; }, []); + // Add utility function to handle blocked content + /** + * Pre-process content to handle blocked images and other resources + */ + function handleBlockedContent(htmlContent: string): string { + if (!htmlContent) return htmlContent; + + try { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // Replace CID and other problematic image sources + const images = tempDiv.querySelectorAll('img'); + images.forEach(img => { + const src = img.getAttribute('src') || ''; + + // Handle CID attachments that would be blocked + if (src.startsWith('cid:')) { + console.log('Replacing CID image source:', src); + img.setAttribute('src', 'data:image/svg+xml;utf8,'); + img.setAttribute('data-original-src', src); + img.style.maxWidth = '300px'; + img.style.border = '1px dashed #ddd'; + } + + // Handle tracking pixels and potentially blocked remote content + if (src.includes('open?') || src.includes('tracking') || src.includes('pixel')) { + console.log('Removing tracking pixel:', src); + img.remove(); + } + }); + + return tempDiv.innerHTML; + } catch (error) { + console.error('Error handling blocked content:', error); + return htmlContent; + } + } + // Update content from props if changed externally - using a simpler approach useEffect(() => { if (quillRef.current && isReady && initialContent) { @@ -411,112 +478,134 @@ const RichEmailEditor: React.FC = ({ initialContent.includes('Forwarded message') || initialContent.includes('---------- Forwarded message ----------'); - // If content type changed (from reply to regular or vice versa), we need to reload - if (contentIsReplyOrForward !== isReplyOrForward) { - console.log('Content type changed from', isReplyOrForward ? 'reply/forward' : 'regular', - 'to', contentIsReplyOrForward ? 'reply/forward' : 'regular', - '- reloading editor'); - setIsReplyOrForward(contentIsReplyOrForward); - // Force a complete re-initialization of the editor by unmounting - if (quillRef.current) { - quillRef.current.off('text-change'); - quillRef.current = null; - } - setIsReady(false); - return; - } - - // Process HTML content using centralized utility - const processed = processHtmlContent(initialContent, { - sanitize: true, - preserveReplyFormat: contentIsReplyOrForward - }); - const sanitizedContent = processed.sanitizedContent; - const direction = processed.direction; // Use direction from processed result - - // Log sanitized content details for debugging - console.log('Sanitized content details:', { - length: sanitizedContent.length, - isEmpty: sanitizedContent.trim().length === 0, - startsWithDiv: sanitizedContent.trim().startsWith(' { - if (quillRef.current) { - try { - // Set the direction for the content - quillRef.current.format('direction', direction); - if (direction === 'rtl') { - quillRef.current.format('align', 'right'); - } - - // Force update - quillRef.current.update(); - - // Set selection to beginning - quillRef.current.setSelection(0, 0); - } catch (innerError) { - console.error('Error applying delayed formatting:', innerError); - } + // Clear content to prevent flashing + try { + quillRef.current.root.innerHTML = 'Loading...'; + } catch (e) { + console.warn('Error clearing editor content:', e); + } + + quillRef.current = null; + } + setIsReady(false); + + // Force a small delay before reinitializing to ensure cleanup completes + setTimeout(() => { + // Explicitly empty out the editor DOM node to ensure clean start + if (editorRef.current) { + while (editorRef.current.firstChild) { + editorRef.current.removeChild(editorRef.current.firstChild); } - }, 100); + } + }, 50); + + return; + } + + // Pre-process to handle blocked content + const preProcessedContent = handleBlockedContent(initialContent); + + // Process HTML content using centralized utility + const processed = processHtmlContent(preProcessedContent, { + sanitize: true, + preserveReplyFormat: contentIsReplyOrForward + }); + const sanitizedContent = processed.sanitizedContent; + const direction = processed.direction; // Use direction from processed result + + // Log sanitized content details for debugging + console.log('Sanitized content details:', { + length: sanitizedContent.length, + isEmpty: sanitizedContent.trim().length === 0, + startsWithDiv: sanitizedContent.trim().startsWith('{ + if (quillRef.current) { + try { + // Set the direction for the content + quillRef.current.format('direction', direction); + if (direction === 'rtl') { + quillRef.current.format('align', 'right'); + } + + // Force update + quillRef.current.update(); + + // Set selection to beginning + quillRef.current.setSelection(0, 0); + } catch (innerError) { + console.error('Error applying delayed formatting:', innerError); + } + } + }, 100); + } + } else { + // For regular content, use normal processing + const cleanedContent = cleanupTableStructures(sanitizedContent, false); + + if (quillRef.current && quillRef.current.root) { + quillRef.current.root.innerHTML = cleanedContent; + + // Safely apply formatting + try { + quillRef.current.format('direction', direction); + if (direction === 'rtl') { + quillRef.current.format('align', 'right'); + } + + // Force update + quillRef.current.update(); + + // Set selection to beginning + quillRef.current.setSelection(0, 0); + } catch (formatError) { + console.error('Error applying formatting:', formatError); } - - // Force update - quillRef.current.update(); - - // Set selection to beginning - quillRef.current.setSelection(0, 0); - } catch (formatError) { - console.error('Error applying formatting:', formatError); } } } - } } catch (err) { console.error('Error updating content:', err); // Safer fallback that avoids clipboard API