import { useReducer, useCallback, useEffect } from 'react'; import { useSession } from 'next-auth/react'; import { useToast } from './use-toast'; import { emailReducer, initialState, EmailState, EmailAction, normalizeFolderAndAccount, Account } from '@/lib/reducers/emailReducer'; import { getCachedEmailsWithTimeout, refreshEmailsInBackground } from '@/lib/services/prefetch-service'; import { Email, EmailData } from './use-courrier'; import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter'; export const useEmailState = () => { const [state, dispatch] = useReducer(emailReducer, initialState); const { data: session } = useSession(); const { toast } = useToast(); // Helper function to log operations const logEmailOp = useCallback((operation: string, details: string, data?: any) => { const timestamp = new Date().toISOString().split('T')[1].substring(0, 12); console.log(`[${timestamp}][EMAIL-STATE][${operation}] ${details}`); if (data) { console.log(`[${timestamp}][EMAIL-STATE][DATA]`, data); } }, []); // Load emails from the server const loadEmails = useCallback(async (isLoadMore = false, accountId?: string) => { if (!session?.user?.id) return; dispatch({ type: 'SET_LOADING', payload: true }); try { // Get normalized parameters using helper function const { normalizedFolder, effectiveAccountId, prefixedFolder } = normalizeFolderAndAccount(state.currentFolder, accountId); logEmailOp('LOAD_EMAILS', `Loading emails for ${prefixedFolder} (account: ${effectiveAccountId})`); // Construct query parameters const queryParams = new URLSearchParams({ folder: normalizedFolder, page: state.page.toString(), perPage: state.perPage.toString(), accountId: effectiveAccountId }); // Try to get cached emails first logEmailOp('CACHE_CHECK', `Checking cache for ${prefixedFolder}, page: ${state.page}`); const cachedEmails = await getCachedEmailsWithTimeout( session.user.id, prefixedFolder, state.page, state.perPage, 100, effectiveAccountId ); if (cachedEmails) { logEmailOp('CACHE_HIT', `Using cached data for ${prefixedFolder}`); // Ensure cached data has emails array property if (Array.isArray(cachedEmails.emails)) { // Dispatch appropriate action based on if we're loading more dispatch({ type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS', payload: cachedEmails.emails }); // Set pagination info from cache if available if (cachedEmails.totalEmails) { dispatch({ type: 'SET_TOTAL_EMAILS', payload: cachedEmails.totalEmails }); } if (cachedEmails.totalPages) { dispatch({ type: 'SET_TOTAL_PAGES', payload: cachedEmails.totalPages }); } // Update available mailboxes if provided if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) { dispatch({ type: 'SET_MAILBOXES', payload: cachedEmails.mailboxes }); } } // Still refresh in background for fresh data logEmailOp('BACKGROUND_REFRESH', `Starting background refresh for ${prefixedFolder}`); refreshEmailsInBackground( session.user.id, normalizedFolder, state.page, state.perPage, effectiveAccountId ).catch(err => { console.error('Background refresh error:', err); }); return; } // Fetch emails from API if no cache hit logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}`); 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(); // Ensure all emails have proper account ID and folder format if (Array.isArray(data.emails)) { data.emails.forEach((email: Email) => { // If email doesn't have an accountId, set it to the effective one if (!email.accountId) { email.accountId = effectiveAccountId; } // Ensure folder has the proper prefix format if (email.folder && !email.folder.includes(':')) { email.folder = `${email.accountId}:${email.folder}`; } }); } // Update state with fetched data dispatch({ type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS', payload: Array.isArray(data.emails) ? data.emails : [] }); if (data.totalEmails) { dispatch({ type: 'SET_TOTAL_EMAILS', payload: data.totalEmails }); } if (data.totalPages) { dispatch({ type: 'SET_TOTAL_PAGES', payload: data.totalPages }); } // Update available mailboxes if provided if (data.mailboxes && data.mailboxes.length > 0) { dispatch({ type: 'SET_MAILBOXES', payload: data.mailboxes }); } } catch (err) { logEmailOp('ERROR', `Failed to load emails: ${err instanceof Error ? err.message : String(err)}`); dispatch({ type: 'SET_ERROR', payload: err instanceof Error ? err.message : 'Failed to load emails' }); toast({ variant: "destructive", title: "Error", description: err instanceof Error ? err.message : 'Failed to load emails' }); } }, [session?.user?.id, state.currentFolder, state.page, state.perPage, toast, logEmailOp]); // Change folder const changeFolder = useCallback(async (folder: string, accountId?: string) => { logEmailOp('CHANGE_FOLDER', `Changing to folder ${folder} with account ${accountId || 'default'}`); try { // This will handle all the state updates in a single atomic operation dispatch({ type: 'CHANGE_FOLDER', payload: { folder, accountId: accountId || 'default' } }); // After dispatch, the state will be updated with consistent values // We'll load emails in the useEffect that watches for folder changes } catch (error) { logEmailOp('ERROR', `Failed to change folder: ${error instanceof Error ? error.message : String(error)}`); dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to change folder' }); } }, [logEmailOp]); // Select account const selectAccount = useCallback((account: Account) => { logEmailOp('SELECT_ACCOUNT', `Selecting account: ${account.email} (${account.id})`); // Handle the entire account selection in a single atomic operation dispatch({ type: 'SELECT_ACCOUNT', payload: account }); // The folder loading will be triggered by the useEffect watching for currentFolder changes }, [logEmailOp]); // Handle email selection const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folder: string) => { logEmailOp('SELECT_EMAIL', `Selecting email ${emailId} from account ${accountId} in folder ${folder}`); if (!emailId) { dispatch({ type: 'SELECT_EMAIL', payload: { emailId: '', accountId: '', folder: '', email: null } }); return; } try { // Find the email in the current list const existingEmail = state.emails.find(e => e.id === emailId); if (existingEmail && existingEmail.contentFetched) { // Use the existing email if it has content already dispatch({ type: 'SELECT_EMAIL', payload: { emailId, accountId, folder, email: existingEmail } }); // Mark as read if not already if (!existingEmail.flags.seen) { markEmailAsRead(emailId, true, accountId); } return; } // Need to fetch the email content dispatch({ type: 'SET_LOADING', payload: true }); // Extract account ID from folder name if present and none was explicitly provided const { normalizedFolder, effectiveAccountId } = normalizeFolderAndAccount(folder, accountId); // Fetch email content from API const response = await fetch(`/api/courrier/${emailId}?folder=${normalizedFolder}&accountId=${effectiveAccountId}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); } const emailData = await response.json(); // Mark the email as read on the server markEmailAsRead(emailId, true, effectiveAccountId); // Select the email dispatch({ type: 'SELECT_EMAIL', payload: { emailId, accountId: effectiveAccountId, folder, email: emailData } }); } catch (error) { logEmailOp('ERROR', `Failed to select email: ${error instanceof Error ? error.message : String(error)}`); dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to select email' }); } finally { dispatch({ type: 'SET_LOADING', payload: false }); } }, [state.emails, logEmailOp]); // Toggle email selection for multi-select const toggleEmailSelection = useCallback((emailId: string) => { dispatch({ type: 'TOGGLE_EMAIL_SELECTION', payload: emailId }); }, []); // Toggle select all const toggleSelectAll = useCallback(() => { dispatch({ type: 'TOGGLE_SELECT_ALL' }); }, []); // Mark email as read/unread const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean, accountId?: string) => { try { // Find the email to get its account ID if not provided const email = state.emails.find(e => e.id === emailId); const effectiveAccountId = accountId || email?.accountId || 'default'; const folder = email?.folder || state.currentFolder; // Extract normalized folder const { normalizedFolder } = normalizeFolderAndAccount(folder, effectiveAccountId); logEmailOp('MARK_READ', `Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in ${normalizedFolder}`); // Update UI state immediately (optimistic update) dispatch({ type: 'MARK_EMAIL_AS_READ', payload: { emailId, isRead, accountId: effectiveAccountId } }); // Make API call to update on server const response = await fetch(`/api/courrier/${emailId}/mark-read`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isRead, folder: normalizedFolder, accountId: effectiveAccountId }) }); if (!response.ok) { throw new Error('Failed to mark email as read'); } return true; } catch (error) { logEmailOp('ERROR', `Failed to mark email as read: ${error instanceof Error ? error.message : String(error)}`); toast({ variant: "destructive", title: "Error", description: 'Failed to update email read status' }); return false; } }, [state.emails, state.currentFolder, toast, logEmailOp]); // Toggle starred status const toggleStarred = useCallback(async (emailId: string) => { try { // Find the email in current list const email = state.emails.find(e => e.id === emailId); if (!email) { throw new Error('Email not found'); } const newFlaggedStatus = !email.flags.flagged; logEmailOp('TOGGLE_STAR', `Setting starred status to ${newFlaggedStatus} for email ${emailId}`); // TODO: Implement optimistic update // Make API call const response = await fetch(`/api/courrier/${emailId}/flag`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ flagged: newFlaggedStatus, folder: email.folder, accountId: email.accountId }) }); if (!response.ok) { throw new Error('Failed to update star status'); } // Reload emails to get updated state loadEmails(); return true; } catch (error) { logEmailOp('ERROR', `Failed to toggle star: ${error instanceof Error ? error.message : String(error)}`); toast({ variant: "destructive", title: "Error", description: 'Failed to update star status' }); return false; } }, [state.emails, toast, loadEmails, logEmailOp]); // Delete emails const deleteEmails = useCallback(async (emailIds: string[]) => { if (emailIds.length === 0) return; dispatch({ type: 'SET_LOADING', payload: true }); try { logEmailOp('DELETE', `Deleting ${emailIds.length} emails`); // Find the first email to get account ID and folder const firstEmail = state.emails.find(e => e.id === emailIds[0]); const accountId = firstEmail?.accountId || 'default'; const folder = firstEmail?.folder || state.currentFolder; const { normalizedFolder } = normalizeFolderAndAccount(folder, accountId); // Make API call to delete emails const response = await fetch('/api/courrier/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailIds, folder: normalizedFolder, accountId }) }); if (!response.ok) { throw new Error('Failed to delete emails'); } // Clear selections dispatch({ type: 'CLEAR_SELECTED_EMAILS' }); // Reload emails to get updated list loadEmails(); toast({ title: "Emails Deleted", description: `${emailIds.length} email(s) moved to trash` }); return true; } catch (error) { logEmailOp('ERROR', `Failed to delete emails: ${error instanceof Error ? error.message : String(error)}`); toast({ variant: "destructive", title: "Error", description: 'Failed to delete emails' }); return false; } finally { dispatch({ type: 'SET_LOADING', payload: false }); } }, [state.emails, state.currentFolder, toast, loadEmails, logEmailOp]); // Send email const sendEmail = useCallback(async (emailData: EmailData) => { dispatch({ type: 'SET_LOADING', payload: true }); try { logEmailOp('SEND', `Sending email to ${emailData.to}`); // Make API call to send email const response = await fetch('/api/courrier/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to send email'); } const result = await response.json(); toast({ title: "Email Sent", description: "Your message has been sent successfully" }); // Refresh emails to show the sent email loadEmails(); return { success: true, ...result }; } catch (error) { logEmailOp('ERROR', `Failed to send email: ${error instanceof Error ? error.message : String(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 { dispatch({ type: 'SET_LOADING', payload: false }); } }, [toast, loadEmails, logEmailOp]); // Search emails const searchEmails = useCallback(async (query: string) => { // Set loading state dispatch({ type: 'SET_LOADING', payload: true }); try { if (!session?.user?.id) return; logEmailOp('SEARCH', `Searching for "${query}" in ${state.currentFolder}`); // Extract account ID from current folder const { normalizedFolder, effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder); // Construct query params for search const queryParams = new URLSearchParams({ folder: normalizedFolder, search: query, accountId: effectiveAccountId }); // Call API for search const response = await fetch(`/api/courrier/search?${queryParams.toString()}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to search emails'); } const results = await response.json(); // Update emails with search results dispatch({ type: 'SET_EMAILS', payload: results.emails || [] }); if (results.totalEmails) { dispatch({ type: 'SET_TOTAL_EMAILS', payload: results.totalEmails }); } if (results.totalPages) { dispatch({ type: 'SET_TOTAL_PAGES', payload: results.totalPages }); } } catch (error) { logEmailOp('ERROR', `Search failed: ${error instanceof Error ? error.message : String(error)}`); dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to search emails' }); toast({ variant: "destructive", title: "Error", description: 'Failed to search emails' }); } finally { dispatch({ type: 'SET_LOADING', payload: false }); } }, [session?.user?.id, state.currentFolder, toast, logEmailOp]); // Format email for reply, reply all, or forward const formatEmailForAction = useCallback((email: any, type: 'reply' | 'reply-all' | 'forward') => { return formatEmailForReplyOrForward(email, type); }, []); // Update page const setPage = useCallback((page: number) => { dispatch({ type: 'SET_PAGE', payload: page }); }, []); // Set emails directly const setEmails = useCallback((emails: Email[]) => { dispatch({ type: 'SET_EMAILS', payload: emails }); }, []); // Handle loading more emails const handleLoadMore = useCallback(() => { if (state.page < state.totalPages && !state.isLoading) { dispatch({ type: 'INCREMENT_PAGE' }); } }, [state.page, state.totalPages, state.isLoading]); // Effect to load emails when folder changes useEffect(() => { if (session?.user?.id && state.currentFolder) { // Extract account ID from folder for consistent loading const { effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder); logEmailOp('FOLDER_CHANGE', `Loading emails for folder ${state.currentFolder} with account ${effectiveAccountId}`); // Load emails with the correct account ID loadEmails(false, effectiveAccountId); } }, [session?.user?.id, state.currentFolder, loadEmails, logEmailOp]); // Effect to load more emails when page changes useEffect(() => { if (session?.user?.id && state.page > 1) { // Extract account ID for consistency const { effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder); logEmailOp('PAGINATION', `Loading page ${state.page} for ${state.currentFolder} with account ${effectiveAccountId}`); // Load more emails with the correct account ID loadEmails(true, effectiveAccountId); } }, [session?.user?.id, state.page, state.currentFolder, loadEmails, logEmailOp]); // Return all state values and actions return { // State values ...state, // Actions loadEmails, handleEmailSelect, toggleEmailSelection, toggleSelectAll, markEmailAsRead, toggleStarred, changeFolder, deleteEmails, sendEmail, searchEmails, formatEmailForAction, setPage, setEmails, selectAccount, handleLoadMore }; };