courrier multi account restore compose

This commit is contained in:
alma 2025-04-30 14:37:01 +02:00
parent 9b41fcbc01
commit 6939ad5e35
2 changed files with 146 additions and 94 deletions

View File

@ -1,4 +1,4 @@
import { useReducer, useCallback, useEffect } from 'react'; import { useReducer, useCallback, useEffect, useRef } from 'react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useToast } from './use-toast'; import { useToast } from './use-toast';
import { import {
@ -681,8 +681,13 @@ export const useEmailState = () => {
} }
}); });
// Log the unread counts for debugging // Store the current update for comparison
logEmailOp('UNREAD_COUNTS', 'Updated unread counts:', tempUnreadMap); if (!(window as any).__lastUnreadUpdate) {
(window as any).__lastUnreadUpdate = {
timestamp: 0,
map: {}
};
}
// Check if the unread counts have actually changed before updating state // Check if the unread counts have actually changed before updating state
let hasChanged = false; let hasChanged = false;
@ -696,128 +701,163 @@ export const useEmailState = () => {
}); });
}); });
// Only update if there are actual changes to avoid infinite updates // Check if we've updated recently
if (hasChanged) { 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 // 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({ dispatch({
type: 'SET_UNREAD_COUNTS', 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]); }, [state.emails, state.accounts, state.unreadCountMap, dispatch, logEmailOp]);
// Call updateUnreadCounts when relevant state changes // Call updateUnreadCounts when relevant state changes
useEffect(() => { useEffect(() => {
// Only update unread counts when emails or flag status changes if (!state.emails || state.emails.length === 0) return;
// NOT when the unreadCountMap itself changes (that would cause infinite loop)
const updateCountsWithDebounce = setTimeout(() => {
updateUnreadCounts();
}, 300); // Debounce to handle multiple email updates
return () => clearTimeout(updateCountsWithDebounce); // Debounce unread count updates to prevent rapid multiple updates
let updateTimeoutId: ReturnType<typeof setTimeout>;
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 // Deliberately exclude unreadCountMap to prevent infinite loops
}, [updateUnreadCounts, state.emails]); }, [state.emails, updateUnreadCounts]);
// Fetch unread counts from API // Fetch unread counts from API
const fetchUnreadCounts = useCallback(async () => { 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 { try {
dispatch({ type: 'SET_LOADING_UNREAD_COUNTS', payload: true });
const timeBeforeCall = performance.now();
logEmailOp('FETCH_UNREAD', 'Fetching unread counts from API'); 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) { if (!response.ok) {
throw new Error('Failed to fetch unread 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;
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 = 0;
const data = await response.json(); const data = await response.json();
const timeAfterCall = performance.now();
logEmailOp('FETCH_UNREAD', `Received unread counts in ${(timeAfterCall - timeBeforeCall).toFixed(2)}ms`, data);
if (data.counts && typeof data.counts === 'object') { if (data && typeof data === 'object') {
logEmailOp('UNREAD_API', 'Received unread counts from API', data.counts); dispatch({ type: 'SET_UNREAD_COUNTS', payload: data });
// 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] = {};
}
Object.entries(folderCounts).forEach(([folder, count]: [string, any]) => {
mergedCounts[accountId][folder] = typeof count === 'number' ? count : 0;
});
});
// Only dispatch if there are actual changes
let hasChanges = false;
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
});
} }
} }
} catch (error) { } catch (error) {
logEmailOp('ERROR', `Failed to fetch unread counts: ${error instanceof Error ? error.message : String(error)}`); console.error('Error fetching unread counts:', error);
// Don't show toast for this error as it's not critical } 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 // Tracking when an email is viewed to optimize unread count refreshes
useEffect(() => { const lastViewedEmailRef = useRef<number | null>(null);
let isMounted = true; const fetchFailuresRef = useRef<number>(0);
const lastFetchFailureRef = useRef<number | null>(null);
if (state.accounts.length > 0) { // Modify viewEmail to track when an email is viewed
fetchUnreadCounts().then(() => { const viewEmail = useCallback((emailId: string, accountId: string, folder: string, email: Email | null) => {
if (!isMounted) return; dispatch({
}).catch(error => { type: 'SELECT_EMAIL',
if (!isMounted) return; payload: { emailId, accountId, folder, email }
logEmailOp('ERROR', `Background unread count fetch failed: ${String(error)}`); });
// 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 {
return () => { // Email was deselected, schedule a refresh of unread counts after delay
isMounted = false; setTimeout(() => {
}; fetchUnreadCounts();
}, [state.accounts.length, fetchUnreadCounts, logEmailOp]); }, 2000);
}
// Set up periodic refresh of unread counts (every 60 seconds) }, [dispatch, fetchUnreadCounts]);
useEffect(() => {
let isMounted = true;
const intervalId = setInterval(() => {
if (!isMounted || state.accounts.length === 0) return;
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]);
// Return all state values and actions // Return all state values and actions
return { return {
@ -839,6 +879,8 @@ export const useEmailState = () => {
setPage, setPage,
setEmails, setEmails,
selectAccount, selectAccount,
handleLoadMore handleLoadMore,
fetchUnreadCounts,
viewEmail
}; };
}; };

View File

@ -18,6 +18,7 @@ export interface EmailState {
selectedEmail: Email | null; selectedEmail: Email | null;
selectedEmailIds: string[]; selectedEmailIds: string[];
isLoading: boolean; isLoading: boolean;
isLoadingUnreadCounts: boolean;
error: string | null; error: string | null;
page: number; page: number;
perPage: number; perPage: number;
@ -26,6 +27,7 @@ export interface EmailState {
mailboxes: string[]; mailboxes: string[];
unreadCountMap: Record<string, Record<string, number>>; unreadCountMap: Record<string, Record<string, number>>;
showFolders: boolean; showFolders: boolean;
currentAccountId?: string;
} }
// Define all possible action types // Define all possible action types
@ -40,6 +42,7 @@ export type EmailAction =
| { type: 'TOGGLE_SELECT_ALL' } | { type: 'TOGGLE_SELECT_ALL' }
| { type: 'CLEAR_SELECTED_EMAILS' } | { type: 'CLEAR_SELECTED_EMAILS' }
| { type: 'SET_LOADING', payload: boolean } | { type: 'SET_LOADING', payload: boolean }
| { type: 'SET_LOADING_UNREAD_COUNTS', payload: boolean }
| { type: 'SET_ERROR', payload: string | null } | { type: 'SET_ERROR', payload: string | null }
| { type: 'SET_PAGE', payload: number } | { type: 'SET_PAGE', payload: number }
| { type: 'INCREMENT_PAGE' } | { type: 'INCREMENT_PAGE' }
@ -61,6 +64,7 @@ export const initialState: EmailState = {
selectedEmail: null, selectedEmail: null,
selectedEmailIds: [], selectedEmailIds: [],
isLoading: false, isLoading: false,
isLoadingUnreadCounts: false,
error: null, error: null,
page: 1, page: 1,
perPage: 20, perPage: 20,
@ -281,6 +285,12 @@ export function emailReducer(state: EmailState, action: EmailAction): EmailState
isLoading: action.payload isLoading: action.payload
}; };
case 'SET_LOADING_UNREAD_COUNTS':
return {
...state,
isLoadingUnreadCounts: action.payload
};
case 'SET_ERROR': case 'SET_ERROR':
return { return {
...state, ...state,