diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 3f1a980c..78cb5a2a 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -98,20 +98,34 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s }; } +// Email content parsing cache to prevent redundant API calls +const parsedEmailCache = new Map(); + function EmailContent({ email }: { email: Email }) { const [content, setContent] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [debugInfo, setDebugInfo] = useState(null); + const didParseRef = useRef(false); useEffect(() => { let mounted = true; + + // Check if we've already parsed this email content before + const cacheKey = email?.id + (email?.content?.substring(0, 50) || ''); + const cachedResult = parsedEmailCache.get(cacheKey); async function loadContent() { if (!email) return; + // If this is the same email, don't reload unless we need to + if (didParseRef.current && content) { + return; + } + setIsLoading(true); setDebugInfo(null); + try { console.log('Loading content for email:', email.id); console.log('Email content length:', email.content?.length || 0); @@ -136,6 +150,58 @@ function EmailContent({ email }: { email: Email }) { } return; } + + // Use cached result if available + if (cachedResult) { + console.log('Using cached parsed email for:', email.id); + if (mounted) { + if (cachedResult.html) { + const sanitizedHtml = DOMPurify.sanitize(cachedResult.html); + setContent( +
+ ); + setDebugInfo('Rendered HTML content from cache'); + } else if (cachedResult.text) { + setContent( +
+ {cachedResult.text} +
+ ); + setDebugInfo('Rendered text content from cache'); + } + setError(null); + setIsLoading(false); + didParseRef.current = true; + } + return; + } + + // Check if the content is already HTML + if (formattedEmail.includes(' + ); + setDebugInfo('Rendered pre-existing HTML content'); + + // Cache the result + parsedEmailCache.set(cacheKey, { html: formattedEmail }); + + setError(null); + setIsLoading(false); + didParseRef.current = true; + } + return; + } console.log('Parsing email content:', formattedEmail.substring(0, 100) + '...'); const parsedEmail = await decodeEmail(formattedEmail); @@ -146,6 +212,12 @@ function EmailContent({ email }: { email: Email }) { textLength: parsedEmail.text?.length || 0 }); + // Cache the result for future use + parsedEmailCache.set(cacheKey, { + html: parsedEmail.html || undefined, + text: parsedEmail.text || undefined + }); + if (mounted) { if (parsedEmail.html) { const sanitizedHtml = DOMPurify.sanitize(parsedEmail.html); @@ -169,6 +241,7 @@ function EmailContent({ email }: { email: Email }) { } setError(null); setIsLoading(false); + didParseRef.current = true; } } catch (err) { console.error('Error rendering email content:', err); @@ -186,7 +259,7 @@ function EmailContent({ email }: { email: Email }) { return () => { mounted = false; }; - }, [email?.id, email?.content]); + }, [email?.id, email?.content, content]); if (isLoading) { return ( @@ -517,6 +590,9 @@ export default function CourrierPage() { type: 'reply' | 'reply-all' | 'forward'; } | null>(null); + // Email content cache to prevent redundant API calls + const emailContentCache = useRef>(new Map()); + // Debug logging for email distribution useEffect(() => { const emailsByFolder = emails.reduce((acc, email) => { @@ -622,13 +698,28 @@ export default function CourrierPage() { // Add a refreshEmails function to handle fresh data loading const refreshEmails = async () => { + // If we're already refreshing, don't start another refresh + if (isLoadingRefresh) return; + setIsLoadingRefresh(true); + + // Clear selected email to avoid UI inconsistencies + setSelectedEmail(null); + try { // Force timestamp to bypass cache const timestamp = Date.now(); + + // Include skipCache parameter to bypass any server-side caching const response = await fetch( `/api/courrier?folder=${encodeURIComponent(currentView)}&page=1&limit=${emailsPerPage}&_t=${timestamp}&skipCache=true`, - { cache: 'no-store' } + { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache' + } + } ); if (!response.ok) { @@ -637,34 +728,36 @@ export default function CourrierPage() { const data = await response.json(); + // Create a Set of standard folder names for more efficient lookup + const standardFoldersSet = new Set(['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']); + // Always update folders on refresh if (data.folders && data.folders.length > 0) { setAvailableFolders(data.folders); - // Update sidebar items based on folders - const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; - const customFolders = data.folders.filter( - (folder: string) => !standardFolders.includes(folder) - ); - + // Filter custom folders more efficiently + const customFolders = data.folders.filter((folder: string) => !standardFoldersSet.has(folder)); setFolders(customFolders); - // Update sidebar items with standard and custom folders - const updatedSidebarItems = [ - ...sidebarItems.filter(item => standardFolders.includes(item.view)), - ...customFolders.map((folder: string) => ({ - view: folder, - label: folder, - icon: getFolderIcon(folder) - })) - ]; + // Create a map of existing standard sidebar items for quick lookup + const standardSidebarItems = initialSidebarItems.filter(item => + standardFoldersSet.has(item.view) + ); - setSidebarItems(updatedSidebarItems); + // Only create new items for custom folders + const customSidebarItems = customFolders.map((folder: string) => ({ + view: folder, + label: folder, + icon: getFolderIcon(folder) + })); + + // Update sidebar items efficiently + setSidebarItems([...standardSidebarItems, ...customSidebarItems]); } - // Process emails - const processedEmails = (data.emails || []) - .map((email: any) => ({ + // Process emails with a more efficient mapping approach + if (data.emails && data.emails.length > 0) { + const processedEmails = data.emails.map((email: any) => ({ id: email.id, accountId: 1, from: email.from || '', @@ -681,16 +774,22 @@ export default function CourrierPage() { flags: email.flags || [], hasAttachments: email.hasAttachments || false })); - - setEmails(processedEmails); - setHasMore(data.hasMore); - - // Calculate unread count - const unreadInboxEmails = processedEmails.filter( - (email: any) => !email.read && email.folder === 'INBOX' - ).length; - setUnreadCount(unreadInboxEmails); - + + setEmails(processedEmails); + setHasMore(data.hasMore); + + // Calculate unread count more efficiently by directly counting during the map phase + if (currentView === 'INBOX') { + const unreadCount = processedEmails.reduce((count: number, email: Email) => + (!email.read && email.folder === 'INBOX') ? count + 1 : count, 0 + ); + setUnreadCount(unreadCount); + } + } else { + // Clear emails if none were returned + setEmails([]); + setHasMore(false); + } } catch (error) { console.error('Error refreshing emails:', error); setError(error instanceof Error ? error.message : 'Failed to refresh emails'); @@ -837,8 +936,34 @@ export default function CourrierPage() { const email = emails.find(e => e.id === emailId); if (!email) return; + // Check if we have the content cached + const cachedContent = emailContentCache.current.get(emailId); + // Set selected email immediately to show the UI - setSelectedEmail(email); + setSelectedEmail(cachedContent || email); + + // If we already have the full content cached, we don't need to fetch it again + if (cachedContent?.content && cachedContent.content.length > 100) { + // Just update read status if needed + if (!email.read) { + try { + fetch(`/api/courrier/${emailId}/mark-read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + // Update the email in state optimistically + setEmails(emails.map(e => + e.id === emailId ? { ...e, read: true } : e + )); + } catch (error) { + console.error('Error marking as read:', error); + } + } + return; + } // Set loading state setContentLoading(true); @@ -869,7 +994,8 @@ export default function CourrierPage() { // Fetch the full email content const response = await fetch(`/api/courrier/${emailId}`, { - signal: controller.signal + signal: controller.signal, + cache: 'force-cache' }); clearTimeout(timeoutId); @@ -880,22 +1006,50 @@ export default function CourrierPage() { const emailData = await response.json(); - // Update the selected email with full content - setSelectedEmail({ + // Pre-parse the email content on the client side to avoid additional API calls + let parsedContent = emailData.content; + + // Only make parse-email call if we need to (large emails or complex formats) + if (emailData.content && emailData.content.length > 0 && !emailData.content.includes(' e.id === emailId - ? { - ...e, - content: emailData.content || '', - read: true, - attachments: emailData.attachments || [] - } + ? fullEmail : e )); } catch (error) {