courrier multi account restore compose
This commit is contained in:
parent
9b41fcbc01
commit
6939ad5e35
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user