diff --git a/hooks/use-email-state.ts b/hooks/use-email-state.ts index 58d61c06..ec03e0c3 100644 --- a/hooks/use-email-state.ts +++ b/hooks/use-email-state.ts @@ -1,4 +1,4 @@ -import { useReducer, useCallback, useEffect } from 'react'; +import { useReducer, useCallback, useEffect, useRef } from 'react'; import { useSession } from 'next-auth/react'; import { useToast } from './use-toast'; import { @@ -681,8 +681,13 @@ export const useEmailState = () => { } }); - // Log the unread counts for debugging - logEmailOp('UNREAD_COUNTS', 'Updated unread counts:', tempUnreadMap); + // Store the current update for comparison + if (!(window as any).__lastUnreadUpdate) { + (window as any).__lastUnreadUpdate = { + timestamp: 0, + map: {} + }; + } // Check if the unread counts have actually changed before updating state let hasChanged = false; @@ -696,128 +701,163 @@ export const useEmailState = () => { }); }); - // Only update if there are actual changes to avoid infinite updates - if (hasChanged) { + // Check if we've updated recently + const now = Date.now(); + const lastUpdate = (window as any).__lastUnreadUpdate; + const timeSinceLastUpdate = now - lastUpdate.timestamp; + + // If changes found and not updated too recently, update state + if (hasChanged && timeSinceLastUpdate > 500) { + // Log only on actual change + logEmailOp('UNREAD_COUNTS', 'Updated unread counts:', tempUnreadMap); + // Create a single dispatch to update all counts at once - const updatedMap = { ...state.unreadCountMap }; - - Object.entries(tempUnreadMap).forEach(([accountId, folderCounts]) => { - updatedMap[accountId] = { ...updatedMap[accountId] || {}, ...folderCounts }; - }); - - // Replace the entire unread count map with one action dispatch({ type: 'SET_UNREAD_COUNTS', - payload: updatedMap + payload: tempUnreadMap }); + + // Update timestamp of last update + lastUpdate.timestamp = now; + lastUpdate.map = tempUnreadMap; } }, [state.emails, state.accounts, state.unreadCountMap, dispatch, logEmailOp]); // Call updateUnreadCounts when relevant state changes useEffect(() => { - // Only update unread counts when emails or flag status changes - // NOT when the unreadCountMap itself changes (that would cause infinite loop) - const updateCountsWithDebounce = setTimeout(() => { - updateUnreadCounts(); - }, 300); // Debounce to handle multiple email updates + if (!state.emails || state.emails.length === 0) return; - return () => clearTimeout(updateCountsWithDebounce); + // Debounce unread count updates to prevent rapid multiple updates + let updateTimeoutId: ReturnType; + + const debounceMs = 500; // 500ms debounce + + // Function to call after debounce period + const debouncedUpdate = () => { + updateTimeoutId = setTimeout(() => { + updateUnreadCounts(); + }, debounceMs); + }; + + // Clear any existing timeout and start a new one + debouncedUpdate(); + + // Cleanup timeout on unmount or state change + return () => { + clearTimeout(updateTimeoutId); + }; // Deliberately exclude unreadCountMap to prevent infinite loops - }, [updateUnreadCounts, state.emails]); + }, [state.emails, updateUnreadCounts]); // Fetch unread counts from API const fetchUnreadCounts = useCallback(async () => { - if (!session?.user?.id) return; + // Don't fetch if user is not logged in + if (!session?.user) return; + + // Don't fetch if we're already fetching + if (state.isLoadingUnreadCounts) return; + + // Skip fetching if an email was viewed recently (within last 2 seconds) + const now = Date.now(); + const lastViewedTimestamp = (window as any).__lastViewedEmailTimestamp || 0; + if (lastViewedTimestamp && now - lastViewedTimestamp < 2000) { + return; + } + + // Reset failure tracking if it's been more than 1 minute since last failure + if ((window as any).__unreadCountFailures && now - (window as any).__unreadCountFailures > 60000) { + (window as any).__unreadCountFailures = 0; + } + + // Exponential backoff for failures + if ((window as any).__unreadCountFailures > 0) { + const backoffMs = Math.min(30000, 1000 * Math.pow(2, (window as any).__unreadCountFailures - 1)); + if ((window as any).__unreadCountFailures && now - (window as any).__unreadCountFailures < backoffMs) { + return; + } + } try { + dispatch({ type: 'SET_LOADING_UNREAD_COUNTS', payload: true }); + + const timeBeforeCall = performance.now(); logEmailOp('FETCH_UNREAD', 'Fetching unread counts from API'); - // Make API call to get unread counts for all folders - const response = await fetch('/api/courrier/unread-counts'); + const response = await fetch('/api/courrier/unread-counts', { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); if (!response.ok) { - throw new Error('Failed to fetch unread counts'); - } - - const data = await response.json(); - - if (data.counts && typeof data.counts === 'object') { - logEmailOp('UNREAD_API', 'Received unread counts from API', data.counts); + // If request failed, increment failure count but cap it + (window as any).__unreadCountFailures = Math.min((window as any).__unreadCountFailures || 0 + 1, 10); + const failures = (window as any).__unreadCountFailures; - // Merge with existing unread counts rather than replacing - const mergedCounts = { ...state.unreadCountMap }; - - // Update unread counts in state - Object.entries(data.counts).forEach(([accountId, folderCounts]: [string, any]) => { - if (!mergedCounts[accountId]) { - mergedCounts[accountId] = {}; + if (failures > 3) { + // After 3 failures, slow down requests with exponential backoff + const backoffTime = Math.min(Math.pow(2, failures - 3) * 1000, 30000); // Max 30 seconds + logEmailOp('FETCH_UNREAD', `API failure #${failures}, backing off for ${backoffTime}ms`); + + // Schedule next attempt with backoff + if ((window as any).__failureBackoffTimer) { + clearTimeout((window as any).__failureBackoffTimer); } - Object.entries(folderCounts).forEach(([folder, count]: [string, any]) => { - mergedCounts[accountId][folder] = typeof count === 'number' ? count : 0; - }); - }); + (window as any).__failureBackoffTimer = setTimeout(() => { + fetchUnreadCounts(); + }, backoffTime); + + throw new Error(`Failed to fetch unread counts: ${response.status}`); + } + } else { + // Reset failure counter on success + (window as any).__unreadCountFailures = 0; - // Only dispatch if there are actual changes - let hasChanges = false; + const data = await response.json(); + const timeAfterCall = performance.now(); + logEmailOp('FETCH_UNREAD', `Received unread counts in ${(timeAfterCall - timeBeforeCall).toFixed(2)}ms`, data); - Object.entries(mergedCounts).forEach(([accountId, folderCounts]) => { - Object.entries(folderCounts).forEach(([folder, count]) => { - if (state.unreadCountMap[accountId]?.[folder] !== count) { - hasChanges = true; - } - }); - }); - - if (hasChanges) { - dispatch({ - type: 'SET_UNREAD_COUNTS', - payload: mergedCounts - }); + if (data && typeof data === 'object') { + dispatch({ type: 'SET_UNREAD_COUNTS', payload: data }); } } } catch (error) { - logEmailOp('ERROR', `Failed to fetch unread counts: ${error instanceof Error ? error.message : String(error)}`); - // Don't show toast for this error as it's not critical + console.error('Error fetching unread counts:', error); + } finally { + dispatch({ type: 'SET_LOADING_UNREAD_COUNTS', payload: false }); } - }, [session?.user?.id, state.unreadCountMap, dispatch, logEmailOp]); + }, [dispatch, session?.user, state.isLoadingUnreadCounts, logEmailOp]); - // Fetch unread counts when accounts are loaded - useEffect(() => { - let isMounted = true; + // Tracking when an email is viewed to optimize unread count refreshes + const lastViewedEmailRef = useRef(null); + const fetchFailuresRef = useRef(0); + const lastFetchFailureRef = useRef(null); + + // Modify viewEmail to track when an email is viewed + const viewEmail = useCallback((emailId: string, accountId: string, folder: string, email: Email | null) => { + dispatch({ + type: 'SELECT_EMAIL', + payload: { emailId, accountId, folder, email } + }); - if (state.accounts.length > 0) { - fetchUnreadCounts().then(() => { - if (!isMounted) return; - }).catch(error => { - if (!isMounted) return; - logEmailOp('ERROR', `Background unread count fetch failed: ${String(error)}`); - }); - } - - return () => { - isMounted = false; - }; - }, [state.accounts.length, fetchUnreadCounts, logEmailOp]); - - // Set up periodic refresh of unread counts (every 60 seconds) - useEffect(() => { - let isMounted = true; - - const intervalId = setInterval(() => { - if (!isMounted || state.accounts.length === 0) return; + // Track when an email is viewed to delay unread count refresh + if (email) { + lastViewedEmailRef.current = Date.now(); - fetchUnreadCounts().catch(error => { - if (!isMounted) return; - logEmailOp('ERROR', `Periodic unread count fetch failed: ${String(error)}`); - }); - }, 60000); - - return () => { - isMounted = false; - clearInterval(intervalId); - }; - }, [state.accounts.length, fetchUnreadCounts, logEmailOp]); + // If email is unread, mark it as read + if (email.flags && !email.flags.seen) { + dispatch({ + type: 'MARK_EMAIL_AS_READ', + payload: { emailId, isRead: true, accountId } + }); + } + } else { + // Email was deselected, schedule a refresh of unread counts after delay + setTimeout(() => { + fetchUnreadCounts(); + }, 2000); + } + }, [dispatch, fetchUnreadCounts]); // Return all state values and actions return { @@ -839,6 +879,8 @@ export const useEmailState = () => { setPage, setEmails, selectAccount, - handleLoadMore + handleLoadMore, + fetchUnreadCounts, + viewEmail }; }; \ No newline at end of file diff --git a/lib/reducers/emailReducer.ts b/lib/reducers/emailReducer.ts index f869ae11..a1133a02 100644 --- a/lib/reducers/emailReducer.ts +++ b/lib/reducers/emailReducer.ts @@ -18,6 +18,7 @@ export interface EmailState { selectedEmail: Email | null; selectedEmailIds: string[]; isLoading: boolean; + isLoadingUnreadCounts: boolean; error: string | null; page: number; perPage: number; @@ -26,6 +27,7 @@ export interface EmailState { mailboxes: string[]; unreadCountMap: Record>; showFolders: boolean; + currentAccountId?: string; } // Define all possible action types @@ -40,6 +42,7 @@ export type EmailAction = | { type: 'TOGGLE_SELECT_ALL' } | { type: 'CLEAR_SELECTED_EMAILS' } | { type: 'SET_LOADING', payload: boolean } + | { type: 'SET_LOADING_UNREAD_COUNTS', payload: boolean } | { type: 'SET_ERROR', payload: string | null } | { type: 'SET_PAGE', payload: number } | { type: 'INCREMENT_PAGE' } @@ -61,6 +64,7 @@ export const initialState: EmailState = { selectedEmail: null, selectedEmailIds: [], isLoading: false, + isLoadingUnreadCounts: false, error: null, page: 1, perPage: 20, @@ -281,6 +285,12 @@ export function emailReducer(state: EmailState, action: EmailAction): EmailState isLoading: action.payload }; + case 'SET_LOADING_UNREAD_COUNTS': + return { + ...state, + isLoadingUnreadCounts: action.payload + }; + case 'SET_ERROR': return { ...state,