1197 lines
45 KiB
TypeScript
1197 lines
45 KiB
TypeScript
import { useReducer, useCallback, useEffect, useRef } 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-utils';
|
|
|
|
// Add a global dispatcher for compatibility with older code
|
|
// This is a temporary solution until we fully migrate to the reducer pattern
|
|
declare global {
|
|
interface Window {
|
|
dispatchEmailAction?: (action: EmailAction) => void;
|
|
__emailStateDispatch?: (action: EmailAction) => void;
|
|
}
|
|
}
|
|
|
|
export const useEmailState = () => {
|
|
const [state, dispatch] = useReducer(emailReducer, initialState);
|
|
const { data: session } = useSession();
|
|
const { toast } = useToast();
|
|
|
|
// Refs to track state
|
|
const updateUnreadTimerRef = useRef<number | null>(null);
|
|
const lastEmailViewedRef = useRef<number | null>(null);
|
|
const failedFetchCountRef = useRef<number>(0);
|
|
const lastFolderRef = useRef<string | null>(null);
|
|
const lastPageLoadedRef = useRef<number>(0);
|
|
const prevFolderRef = useRef<string | null>(null);
|
|
const loadMoreTriggerTimeRef = useRef<number>(0);
|
|
|
|
// Expose dispatch function to window for external components
|
|
useEffect(() => {
|
|
// Make dispatch available globally for older code
|
|
window.dispatchEmailAction = dispatch;
|
|
window.__emailStateDispatch = dispatch;
|
|
|
|
// Clean up on unmount
|
|
return () => {
|
|
window.dispatchEmailAction = undefined;
|
|
window.__emailStateDispatch = undefined;
|
|
};
|
|
}, [dispatch]);
|
|
|
|
// 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 (page: number, perPage: number, isLoadMore: boolean = false) => {
|
|
// CRITICAL FIX: Do important validation before setting loading state
|
|
if (!session?.user?.id) return;
|
|
|
|
// CRITICAL FIX: Always log the isLoadMore parameter
|
|
console.log(`[DEBUG-LOAD_EMAILS] Called with isLoadMore=${isLoadMore}, page=${page}, currentEmails=${state.emails.length}`);
|
|
|
|
// Set the current folder and account being loaded to detect changes
|
|
const startFolder = state.currentFolder;
|
|
const startAccount = state.selectedAccount ? state.selectedAccount.id : 'default';
|
|
|
|
// CRITICAL FIX: Force loading state to true
|
|
dispatch({ type: 'SET_LOADING', payload: true });
|
|
|
|
try {
|
|
// Get normalized parameters using helper function with proper account ID handling
|
|
const accountId = state.selectedAccount ? state.selectedAccount.id : undefined;
|
|
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
|
|
normalizeFolderAndAccount(state.currentFolder, accountId);
|
|
|
|
logEmailOp('LOAD_EMAILS', `Loading emails for ${prefixedFolder} (account: ${effectiveAccountId}, isLoadMore: ${isLoadMore}, page: ${page})`);
|
|
|
|
// Construct query parameters
|
|
const queryParams = new URLSearchParams({
|
|
folder: normalizedFolder,
|
|
page: page.toString(),
|
|
perPage: perPage.toString(),
|
|
accountId: effectiveAccountId
|
|
});
|
|
|
|
// Debug log existing emails count
|
|
if (isLoadMore) {
|
|
console.log(`[DEBUG-PAGINATION] Loading more emails. Current page: ${page}, existing emails: ${state.emails.length}`);
|
|
}
|
|
|
|
// Try to get cached emails first
|
|
logEmailOp('CACHE_CHECK', `Checking cache for ${prefixedFolder}, page: ${page}`);
|
|
const cachedEmails = await getCachedEmailsWithTimeout(
|
|
session.user.id,
|
|
prefixedFolder,
|
|
page,
|
|
perPage,
|
|
100,
|
|
effectiveAccountId
|
|
);
|
|
|
|
if (cachedEmails) {
|
|
logEmailOp('CACHE_HIT', `Using cached data for ${prefixedFolder}, page: ${page}, emails: ${cachedEmails.emails?.length || 0}, isLoadMore: ${isLoadMore}`);
|
|
|
|
// Ensure cached data has emails array property
|
|
if (Array.isArray(cachedEmails.emails)) {
|
|
// CRITICAL FIX: Double check we're using the right action type based on isLoadMore param
|
|
console.log(`[DEBUG-CACHE_HIT] Dispatching ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${cachedEmails.emails.length} emails`);
|
|
|
|
// Dispatch appropriate action based on if we're loading more - DO NOT OVERRIDE isLoadMore!
|
|
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 });
|
|
}
|
|
}
|
|
|
|
// CRITICAL FIX: If this was a loadMore operation, check the result after the dispatch
|
|
if (isLoadMore) {
|
|
setTimeout(() => {
|
|
console.log(`[DEBUG-CACHE_HIT_APPEND] After ${isLoadMore ? 'APPEND' : 'SET'}, email count is now: ${state.emails.length}`);
|
|
}, 0);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Fetch emails from API if no cache hit
|
|
logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}, isLoadMore: ${isLoadMore}`);
|
|
console.log(`[DEBUG-API_FETCH] Fetching from /api/courrier/emails?${queryParams.toString()}`);
|
|
const response = await fetch(`/api/courrier/emails?${queryParams.toString()}`);
|
|
|
|
if (!response.ok) {
|
|
// CRITICAL FIX: Try to recover from fetch errors by retrying with different pagination
|
|
if (isLoadMore && page > 1) {
|
|
logEmailOp('ERROR_RECOVERY', `Failed to fetch emails for page ${page}, attempting to recover by decrementing page`);
|
|
console.log(`[DEBUG-ERROR] API returned ${response.status} for page ${page}`);
|
|
// If we're loading more and there's an error, just decrement the page to avoid getting stuck
|
|
dispatch({ type: 'SET_PAGE', payload: page - 1 });
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
// Also reset total pages to try again
|
|
dispatch({ type: 'SET_TOTAL_PAGES', payload: page });
|
|
return;
|
|
}
|
|
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to fetch emails');
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log(`[DEBUG-API_RESPONSE] Got response with ${data.emails?.length || 0} emails, totalPages: ${data.totalPages}, totalEmails: ${data.totalEmails}, isLoadMore: ${isLoadMore}`);
|
|
|
|
// CRITICAL FIX: Enhanced empty results handling
|
|
if (!data.emails || data.emails.length === 0) {
|
|
console.log(`[DEBUG-EMPTY] No emails in response for page ${page}`);
|
|
// If we're at a page > 1 and got no results, the paging is off, so try again with page 1
|
|
if (page > 1 && !isLoadMore) {
|
|
logEmailOp('EMPTY_RESULTS', `No emails returned for page ${page}, resetting to page 1`);
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
return;
|
|
}
|
|
|
|
// If we're already at page 1, just update the state with no emails
|
|
if (!isLoadMore) {
|
|
logEmailOp('EMPTY_RESULTS', `No emails found in ${state.currentFolder}`);
|
|
dispatch({ type: 'SET_EMAILS', payload: [] });
|
|
dispatch({ type: 'SET_TOTAL_EMAILS', payload: 0 });
|
|
dispatch({ type: 'SET_TOTAL_PAGES', payload: 0 });
|
|
} else {
|
|
// For load more, just set loading to false but keep existing emails
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ensure all emails have proper account ID and folder format
|
|
if (Array.isArray(data.emails)) {
|
|
// Log email dates for debugging
|
|
if (data.emails.length > 0) {
|
|
logEmailOp('EMAIL_DATES', `First few email dates before processing:`,
|
|
data.emails.slice(0, 5).map((e: any) => ({
|
|
id: e.id.substring(0, 8),
|
|
subject: e.subject?.substring(0, 20),
|
|
date: e.date,
|
|
dateObj: new Date(e.date),
|
|
timestamp: new Date(e.date).getTime()
|
|
}))
|
|
);
|
|
}
|
|
|
|
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}`;
|
|
}
|
|
|
|
// Ensure date is a valid Date object (handle strings or timestamps)
|
|
if (email.date && !(email.date instanceof Date)) {
|
|
try {
|
|
// Convert to a proper Date object if it's a string or number
|
|
const dateObj = new Date(email.date);
|
|
// Verify it's a valid date
|
|
if (!isNaN(dateObj.getTime())) {
|
|
email.date = dateObj;
|
|
}
|
|
} catch (err) {
|
|
// If conversion fails, log and use current date as fallback
|
|
console.error(`Invalid date format for email ${email.id}: ${email.date}`);
|
|
email.date = new Date();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// CRITICAL FIX: Log what we're about to do
|
|
console.log(`[DEBUG-DISPATCH] About to dispatch ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${data.emails?.length || 0} emails`);
|
|
|
|
// Update state with fetched data
|
|
dispatch({
|
|
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
|
|
payload: Array.isArray(data.emails) ? data.emails : []
|
|
});
|
|
|
|
// Double-check that we've updated the email list correctly after dispatch
|
|
setTimeout(() => {
|
|
console.log(`[DEBUG-AFTER-DISPATCH] Email count is now: ${state.emails.length}, should include the ${data.emails?.length || 0} new emails we just loaded`);
|
|
}, 0);
|
|
|
|
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'
|
|
});
|
|
} finally {
|
|
// CRITICAL FIX: Only clear loading state if the folder/account hasn't changed
|
|
if (startFolder === state.currentFolder &&
|
|
(startAccount === (state.selectedAccount?.id || 'default'))) {
|
|
// Safe to clear loading state
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
} else {
|
|
console.log(`[DEBUG-LOAD_EMAILS] Folder/account changed during load, not clearing loading state`);
|
|
}
|
|
}
|
|
}, [session?.user?.id, state.currentFolder, state.selectedAccount, state.page, state.perPage, state.emails.length, toast, logEmailOp]);
|
|
|
|
// Change folder
|
|
const changeFolder = useCallback(async (folder: string, accountId?: string) => {
|
|
logEmailOp('CHANGE_FOLDER', `Changing to folder ${folder} with account ${accountId || 'default'}`);
|
|
|
|
try {
|
|
// CRITICAL FIX: Reset pagination state immediately
|
|
lastPageLoadedRef.current = 0;
|
|
|
|
// Reset page to 1 directly to prevent any issues with page effects
|
|
// This will be atomic with the CHANGE_FOLDER action
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
|
|
// Clear existing emails - don't show old emails during load
|
|
dispatch({ type: 'SET_EMAILS', payload: [] });
|
|
|
|
// Set loading state explicitly - this is critical
|
|
dispatch({ type: 'SET_LOADING', payload: true });
|
|
|
|
// This will handle folder setting in a single atomic operation
|
|
dispatch({
|
|
type: 'CHANGE_FOLDER',
|
|
payload: { folder, accountId: accountId || 'default' }
|
|
});
|
|
|
|
// CRITICAL: The email loading will be triggered by the folder effect
|
|
// We don't need to call loadEmails directly here
|
|
} 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'
|
|
});
|
|
|
|
// Always ensure loading state is cleared on error
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
}
|
|
}, [logEmailOp, dispatch]);
|
|
|
|
// 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 }
|
|
});
|
|
|
|
// NOTE: Don't update unread counts here - that's now handled by the updateUnreadCounts function
|
|
// which is triggered by the email update above via the useEffect
|
|
|
|
// 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(state.page, state.perPage, true);
|
|
|
|
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]);
|
|
|
|
// Function to check for new emails without disrupting the user
|
|
const checkForNewEmails = useCallback(async () => {
|
|
if (!session?.user?.id) return;
|
|
|
|
// Don't check if already loading emails
|
|
if (state.isLoading) return;
|
|
|
|
try {
|
|
// Get normalized parameters using helper function
|
|
const accountId = state.selectedAccount ? state.selectedAccount.id : undefined;
|
|
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
|
|
normalizeFolderAndAccount(state.currentFolder, accountId);
|
|
|
|
logEmailOp('CHECK_NEW_EMAILS', `Checking for new emails in ${prefixedFolder}`);
|
|
|
|
// Quietly check for new emails with a special parameter
|
|
const queryParams = new URLSearchParams({
|
|
folder: normalizedFolder,
|
|
page: '1',
|
|
perPage: '1', // We only need to check the newest email
|
|
accountId: effectiveAccountId,
|
|
checkOnly: 'true' // Special parameter to indicate this is just a check
|
|
});
|
|
|
|
const response = await fetch(`/api/courrier/emails?${queryParams.toString()}`, {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
cache: 'no-cache'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to check for new emails: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Store the latest email's ID for easier reference
|
|
const lastKnownEmailId = state.emails.length > 0 ? parseInt(state.emails[0].id) : 0;
|
|
|
|
// Use newestEmailId from API response (more reliable than checking emails array)
|
|
if (data.newestEmailId && data.newestEmailId > lastKnownEmailId) {
|
|
logEmailOp('NEW_EMAILS', `Found new emails, newest ID: ${data.newestEmailId} (current: ${lastKnownEmailId})`);
|
|
|
|
// Show a toast notification with the new custom variant
|
|
toast({
|
|
variant: "new-email", // Use our new custom variant
|
|
title: "New emails",
|
|
description: "You have new emails in your inbox",
|
|
duration: 5000
|
|
});
|
|
|
|
// Full refresh just like the refresh button in sidebar
|
|
// Reset to page 1 to ensure we get the newest emails
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
loadEmails(1, state.perPage, false);
|
|
|
|
// Also update unread counts - this will be handled in the effect
|
|
// The fetchUnreadCounts function will be available when this callback is called
|
|
} else {
|
|
logEmailOp('CHECK_NEW_EMAILS', 'No new emails found');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking for new emails:', error);
|
|
}
|
|
}, [session?.user?.id, state.currentFolder, state.isLoading, state.emails, state.perPage, toast, loadEmails, logEmailOp, dispatch]);
|
|
|
|
// 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' });
|
|
|
|
// Show toast notification
|
|
toast({
|
|
title: "Emails Deleted",
|
|
description: `${emailIds.length} email(s) moved to trash`
|
|
});
|
|
|
|
// Full refresh just like the refresh button in sidebar
|
|
// Reset to page 1 to ensure we get the updated email list
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
loadEmails(1, state.perPage, false);
|
|
|
|
// Also update unread counts - this will be handled in the effect
|
|
// The fetchUnreadCounts function will be available when this callback is called
|
|
|
|
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, state.perPage, toast, loadEmails, logEmailOp, dispatch]);
|
|
|
|
// 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"
|
|
});
|
|
|
|
// Wait a moment for the email to be available in the sent folder
|
|
// (emails may need time to be stored on IMAP server)
|
|
setTimeout(() => {
|
|
// Check for new emails and refresh mailbox
|
|
checkForNewEmails();
|
|
|
|
// Refresh emails to show the sent email in current view
|
|
loadEmails(state.page, state.perPage, false);
|
|
}, 1500);
|
|
|
|
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, checkForNewEmails]);
|
|
|
|
// 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(() => {
|
|
// Don't load more if already loading or if there are no more pages
|
|
if (state.isLoading || state.page >= state.totalPages) {
|
|
console.log(`[LOAD_MORE] Skipping load more - already loading: ${state.isLoading}, page: ${state.page}, totalPages: ${state.totalPages}`);
|
|
return;
|
|
}
|
|
|
|
// Log the current state
|
|
console.log(`[LOAD_MORE] Loading more emails for ${state.currentFolder}, currentPage: ${state.page}, totalPages: ${state.totalPages}, current email count: ${state.emails.length}`);
|
|
|
|
// Set loading state immediately to prevent double-loading
|
|
dispatch({
|
|
type: 'SET_LOADING',
|
|
payload: true
|
|
});
|
|
|
|
// Calculate next page
|
|
const nextPage = state.page + 1;
|
|
|
|
// Update the page state - fix type issue
|
|
dispatch({
|
|
type: 'SET_PAGE',
|
|
payload: nextPage
|
|
});
|
|
|
|
// CRITICAL FIX: Update the lastLoadedPage ref to track pagination state
|
|
lastPageLoadedRef.current = nextPage;
|
|
|
|
// Load the next page
|
|
loadEmails(nextPage, state.perPage, true).then(() => {
|
|
console.log(`[LOAD_MORE] Completed loading more emails for page ${nextPage}`);
|
|
});
|
|
}, [state.isLoading, state.page, state.totalPages, state.currentFolder, state.emails.length, state.perPage, dispatch, loadEmails]);
|
|
|
|
// Effect to load emails when folder changes
|
|
useEffect(() => {
|
|
if (session?.user?.id && state.currentFolder) {
|
|
// CRITICAL FIX: REMOVE this check that's causing the problem
|
|
// Instead, detect a real folder change and always load when that happens
|
|
|
|
// Extract account ID for consistent loading
|
|
const { effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder);
|
|
|
|
// Track if the folder actually changed
|
|
const folderChanged = prevFolderRef.current !== state.currentFolder;
|
|
|
|
if (folderChanged) {
|
|
console.log(`[DEBUG-FOLDER_EFFECT] Folder changed from ${prevFolderRef.current} to ${state.currentFolder}`);
|
|
prevFolderRef.current = state.currentFolder;
|
|
|
|
// CRITICAL FIX: Always reset pagination state when folder actually changes
|
|
console.log(`[DEBUG-FOLDER_EFFECT] Folder changed - resetting pagination state`);
|
|
|
|
// Reset page to 1 AND reset lastPageLoadedRef to ensure we load
|
|
lastPageLoadedRef.current = 0;
|
|
|
|
if (state.page !== 1) {
|
|
console.log(`[DEBUG-FOLDER_EFFECT] Resetting page to 1 because folder changed`);
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
}
|
|
|
|
// CRITICAL FIX: Clear emails and set loading when folder changes
|
|
dispatch({ type: 'SET_EMAILS', payload: [] });
|
|
dispatch({ type: 'SET_LOADING', payload: true });
|
|
|
|
// CRITICAL FIX: Always load emails when folder changes, no matter what
|
|
console.log(`[DEBUG-FOLDER_EFFECT] Loading emails for new folder: ${state.currentFolder}`);
|
|
loadEmails(1, state.perPage, false);
|
|
return; // Exit early after handling folder change
|
|
}
|
|
|
|
// If no folder change detected, only load if on page 1 and not already loaded
|
|
if (state.page === 1 && lastPageLoadedRef.current === 0) {
|
|
logEmailOp('FOLDER_LOAD', `Loading initial emails for folder ${state.currentFolder}`);
|
|
loadEmails(state.page, state.perPage, false);
|
|
}
|
|
}
|
|
}, [session?.user?.id, state.currentFolder, state.page, state.perPage, loadEmails, logEmailOp, dispatch]);
|
|
|
|
// Effect to load more emails when page changes
|
|
useEffect(() => {
|
|
if (!session?.user?.id || !state.currentFolder) return;
|
|
|
|
// Make sure we're on at least page 1
|
|
if (state.page < 1) {
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
return;
|
|
}
|
|
|
|
console.log(`[DEBUG-PAGE_EFFECT] Page changed to ${state.page}`);
|
|
|
|
// CRITICAL FIX: Add a special case for page 1 loads - we should never skip loading the first page
|
|
// This ensures that after a folder change, page 1 always loads even if loading state is true
|
|
if (state.page === 1) {
|
|
const currentFolder = state.currentFolder;
|
|
const lastLoadedFolder = prevFolderRef.current;
|
|
|
|
// Check if this is a fresh folder load (folder changed or first time loading)
|
|
if (currentFolder !== lastLoadedFolder || lastPageLoadedRef.current === 0) {
|
|
// Force loading page 1 for new folders, regardless of loading state
|
|
console.log(`[DEBUG-PAGE_EFFECT] Force loading page 1 for folder: ${currentFolder}`);
|
|
|
|
// Set the loading state explicitly (might already be true)
|
|
dispatch({ type: 'SET_LOADING', payload: true });
|
|
|
|
// Update refs to track the current state
|
|
prevFolderRef.current = currentFolder;
|
|
lastPageLoadedRef.current = 1;
|
|
|
|
// Call loadEmails to load the first page - never skip this!
|
|
loadEmails(1, state.perPage, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// For pages > 1 or already loaded folders, follow normal rules
|
|
|
|
// Skip if already loading
|
|
if (state.isLoading) {
|
|
console.log(`[DEBUG-PAGE_EFFECT] Skipping effect execution entirely - already loading`);
|
|
return;
|
|
}
|
|
|
|
// Normalize folder and get account ID
|
|
const { effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder);
|
|
|
|
// Check if this is a duplicate page load
|
|
if (state.page === lastPageLoadedRef.current) {
|
|
console.log(`[DEBUG-PAGE_EFFECT] Skipping - already loaded page ${state.page}`);
|
|
return;
|
|
}
|
|
|
|
// Skip loads for zero-based pages
|
|
if (state.page === 0) {
|
|
console.log(`[DEBUG-PAGE_EFFECT] Skipping load for invalid page ${state.page}`);
|
|
return;
|
|
}
|
|
|
|
// Update our reference to prevent duplicate loads
|
|
lastPageLoadedRef.current = state.page;
|
|
|
|
// Always use isLoadMore=true when page > 1
|
|
console.log(`[DEBUG-PAGE_EFFECT] Calling loadEmails with isLoadMore=true for page ${state.page}`);
|
|
loadEmails(state.page, state.perPage, true);
|
|
|
|
// Do NOT include state.emails.length here to prevent infinite loops
|
|
}, [session?.user?.id, state.page, state.currentFolder, state.isLoading, state.perPage, loadEmails, logEmailOp, dispatch]);
|
|
|
|
// Fetch unread counts from API
|
|
const fetchUnreadCounts = useCallback(async () => {
|
|
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 5 seconds)
|
|
const now = Date.now();
|
|
// Initialize the ref to the current time if it's null
|
|
if (lastEmailViewedRef.current === null) {
|
|
lastEmailViewedRef.current = now;
|
|
}
|
|
|
|
// Now we can safely use it since we've initialized it
|
|
if (now - lastEmailViewedRef.current < 5000) {
|
|
console.log('Skipping unread count update - email viewed recently');
|
|
return;
|
|
}
|
|
|
|
// Try to get from sessionStorage first for faster response
|
|
try {
|
|
const storageKey = `unread_counts_${session.user.id}`;
|
|
const storedData = sessionStorage.getItem(storageKey);
|
|
|
|
if (storedData) {
|
|
const { data, timestamp } = JSON.parse(storedData);
|
|
// Use stored data if it's less than 30 seconds old
|
|
if (now - timestamp < 30000) {
|
|
logEmailOp('FETCH_UNREAD', 'Using sessionStorage data', { age: Math.round((now - timestamp)/1000) + 's' });
|
|
dispatch({ type: 'SET_UNREAD_COUNTS', payload: data });
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Ignore storage errors
|
|
}
|
|
|
|
// Reset failure tracking if it's been more than 1 minute since last failure
|
|
if ((window as any).__unreadCountFailures?.lastFailureTime &&
|
|
now - (window as any).__unreadCountFailures.lastFailureTime > 60000) {
|
|
(window as any).__unreadCountFailures = { count: 0, lastFailureTime: 0 };
|
|
}
|
|
|
|
// Exponential backoff for failures with proper tracking object
|
|
if (!(window as any).__unreadCountFailures) {
|
|
(window as any).__unreadCountFailures = { count: 0, lastFailureTime: 0 };
|
|
}
|
|
|
|
if ((window as any).__unreadCountFailures.count > 0) {
|
|
const failures = (window as any).__unreadCountFailures.count;
|
|
const backoffMs = Math.min(30000, 1000 * Math.pow(2, failures - 1));
|
|
if (now - (window as any).__unreadCountFailures.lastFailureTime < backoffMs) {
|
|
logEmailOp('BACKOFF', `Skipping unread fetch, in backoff period (${backoffMs}ms)`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
dispatch({ type: 'SET_LOADING_UNREAD_COUNTS', payload: true });
|
|
|
|
const timeBeforeCall = performance.now();
|
|
logEmailOp('FETCH_UNREAD', 'Fetching unread counts from API');
|
|
|
|
const response = await fetch('/api/courrier/unread-counts', {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
// Add cache control headers
|
|
cache: 'no-cache',
|
|
next: { revalidate: 0 }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// If request failed, track failures properly
|
|
(window as any).__unreadCountFailures.count =
|
|
Math.min((window as any).__unreadCountFailures.count + 1, 10);
|
|
(window as any).__unreadCountFailures.lastFailureTime = now;
|
|
|
|
const failures = (window as any).__unreadCountFailures.count;
|
|
|
|
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);
|
|
}
|
|
|
|
(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 = { count: 0, lastFailureTime: 0 };
|
|
|
|
const data = await response.json();
|
|
const timeAfterCall = performance.now();
|
|
|
|
// Skip if we got the "pending_refresh" status
|
|
if (data._status === 'pending_refresh') {
|
|
logEmailOp('FETCH_UNREAD', 'Server is refreshing counts, will try again soon');
|
|
|
|
// Retry after a short delay
|
|
setTimeout(() => {
|
|
fetchUnreadCounts();
|
|
}, 2000);
|
|
|
|
return;
|
|
}
|
|
|
|
logEmailOp('FETCH_UNREAD', `Received unread counts in ${(timeAfterCall - timeBeforeCall).toFixed(2)}ms`);
|
|
|
|
if (data && typeof data === 'object') {
|
|
dispatch({ type: 'SET_UNREAD_COUNTS', payload: data });
|
|
|
|
// Store in sessionStorage for faster future access
|
|
try {
|
|
sessionStorage.setItem(
|
|
`unread_counts_${session.user.id}`,
|
|
JSON.stringify({
|
|
data,
|
|
timestamp: now
|
|
})
|
|
);
|
|
} catch (err) {
|
|
// Ignore storage errors
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching unread counts:', error);
|
|
} finally {
|
|
dispatch({ type: 'SET_LOADING_UNREAD_COUNTS', payload: false });
|
|
}
|
|
}, [dispatch, session?.user, state.isLoadingUnreadCounts, logEmailOp]);
|
|
|
|
// Calculate and update unread counts
|
|
const updateUnreadCounts = useCallback(() => {
|
|
// Skip if no emails or accounts
|
|
if (state.emails.length === 0 || state.accounts.length === 0) return;
|
|
|
|
// To avoid running this too frequently, check the timestamp of last update
|
|
if (!(window as any).__lastUnreadUpdate) {
|
|
(window as any).__lastUnreadUpdate = { timestamp: 0 };
|
|
}
|
|
|
|
const now = Date.now();
|
|
const lastUpdate = (window as any).__lastUnreadUpdate;
|
|
const MIN_UPDATE_INTERVAL = 10000; // 10 seconds minimum between updates (increased from 2s)
|
|
|
|
if (now - lastUpdate.timestamp < MIN_UPDATE_INTERVAL) {
|
|
return; // Skip if updated too recently
|
|
}
|
|
|
|
// Rather than calculating locally, fetch from the API
|
|
fetchUnreadCounts();
|
|
|
|
// Update timestamp of last update
|
|
lastUpdate.timestamp = now;
|
|
}, [state.emails.length, state.accounts.length, fetchUnreadCounts]);
|
|
|
|
// Call updateUnreadCounts when relevant state changes
|
|
useEffect(() => {
|
|
if (!state.emails || state.emails.length === 0) return;
|
|
|
|
// Debounce unread count updates to prevent rapid multiple updates
|
|
let updateTimeoutId: ReturnType<typeof setTimeout>;
|
|
|
|
const debounceMs = 5000; // Increase debounce to 5 seconds (from 2s)
|
|
|
|
// Function to call after debounce period
|
|
const debouncedUpdate = () => {
|
|
updateTimeoutId = setTimeout(() => {
|
|
updateUnreadCounts();
|
|
}, debounceMs);
|
|
};
|
|
|
|
// Clear any existing timeout and start a new one
|
|
debouncedUpdate();
|
|
|
|
// Also set up a periodic refresh every minute if the tab is active
|
|
const periodicRefreshId = setInterval(() => {
|
|
if (document.visibilityState === 'visible') {
|
|
updateUnreadCounts();
|
|
}
|
|
}, 60000); // 1 minute
|
|
|
|
// Cleanup timeout on unmount or state change
|
|
return () => {
|
|
clearTimeout(updateTimeoutId);
|
|
clearInterval(periodicRefreshId);
|
|
};
|
|
// Deliberately exclude unreadCountMap to prevent infinite loops
|
|
}, [state.emails, updateUnreadCounts]);
|
|
|
|
// Set up periodic check for new emails
|
|
useEffect(() => {
|
|
if (!state.emails || state.emails.length === 0) return;
|
|
|
|
// Set up a periodic check for new emails at the same interval as unread counts
|
|
const checkNewEmailsId = setInterval(() => {
|
|
if (document.visibilityState === 'visible') {
|
|
checkForNewEmails();
|
|
}
|
|
}, 60000); // 1 minute - same as unread count refresh
|
|
|
|
// Cleanup interval on unmount or state change
|
|
return () => {
|
|
clearInterval(checkNewEmailsId);
|
|
};
|
|
}, [state.emails, checkForNewEmails]);
|
|
|
|
// Tracking when an email is viewed to optimize unread count refreshes
|
|
const lastViewedEmailRef = useRef<number | null>(null);
|
|
const fetchFailuresRef = useRef<number>(0);
|
|
const lastFetchFailureRef = useRef<number | null>(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 }
|
|
});
|
|
|
|
// Track when an email is viewed to delay unread count refresh
|
|
if (email) {
|
|
lastViewedEmailRef.current = Date.now();
|
|
|
|
// 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]);
|
|
|
|
// Set up a function to manually trigger checking for new emails
|
|
const forceCheckForNewEmails = useCallback(() => {
|
|
// Don't check if we're already loading
|
|
if (state.isLoading) return;
|
|
|
|
// Log that we're manually checking
|
|
logEmailOp('MANUAL_CHECK', 'Manually checking for new emails');
|
|
|
|
// Reset to page 1 to ensure we get the newest emails
|
|
dispatch({ type: 'SET_PAGE', payload: 1 });
|
|
|
|
// Perform a complete refresh of emails
|
|
loadEmails(1, state.perPage, false);
|
|
|
|
// Also update unread counts
|
|
fetchUnreadCounts();
|
|
}, [state.isLoading, state.perPage, loadEmails, logEmailOp, dispatch, fetchUnreadCounts]);
|
|
|
|
// 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,
|
|
fetchUnreadCounts,
|
|
viewEmail,
|
|
checkForNewEmails,
|
|
forceCheckForNewEmails
|
|
};
|
|
};
|