diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 46733579..ae3397d0 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -41,12 +41,16 @@ import EmailDetailView from '@/components/email/EmailDetailView'; import ComposeEmail from '@/components/email/ComposeEmail'; import { DeleteConfirmDialog } from '@/components/email/EmailDialogs'; -// Import the custom hook +// Import the custom hooks import { useCourrier, EmailData } from '@/hooks/use-courrier'; +import { useEmailState } from '@/hooks/use-email-state'; // Import the prefetching function import { prefetchFolderEmails } from '@/lib/services/prefetch-service'; +// Import Account type from the reducer +import { Account } from '@/lib/reducers/emailReducer'; + // Simplified version for this component function SimplifiedLoadingFix() { // In production, don't render anything @@ -62,14 +66,6 @@ function SimplifiedLoadingFix() { ); } -interface Account { - id: string; - name: string; - email: string; - color: string; - folders: string[]; -} - interface EmailWithFlags { id: string; read?: boolean; diff --git a/hooks/use-email-state.ts b/hooks/use-email-state.ts new file mode 100644 index 00000000..7a8ae259 --- /dev/null +++ b/hooks/use-email-state.ts @@ -0,0 +1,585 @@ +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 + }; +}; \ No newline at end of file diff --git a/lib/reducers/emailReducer.ts b/lib/reducers/emailReducer.ts new file mode 100644 index 00000000..e8bbf802 --- /dev/null +++ b/lib/reducers/emailReducer.ts @@ -0,0 +1,324 @@ +import { Email } from '@/hooks/use-courrier'; + +// Define all possible state types +export interface Account { + id: string; + name: string; + email: string; + color: string; + folders: string[]; +} + +export interface EmailState { + accounts: Account[]; + selectedAccount: Account | null; + selectedFolders: Record; + currentFolder: string; + emails: Email[]; + selectedEmail: Email | null; + selectedEmailIds: string[]; + isLoading: boolean; + error: string | null; + page: number; + perPage: number; + totalPages: number; + totalEmails: number; + mailboxes: string[]; + unreadCountMap: Record>; + showFolders: boolean; +} + +// Define all possible action types +export type EmailAction = + | { type: 'SET_ACCOUNTS', payload: Account[] } + | { type: 'SELECT_ACCOUNT', payload: Account } + | { type: 'CHANGE_FOLDER', payload: { folder: string, accountId: string } } + | { type: 'SET_EMAILS', payload: Email[] } + | { type: 'APPEND_EMAILS', payload: Email[] } + | { type: 'SELECT_EMAIL', payload: { emailId: string, accountId: string, folder: string, email: Email | null } } + | { type: 'TOGGLE_EMAIL_SELECTION', payload: string } + | { type: 'TOGGLE_SELECT_ALL' } + | { type: 'CLEAR_SELECTED_EMAILS' } + | { type: 'SET_LOADING', payload: boolean } + | { type: 'SET_ERROR', payload: string | null } + | { type: 'SET_PAGE', payload: number } + | { type: 'INCREMENT_PAGE' } + | { type: 'SET_TOTAL_PAGES', payload: number } + | { type: 'SET_TOTAL_EMAILS', payload: number } + | { type: 'SET_MAILBOXES', payload: string[] } + | { type: 'UPDATE_UNREAD_COUNT', payload: { accountId: string, folder: string, count: number } } + | { type: 'TOGGLE_SHOW_FOLDERS', payload: boolean } + | { type: 'MARK_EMAIL_AS_READ', payload: { emailId: string, isRead: boolean, accountId?: string } }; + +// Initial state +export const initialState: EmailState = { + accounts: [], + selectedAccount: null, + selectedFolders: {}, + currentFolder: 'INBOX', + emails: [], + selectedEmail: null, + selectedEmailIds: [], + isLoading: false, + error: null, + page: 1, + perPage: 20, + totalPages: 0, + totalEmails: 0, + mailboxes: [], + unreadCountMap: {}, + showFolders: false +}; + +// Helper functions for consistency +export const normalizeFolderAndAccount = (folder: string, accountId?: string) => { + let normalizedFolder: string; + let effectiveAccountId: string = accountId || 'default'; + + // First, handle the folder format + if (folder.includes(':')) { + // Extract parts if folder already has a prefix + const parts = folder.split(':'); + const folderAccountId = parts[0]; + normalizedFolder = parts[1]; + + // If explicit accountId is provided, it ALWAYS takes precedence + if (accountId) { + console.log(`Using provided accountId (${accountId}) over folder prefix (${folderAccountId})`); + effectiveAccountId = accountId; + } else { + effectiveAccountId = folderAccountId; + } + } else { + // No folder prefix, use the folder name as is + normalizedFolder = folder; + } + + return { + normalizedFolder, + effectiveAccountId, + prefixedFolder: `${effectiveAccountId}:${normalizedFolder}` + }; +}; + +// Reducer function +export function emailReducer(state: EmailState, action: EmailAction): EmailState { + console.log(`[EMAIL_REDUCER] Action: ${action.type}`, action); + + switch (action.type) { + case 'SET_ACCOUNTS': + return { + ...state, + accounts: action.payload + }; + + case 'SELECT_ACCOUNT': { + // This is a critical action that needs special handling + const account = action.payload; + const inboxFolder = `${account.id}:INBOX`; + + console.log(`[EMAIL_REDUCER] Selecting account: ${account.email} (${account.id})`); + + // Return a completely new state that's atomically consistent + return { + ...state, + selectedAccount: account, + currentFolder: inboxFolder, + selectedFolders: { + ...state.selectedFolders, + [account.id]: inboxFolder + }, + // Clear email selections as part of the atomic account switch + selectedEmail: null, + selectedEmailIds: [], + emails: [], + isLoading: true, + showFolders: true, + page: 1 + }; + } + + case 'CHANGE_FOLDER': { + const { folder, accountId } = action.payload; + + // Use our helper to ensure consistent folder/account handling + const { normalizedFolder, effectiveAccountId, prefixedFolder } = + normalizeFolderAndAccount(folder, accountId); + + console.log(`[EMAIL_REDUCER] Changing folder to: ${prefixedFolder} (account: ${effectiveAccountId})`); + + // Return a new state with consistent folder and account info + return { + ...state, + currentFolder: prefixedFolder, + selectedFolders: { + ...state.selectedFolders, + [effectiveAccountId]: prefixedFolder + }, + // Clear email-specific state when changing folders + selectedEmail: null, + selectedEmailIds: [], + emails: [], + isLoading: true, + page: 1 + }; + } + + case 'SET_EMAILS': + return { + ...state, + emails: action.payload, + isLoading: false + }; + + case 'APPEND_EMAILS': { + // Create a set of existing email IDs to avoid duplicates + const existingIds = new Set(state.emails.map(email => email.id)); + + // Filter out any duplicates before appending + const newEmails = action.payload.filter(email => !existingIds.has(email.id)); + + // Combine and sort emails by date (newest first) + const combinedEmails = [...state.emails, ...newEmails].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + + return { + ...state, + emails: combinedEmails, + isLoading: false + }; + } + + case 'SELECT_EMAIL': + return { + ...state, + selectedEmail: action.payload.email, + // If an email is selected, add it to the selectedEmailIds if not already there + selectedEmailIds: action.payload.email && !state.selectedEmailIds.includes(action.payload.emailId) + ? [...state.selectedEmailIds, action.payload.emailId] + : state.selectedEmailIds + }; + + case 'TOGGLE_EMAIL_SELECTION': { + const emailId = action.payload; + const isSelected = state.selectedEmailIds.includes(emailId); + + return { + ...state, + selectedEmailIds: isSelected + ? state.selectedEmailIds.filter(id => id !== emailId) + : [...state.selectedEmailIds, emailId] + }; + } + + case 'TOGGLE_SELECT_ALL': { + // If all emails are already selected, clear the selection + const allEmailIds = state.emails.map(email => email.id); + const allSelected = allEmailIds.every(id => state.selectedEmailIds.includes(id)); + + return { + ...state, + selectedEmailIds: allSelected ? [] : allEmailIds + }; + } + + case 'CLEAR_SELECTED_EMAILS': + return { + ...state, + selectedEmailIds: [], + selectedEmail: null + }; + + case 'SET_LOADING': + return { + ...state, + isLoading: action.payload + }; + + case 'SET_ERROR': + return { + ...state, + error: action.payload, + isLoading: false + }; + + case 'SET_PAGE': + return { + ...state, + page: action.payload + }; + + case 'INCREMENT_PAGE': + return { + ...state, + page: state.page + 1 + }; + + case 'SET_TOTAL_PAGES': + return { + ...state, + totalPages: action.payload + }; + + case 'SET_TOTAL_EMAILS': + return { + ...state, + totalEmails: action.payload + }; + + case 'SET_MAILBOXES': + return { + ...state, + mailboxes: action.payload + }; + + case 'UPDATE_UNREAD_COUNT': { + const { accountId, folder, count } = action.payload; + + return { + ...state, + unreadCountMap: { + ...state.unreadCountMap, + [accountId]: { + ...(state.unreadCountMap[accountId] || {}), + [folder]: count + } + } + }; + } + + case 'TOGGLE_SHOW_FOLDERS': + return { + ...state, + showFolders: action.payload + }; + + case 'MARK_EMAIL_AS_READ': { + const { emailId, isRead, accountId } = action.payload; + + // Update emails list + const updatedEmails = state.emails.map(email => + (email.id === emailId && (!accountId || email.accountId === accountId)) + ? { ...email, flags: { ...email.flags, seen: isRead } } + : email + ); + + // Update selected email if it matches + const updatedSelectedEmail = state.selectedEmail && + state.selectedEmail.id === emailId && + (!accountId || state.selectedEmail.accountId === accountId) + ? { ...state.selectedEmail, flags: { ...state.selectedEmail.flags, seen: isRead } } + : state.selectedEmail; + + return { + ...state, + emails: updatedEmails, + selectedEmail: updatedSelectedEmail + }; + } + + default: + return state; + } +} \ No newline at end of file