From 0d4fc59904335a0329ccba1eea760c99c8de0b56 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 17:28:30 +0200 Subject: [PATCH] courrier preview --- components/email/ComposeEmail.tsx | 17 + components/email/RichEmailEditor.tsx | 569 ++++++++++++++++----------- 2 files changed, 346 insertions(+), 240 deletions(-) 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' + ? `
---------- Forwarded message ----------
Unable to load original message content
` + : `
On ${dateStr}, ${fromStr} wrote:
Unable to load original message content
`; + setEmailContent(fallbackContent); + } + }, 5000); + // Get recipients based on type if (type === 'reply' || type === 'reply-all') { // Get formatted data for reply @@ -258,6 +272,9 @@ export default function ComposeEmail(props: ComposeEmailProps) { setAttachments(formattedAttachments); } } + + // Clear the safety timeout if we complete successfully + return () => clearTimeout(safetyTimeoutId); } catch (error) { console.error('Error initializing compose form:', error); // Provide a fallback in case of error diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index 4297ef05..c08e7703 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -180,6 +180,22 @@ const RichEmailEditor: React.FC = ({ console.log('Initializing editor in', contentIsReplyOrForward ? 'reply/forward' : 'compose', 'mode'); + // Clean up any existing Quill instance + if (quillRef.current) { + console.log('Cleaning up existing Quill instance before reinitializing'); + quillRef.current.off('text-change'); + try { + // Safely remove the Quill instance to prevent memory leaks + const editorContainer = editorRef.current.querySelector('.ql-editor'); + if (editorContainer) { + editorContainer.innerHTML = ''; + } + } catch (e) { + console.warn('Error cleaning up editor:', e); + } + quillRef.current = null; + } + const Quill = (await import('quill')).default; // Import quill-better-table conditionally based on content type @@ -216,161 +232,172 @@ const RichEmailEditor: React.FC = ({ ['clean'], ]; - // Create new Quill instance with the DOM element and custom toolbar - const editorElement = editorRef.current; - quillRef.current = new Quill(editorElement, { - modules: { - toolbar: { - container: toolbarRef.current, - handlers: { - // Add any custom toolbar handlers here - } + try { + // Create new Quill instance with the DOM element and custom toolbar + const editorElement = editorRef.current; + quillRef.current = new Quill(editorElement, { + modules: { + toolbar: { + container: toolbarRef.current, + handlers: { + // Add any custom toolbar handlers here + } + }, + clipboard: { + matchVisual: false // Disable clipboard matching for better HTML handling + }, + // Only enable better-table for regular content, not for replies/forwards + 'better-table': tableModule && !contentIsReplyOrForward ? true : false, }, - clipboard: { - matchVisual: false // Disable clipboard matching for better HTML handling - }, - // Only enable better-table for regular content, not for replies/forwards - 'better-table': tableModule && !contentIsReplyOrForward ? true : false, - }, - placeholder: placeholder, - theme: 'snow', - }); + placeholder: placeholder, + theme: 'snow', + }); - // Set initial content properly - if (initialContent) { - try { - console.log('Setting initial content in editor', { - length: initialContent.length, - startsWithHtml: initialContent.trim().startsWith('<'), - containsForwardedMessage: initialContent.includes('---------- Forwarded message ----------'), - containsReplyIndicator: initialContent.includes('wrote:'), - hasBlockquote: initialContent.includes(' { + 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,[Image: Attachment]'); + 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