NeahNew/lib/reducers/emailReducer.ts
2025-05-03 14:17:46 +02:00

450 lines
13 KiB
TypeScript

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<string, string>;
currentFolder: string;
emails: Email[];
selectedEmail: Email | null;
selectedEmailIds: string[];
isLoading: boolean;
isLoadingUnreadCounts: boolean;
error: string | null;
page: number;
perPage: number;
totalPages: number;
totalEmails: number;
mailboxes: string[];
unreadCountMap: Record<string, Record<string, number>>;
showFolders: boolean;
currentAccountId?: string;
}
// 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_LOADING_UNREAD_COUNTS', 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: 'SET_UNREAD_COUNTS', payload: Record<string, Record<string, 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,
isLoadingUnreadCounts: 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':
// Sort emails by date (newest first) to ensure consistent sorting
// First make a copy to avoid mutating the input
const unsortedEmails = [...action.payload];
// For debugging - log a few emails before sorting
if (unsortedEmails.length > 0) {
console.log(`[EMAIL_REDUCER] Sorting ${unsortedEmails.length} emails`);
// Log a sample of emails before sorting
console.log('[EMAIL_REDUCER] Sample emails before sorting:',
unsortedEmails.slice(0, 3).map(e => ({
id: e.id.substring(0, 8),
subject: e.subject?.substring(0, 20),
date: e.date,
timestamp: new Date(e.date).getTime()
}))
);
}
// CRITICAL FIX: Enhanced sorting function that ensures proper date handling
const sortedEmails = unsortedEmails.sort((a, b) => {
// Convert all dates to timestamps for comparison
let dateA: number, dateB: number;
try {
dateA = a.date instanceof Date ? a.date.getTime() : new Date(a.date).getTime();
} catch (e) {
dateA = 0; // Default to oldest if invalid
}
try {
dateB = b.date instanceof Date ? b.date.getTime() : new Date(b.date).getTime();
} catch (e) {
dateB = 0; // Default to oldest if invalid
}
// Handle invalid dates
if (isNaN(dateA) && isNaN(dateB)) return 0;
if (isNaN(dateA)) return 1; // Put invalid dates at the end
if (isNaN(dateB)) return -1;
// Sort newest first
return dateB - dateA;
});
// For debugging - log a few emails after sorting
if (sortedEmails.length > 0) {
console.log('[EMAIL_REDUCER] Sample emails after sorting:',
sortedEmails.slice(0, 3).map(e => ({
id: e.id.substring(0, 8),
subject: e.subject?.substring(0, 20),
date: e.date,
timestamp: new Date(e.date).getTime()
}))
);
}
return {
...state,
emails: sortedEmails,
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));
console.log(`[DEBUG-REDUCER] APPEND_EMAILS - Got ${action.payload.length} emails to append, current list has ${state.emails.length}`);
// Filter out any duplicates before appending
const newEmails = action.payload.filter(email => !existingIds.has(email.id));
// Log appending for debugging
console.log(`[DEBUG-REDUCER] Filtered to ${newEmails.length} new non-duplicate emails`);
// CRITICAL FIX: If no new emails were found, set isLoading to false but don't change the email list
if (newEmails.length === 0) {
console.log('[DEBUG-REDUCER] No new emails to append, returning current state with isLoading=false');
return {
...state,
isLoading: false
};
}
// Debug the dates to check sorting
if (newEmails.length > 0) {
console.log('[DEBUG-REDUCER] Sample new emails before combining:',
newEmails.slice(0, 3).map(e => ({
id: e.id.substring(0, 8),
subject: e.subject?.substring(0, 20),
date: e.date,
timestamp: new Date(e.date).getTime()
}))
);
}
// FIXED: Properly combine existing and new emails
// We need to ensure we keep ALL emails when appending
const combinedEmails = [...state.emails, ...newEmails];
// Sort combined emails by date (newest first)
const sortedEmails = combinedEmails.sort(
(a, b) => {
// Convert all dates to timestamps for comparison
let dateA: number, dateB: number;
try {
dateA = a.date instanceof Date ? a.date.getTime() : new Date(a.date).getTime();
} catch (e) {
dateA = 0; // Default to oldest if invalid
}
try {
dateB = b.date instanceof Date ? b.date.getTime() : new Date(b.date).getTime();
} catch (e) {
dateB = 0; // Default to oldest if invalid
}
// Handle invalid dates
if (isNaN(dateA) && isNaN(dateB)) return 0;
if (isNaN(dateA)) return 1; // Put invalid dates at the end
if (isNaN(dateB)) return -1;
// Sort newest first
return dateB - dateA;
}
);
console.log(`[DEBUG-REDUCER] Final combined list has ${sortedEmails.length} emails (${state.emails.length} old + ${newEmails.length} new)`);
return {
...state,
emails: sortedEmails,
isLoading: false
};
}
case 'SELECT_EMAIL':
return {
...state,
selectedEmail: action.payload.email,
// Don't modify selectedEmailIds when just selecting an email for preview
};
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_LOADING_UNREAD_COUNTS':
return {
...state,
isLoadingUnreadCounts: 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 'SET_UNREAD_COUNTS':
return {
...state,
unreadCountMap: action.payload
};
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;
}
}