import { useState, useCallback, useEffect, useRef } from 'react'; import { useSession } from 'next-auth/react'; import { useToast } from './use-toast'; import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter'; import { getCachedEmailsWithTimeout, refreshEmailsInBackground } from '@/lib/services/prefetch-service'; export interface EmailAddress { name: string; address: string; } export interface Email { id: string; from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; subject: string; content: string; preview?: string; date: string; read: boolean; starred: boolean; attachments?: { filename: string; contentType: string; size: number; content?: string }[]; folder: string; hasAttachments: boolean; contentFetched?: boolean; } export interface EmailListResult { emails: Email[]; totalEmails: number; page: number; perPage: number; totalPages: number; folder: string; mailboxes: string[]; } export interface EmailData { to: string; cc?: string; bcc?: string; subject: string; body: string; attachments?: Array<{ name: string; content: string; type: string; }>; } export type MailFolder = string; // Near the top of the file, before the useCourrier hook interface EmailResponse { emails: Email[]; total: number; totalPages: number; hasMore: boolean; } // Hook for managing email operations export const useCourrier = () => { // State for email data const [emails, setEmails] = useState([]); const [selectedEmail, setSelectedEmail] = useState(null); const [selectedEmailIds, setSelectedEmailIds] = useState([]); const [currentFolder, setCurrentFolder] = useState('INBOX'); const [mailboxes, setMailboxes] = useState([]); // State for UI const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(20); const [totalEmails, setTotalEmails] = useState(0); const [totalPages, setTotalPages] = useState(0); const [hasMore, setHasMore] = useState(false); // Auth and notifications const { data: session } = useSession(); const { toast } = useToast(); // Add the missing refs const loadingRequestsRef = useRef>(new Set()); const loadMoreRef = useRef(0); // Load emails from the server const loadEmails = useCallback( async (folder = currentFolder, pageToLoad = page, resetList = true, isInitial = false) => { if (!session?.user?.id || isLoading) return; // Track this request to avoid duplicates const requestKey = `${folder}_${pageToLoad}`; if (loadingRequestsRef.current.has(requestKey)) { console.log(`Skipping duplicate request for ${requestKey}`); return; } loadingRequestsRef.current.add(requestKey); setIsLoading(true); try { // Get emails for the current folder const response = await getEmails(session.user.id, folder, pageToLoad); // Update state based on response if (resetList) { setEmails(response.emails); } else { setEmails(prev => [...prev, ...response.emails]); } setTotalEmails(response.total); setTotalPages(response.totalPages); setHasMore(response.hasMore); setPage(pageToLoad); if (folder !== currentFolder) { setCurrentFolder(folder); } // Clear errors setError(null); } catch (error) { console.error('Error loading emails:', error); setError(error instanceof Error ? error.message : 'Failed to load emails'); toast({ variant: "destructive", title: "Error", description: "Failed to load emails" }); } finally { setIsLoading(false); // Clear the loading request tracker loadingRequestsRef.current.delete(requestKey); } }, [session?.user?.id, currentFolder, page, isLoading, toast] ); // Load emails when folder or page changes useEffect(() => { if (session?.user?.id) { // If page is greater than 1, we're loading more emails const isLoadingMore = page > 1; // Add a small delay to prevent rapid consecutive loads const loadTimer = setTimeout(() => { loadEmails(currentFolder, page, false, false); }, 50); // If we're loading the first page, publish an event to reset scroll position if (page === 1 && typeof window !== 'undefined') { // Use a custom event to communicate with the EmailList component const event = new CustomEvent('reset-email-scroll'); window.dispatchEvent(event); } return () => clearTimeout(loadTimer); } }, [currentFolder, page, session?.user?.id, loadEmails]); // Fetch a single email's content const fetchEmailContent = useCallback(async (emailId: string) => { try { const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(currentFolder)}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error('Error fetching email content:', error); throw error; } }, [currentFolder]); // Select an email to view const handleEmailSelect = useCallback(async (emailId: string) => { setIsLoading(true); try { // Find the email in the current list const email = emails.find(e => e.id === emailId); if (!email) { throw new Error('Email not found'); } // If content is not fetched, get the full content if (!email.contentFetched) { const fullEmail = await fetchEmailContent(emailId); // Merge the full content with the email const updatedEmail = { ...email, content: fullEmail.content, attachments: fullEmail.attachments, contentFetched: true }; // Update the email in the list setEmails(emails.map(e => e.id === emailId ? updatedEmail : e)); setSelectedEmail(updatedEmail); } else { setSelectedEmail(email); } // Mark the email as read if it's not already if (!email.read) { markEmailAsRead(emailId, true); } } catch (err) { console.error('Error selecting email:', err); toast({ variant: "destructive", title: "Error", description: "Could not load email content" }); } finally { setIsLoading(false); } }, [emails, fetchEmailContent, toast]); // Mark an email as read/unread const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => { try { const response = await fetch(`/api/courrier/${emailId}/mark-read`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isRead, folder: currentFolder }) }); if (!response.ok) { throw new Error('Failed to mark email as read'); } // Update the email in the list setEmails(emails.map(email => email.id === emailId ? { ...email, read: isRead } : email )); // If the selected email is the one being marked, update it too if (selectedEmail && selectedEmail.id === emailId) { setSelectedEmail({ ...selectedEmail, read: isRead }); } return true; } catch (error) { console.error('Error marking email as read:', error); return false; } }, [emails, selectedEmail, currentFolder]); // Toggle starred status for an email const toggleStarred = useCallback(async (emailId: string) => { const email = emails.find(e => e.id === emailId); if (!email) return; const newStarredStatus = !email.starred; try { const response = await fetch(`/api/courrier/${emailId}/star`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ starred: newStarredStatus, folder: currentFolder }) }); if (!response.ok) { throw new Error('Failed to toggle star status'); } // Update the email in the list setEmails(emails.map(email => email.id === emailId ? { ...email, starred: newStarredStatus } : email )); // If the selected email is the one being starred, update it too if (selectedEmail && selectedEmail.id === emailId) { setSelectedEmail({ ...selectedEmail, starred: newStarredStatus }); } } catch (error) { console.error('Error toggling star status:', error); toast({ variant: "destructive", title: "Error", description: "Could not update star status" }); } }, [emails, selectedEmail, currentFolder, toast]); // Send an email const sendEmail = useCallback(async (emailData: EmailData) => { if (!session?.user?.id) { toast({ variant: "destructive", title: "Error", description: "You must be logged in to send emails" }); return { success: false, error: "Not authenticated" }; } setIsSending(true); try { const response = await fetch('/api/courrier/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData) }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || 'Failed to send email'); } toast({ title: "Success", description: "Email sent successfully" }); return { success: true, messageId: result.messageId }; } catch (error) { console.error('Error sending email:', error); toast({ variant: "destructive", title: "Error", description: error instanceof Error ? error.message : 'Failed to send email' }); return { success: false, error: error instanceof Error ? error.message : 'Failed to send email' }; } finally { setIsSending(false); } }, [session?.user?.id, toast]); // Delete selected emails const deleteEmails = useCallback(async (emailIds: string[]) => { if (emailIds.length === 0) return; setIsDeleting(true); try { const response = await fetch('/api/courrier/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailIds, folder: currentFolder }) }); if (!response.ok) { throw new Error('Failed to delete emails'); } // Remove the deleted emails from the list setEmails(emails.filter(email => !emailIds.includes(email.id))); // Clear selection if the selected email was deleted if (selectedEmail && emailIds.includes(selectedEmail.id)) { setSelectedEmail(null); } // Clear selected IDs setSelectedEmailIds([]); toast({ title: "Success", description: `${emailIds.length} email(s) deleted` }); } catch (error) { console.error('Error deleting emails:', error); toast({ variant: "destructive", title: "Error", description: "Failed to delete emails" }); } finally { setIsDeleting(false); } }, [emails, selectedEmail, currentFolder, toast]); // Toggle selection of an email const toggleEmailSelection = useCallback((emailId: string) => { setSelectedEmailIds(prev => { if (prev.includes(emailId)) { return prev.filter(id => id !== emailId); } else { return [...prev, emailId]; } }); }, []); // Select all emails const toggleSelectAll = useCallback(() => { if (selectedEmailIds.length === emails.length) { setSelectedEmailIds([]); } else { setSelectedEmailIds(emails.map(email => email.id)); } }, [emails, selectedEmailIds]); // Change the current folder const changeFolder = useCallback((folder: MailFolder) => { setCurrentFolder(folder); setPage(1); setSelectedEmail(null); setSelectedEmailIds([]); }, []); // Search emails const searchEmails = useCallback((query: string) => { setSearchQuery(query); setPage(1); loadEmails(); }, [loadEmails]); // Format an email for reply or forward const formatEmailForAction = useCallback((email: Email, type: 'reply' | 'reply-all' | 'forward') => { if (!email) return null; return formatEmailForReplyOrForward(email, type); }, []); /** * Fetches emails from the API */ const getEmails = async (userId: string, folder: string, page: number): Promise => { // Build query params const queryParams = new URLSearchParams({ folder: folder, page: page.toString(), perPage: perPage.toString() }); if (searchQuery) { queryParams.set('search', searchQuery); } // Fetch emails from API const response = await fetch(`/api/courrier?${queryParams.toString()}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch emails'); } const data = await response.json(); return { emails: Array.isArray(data.emails) ? data.emails : [], total: data.totalEmails || 0, totalPages: data.totalPages || 0, hasMore: data.totalPages > page }; }; /** * Prefetches emails for a specific folder */ const prefetchFolderEmails = async (userId: string, folder: string, startPage: number, endPage: number) => { try { for (let p = startPage; p <= endPage; p++) { await getEmails(userId, folder, p); // Add small delay between requests if (p < endPage) await new Promise(r => setTimeout(r, 500)); } } catch (error) { console.error("Error prefetching emails:", error); } }; // Update loadMoreEmails const loadMoreEmails = useCallback(async () => { if (isLoading || !hasMore || !session) { return; } // Don't allow loading more if we've loaded too recently const now = Date.now(); const lastLoadTime = loadMoreRef.current || 0; if (now - lastLoadTime < 1000) { // Throttle to once per second console.log('Throttling loadMoreEmails - too many requests'); return; } // Track when we last attempted to load more loadMoreRef.current = now; // Load the next page console.log(`Loading more emails for ${currentFolder}, page ${page + 1}`); return loadEmails(currentFolder, page + 1, false, false); }, [isLoading, hasMore, session, currentFolder, page, loadEmails]); // Return all the functionality and state values return { // Data emails, selectedEmail, selectedEmailIds, currentFolder, mailboxes, isLoading, isSending, isDeleting, error, searchQuery, page, perPage, totalEmails, totalPages, hasMore, // Functions loadEmails, handleEmailSelect, markEmailAsRead, toggleStarred, sendEmail, deleteEmails, toggleEmailSelection, toggleSelectAll, changeFolder, searchEmails, formatEmailForAction, setPage, setPerPage, setSearchQuery, loadMoreEmails, }; };