courrier multi account restore compose

This commit is contained in:
alma 2025-04-29 10:35:20 +02:00
parent 7a33f610c6
commit d34bf5202c
3 changed files with 250 additions and 69 deletions

View File

@ -128,6 +128,15 @@ const colorPalette = [
'bg-cyan-500', 'bg-cyan-500',
]; ];
// Helper function for consistent logging
const logEmailOp = (operation: string, details: string, data?: any) => {
const timestamp = new Date().toISOString().split('T')[1].substring(0, 12);
console.log(`[${timestamp}][EMAIL-APP][${operation}] ${details}`);
if (data) {
console.log(`[${timestamp}][EMAIL-APP][DATA]`, data);
}
};
export default function CourrierPage() { export default function CourrierPage() {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession(); const { data: session } = useSession();
@ -267,6 +276,31 @@ export default function CourrierPage() {
} }
}, [accounts, mailboxes]); }, [accounts, mailboxes]);
// Handle initial loading and folder prefetching
useEffect(() => {
// Only run if we have a selected account and current folder
if (selectedAccount && currentFolder && !isLoading) {
logEmailOp('PREFETCH', `Prefetching emails for folder: ${currentFolder}`);
// Ensure the UI is updated to reflect current selections
if (selectedAccount.id !== 'loading-account') {
// Show the current folder as selected
setSelectedFolders(prev => {
if (prev[selectedAccount.id] === currentFolder) {
// Already selected, no need to update
return prev;
}
logEmailOp('FOLDER-SELECT', `Updating selected folder for ${selectedAccount.id} to ${currentFolder}`);
return {
...prev,
[selectedAccount.id]: currentFolder
};
});
}
}
}, [selectedAccount, currentFolder, isLoading]);
// Initialize session and start prefetching // Initialize session and start prefetching
useEffect(() => { useEffect(() => {
// Flag to prevent multiple initialization attempts // Flag to prevent multiple initialization attempts
@ -278,6 +312,7 @@ export default function CourrierPage() {
const initSession = async () => { const initSession = async () => {
try { try {
if (!isMounted) return; if (!isMounted) return;
logEmailOp('SESSION', 'Initializing email session');
setLoading(true); setLoading(true);
// First check if Redis is ready before making API calls // First check if Redis is ready before making API calls
@ -288,6 +323,7 @@ export default function CourrierPage() {
if (!isMounted) return; if (!isMounted) return;
// Call the session API to check email credentials and start prefetching // Call the session API to check email credentials and start prefetching
logEmailOp('SESSION', 'Fetching session data from API');
const response = await fetch('/api/courrier/session', { const response = await fetch('/api/courrier/session', {
credentials: 'include', // Ensure cookies are sent credentials: 'include', // Ensure cookies are sent
headers: { headers: {
@ -572,8 +608,11 @@ export default function CourrierPage() {
// Update handleMailboxChange to ensure consistent folder naming and prevent race conditions // Update handleMailboxChange to ensure consistent folder naming and prevent race conditions
const handleMailboxChange = (folder: string, accountId?: string) => { const handleMailboxChange = (folder: string, accountId?: string) => {
// Enhanced logging to trace the flow
logEmailOp('MAILBOX-CHANGE', `Starting mailbox change to folder=${folder}, accountId=${accountId || 'undefined'}`);
if (!accountId || accountId === 'loading-account') { if (!accountId || accountId === 'loading-account') {
console.warn('No valid accountId provided for folder change'); logEmailOp('MAILBOX-CHANGE', 'No valid accountId provided, using default flow', { folder });
changeFolder(folder); changeFolder(folder);
return; return;
} }
@ -586,6 +625,7 @@ export default function CourrierPage() {
const account = accounts.find(a => a.id.toString() === accountId.toString()); const account = accounts.find(a => a.id.toString() === accountId.toString());
if (!account) { if (!account) {
logEmailOp('MAILBOX-CHANGE', `ERROR: Account not found: ${accountId}`);
toast({ toast({
title: "Account not found", title: "Account not found",
description: `The account ${accountId} could not be found.`, description: `The account ${accountId} could not be found.`,
@ -594,39 +634,88 @@ export default function CourrierPage() {
setLoading(false); setLoading(false);
return; return;
} }
// Normalize folder name with account prefix
const prefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`;
// Verify folder exists in account logEmailOp('MAILBOX-CHANGE', `Found account: ${account.email}`, {
const folderExists = account.folders?.some(f => folderCount: account.folders?.length || 0,
f === prefixedFolder || f === folder || folder: folder
(folder.includes(':') ? false : `${accountId}:${folder}` === f) });
// Extract the base folder name if it already has an account prefix
let baseFolder = folder;
let folderAccountId = accountId;
if (folder.includes(':')) {
const parts = folder.split(':');
folderAccountId = parts[0];
baseFolder = parts[1];
logEmailOp('MAILBOX-CHANGE', `Parsed folder: accountId=${folderAccountId}, baseFolder=${baseFolder}`);
// If the prefixed account doesn't match our account, log a warning
if (folderAccountId !== accountId.toString()) {
logEmailOp('MAILBOX-CHANGE', `WARNING: Folder prefix mismatch`, {
folderAccount: folderAccountId,
targetAccount: accountId,
originalFolder: folder
});
}
}
// Always create a consistent folder name with current account prefix
const prefixedFolder = `${accountId}:${baseFolder}`;
logEmailOp('MAILBOX-CHANGE', `Normalized folder name: ${prefixedFolder}`);
// Find exact folder match in account's folders
const exactMatch = account.folders?.find(f =>
f === prefixedFolder ||
f === baseFolder ||
(f.includes(':') && f.split(':')[1] === baseFolder)
); );
if (!folderExists && !folder.includes('INBOX') && folder !== 'INBOX') { if (exactMatch) {
logEmailOp('MAILBOX-CHANGE', `Found exact folder match: ${exactMatch}`);
} else {
logEmailOp('MAILBOX-CHANGE', `No exact folder match found for ${baseFolder}`);
}
// If no match and not INBOX, show error
if (!exactMatch && !baseFolder.includes('INBOX') && baseFolder !== 'INBOX') {
logEmailOp('MAILBOX-CHANGE', `ERROR: Folder not found: ${baseFolder}`);
toast({ toast({
title: "Folder not found", title: "Folder not found",
description: `The folder ${folder} does not exist for this account.`, description: `The folder "${baseFolder}" does not exist for this account.`,
variant: "destructive", variant: "destructive",
}); });
setLoading(false); setLoading(false);
return; return;
} }
// Use the exact match if found, otherwise use our constructed prefixedFolder
const folderToUse = exactMatch || prefixedFolder;
logEmailOp('MAILBOX-CHANGE', `Proceeding with folder: ${folderToUse}`);
// Update UI state // Update UI state
setSelectedAccount(account); setSelectedAccount(account);
setSelectedFolders(prev => ({
...prev, // Update the selected folders map with the normalized folder name
[accountId]: prefixedFolder setSelectedFolders(prev => {
})); const updated = {
...prev,
[accountId]: folderToUse
};
logEmailOp('MAILBOX-CHANGE', `Updated selected folders map`, updated);
return updated;
});
// Use a deliberate delay before changing folder to ensure state is updated // Use a deliberate delay before changing folder to ensure state is updated
logEmailOp('MAILBOX-CHANGE', `Starting folder change after delay...`);
// Reduce timeout to improve responsiveness
setTimeout(() => { setTimeout(() => {
console.log(`Loading emails for folder: ${prefixedFolder}, account: ${accountId}`); logEmailOp('MAILBOX-CHANGE', `Loading emails for folder: ${folderToUse}, account: ${accountId}`);
changeFolder(prefixedFolder, accountId); changeFolder(folderToUse, accountId);
setLoading(false); setLoading(false);
}, 300); }, 150); // Reduced from 300ms to 150ms for better responsiveness
}; };
// Update the folder button rendering to show selected state based on account // Update the folder button rendering to show selected state based on account
@ -712,45 +801,74 @@ export default function CourrierPage() {
}, [selectedAccount, showFolders]); }, [selectedAccount, showFolders]);
const handleAccountSelect = (account: Account) => { const handleAccountSelect = (account: Account) => {
// Enhanced logging to trace the flow
logEmailOp('ACCOUNT-SELECT', `Selecting account: ${account.email} (id: ${account.id})`, {
folderCount: account.folders?.length || 0
});
// First set loading state to provide visual feedback // First set loading state to provide visual feedback
setLoading(true); setLoading(true);
// Clear current emails to avoid confusion during transition // Clear current emails to avoid confusion during transition
setEmails([]); setEmails([]);
// Update selected account first (use type assertion) // Update selected account
setSelectedAccount(account as any); setSelectedAccount(account);
logEmailOp('ACCOUNT-SELECT', `Selected account set to: ${account.email}`);
// Initially hide folders during transition // Initially hide folders during transition
setShowFolders(false); setShowFolders(false);
// Use a small delay to ensure UI updates before showing folders and loading emails // Find an INBOX folder in this account's folders
// Look for exact INBOX or accountId:INBOX pattern
const inboxPatterns = ['INBOX', ':INBOX', 'inbox', 'Inbox'];
let inboxFolder = account.folders.find(f =>
inboxPatterns.some(pattern => f.includes(pattern))
);
// If no inbox found, use the first folder or create a default inbox path
if (!inboxFolder) {
inboxFolder = account.folders.length > 0
? account.folders[0]
: `${account.id}:INBOX`;
logEmailOp('ACCOUNT-SELECT', `No INBOX found, using folder: ${inboxFolder}`);
} else {
logEmailOp('ACCOUNT-SELECT', `Found INBOX folder: ${inboxFolder}`);
}
// Ensure the folder has the account prefix
if (!inboxFolder.includes(':')) {
inboxFolder = `${account.id}:${inboxFolder}`;
logEmailOp('ACCOUNT-SELECT', `Added account prefix to folder: ${inboxFolder}`);
}
// We don't need to manually set currentFolder as it's managed by changeFolder
// Instead, just log that we'll update it through the proper function
logEmailOp('ACCOUNT-SELECT', `Will update current folder to: ${inboxFolder} through folder change`);
// Create a streamlined account selection sequence
// Step 1: Show folders immediately for better feedback
setShowFolders(true);
logEmailOp('ACCOUNT-SELECT', `Folders shown for account: ${account.email}`);
// Step 2: Update the selected folders map immediately
setSelectedFolders(prev => {
const updated = {
...prev,
[account.id.toString()]: inboxFolder
};
logEmailOp('ACCOUNT-SELECT', `Updated selected folders map`, updated);
return updated;
});
// Step 3: Wait a small delay and then load emails
logEmailOp('ACCOUNT-SELECT', `Starting folder change after short delay`);
setTimeout(() => { setTimeout(() => {
if (account.id !== 'loading-account') { logEmailOp('ACCOUNT-SELECT', `Triggering folder change to: ${inboxFolder}`);
// Now show folders handleMailboxChange(inboxFolder, account.id.toString());
setShowFolders(true); setLoading(false);
}, 100); // Reduced delay for better responsiveness
// Default to INBOX for the selected account
const inboxFolder = account.folders.find(f =>
f.includes('INBOX') ||
(f.includes(':') && f.split(':')[1] === 'INBOX')
) || (account.id + ':INBOX');
// Update the selected folders map
setSelectedFolders(prev => ({
...prev,
[account.id.toString()]: inboxFolder
}));
// Use a slightly longer delay before loading emails to ensure all state is updated
setTimeout(() => {
handleMailboxChange(inboxFolder, account.id.toString());
setLoading(false);
}, 200);
} else {
setLoading(false);
}
}, 100);
}; };
const handleAddAccount = async (accountData: AccountData) => { const handleAddAccount = async (accountData: AccountData) => {

View File

@ -44,12 +44,32 @@ const loadEmails = async (
accountIdOverride?: string accountIdOverride?: string
) => { ) => {
const folderToUse = folderOverride || currentFolder; const folderToUse = folderOverride || currentFolder;
// Enhanced folder and account handling
let normalizedFolder = folderToUse;
let normalizedAccountId = accountIdOverride;
// Extract account ID from folder if it has a prefix and no explicit account ID was provided
if (folderToUse.includes(':')) {
const [folderAccountId, baseFolderName] = folderToUse.split(':');
console.log(`Folder has prefix: accountId=${folderAccountId}, baseName=${baseFolderName}`);
// If no explicit account ID was provided, use the one from the folder name
if (!normalizedAccountId) {
normalizedAccountId = folderAccountId;
console.log(`Using account ID from folder prefix: ${normalizedAccountId}`);
}
// If both exist but don't match, log a warning
else if (normalizedAccountId !== folderAccountId) {
console.warn(`⚠️ Mismatch between folder account prefix (${folderAccountId}) and provided accountId (${normalizedAccountId})`);
console.warn(`Using provided accountId (${normalizedAccountId}) but this may cause unexpected behavior`);
}
}
const pageToUse = pageOverride || page; const pageToUse = pageOverride || page;
const perPageToUse = perPageOverride || perPage; const perPageToUse = perPageOverride || perPage;
const accountIdToUse = accountIdOverride !== undefined ? accountIdOverride :
folderToUse.includes(':') ? folderToUse.split(':')[0] : undefined;
console.log(`Loading emails: folder=${folderToUse}, page=${pageToUse}, accountId=${accountIdToUse || 'default'}`); console.log(`Loading emails: folder=${folderToUse}, page=${pageToUse}, accountId=${normalizedAccountId || 'default'}`);
try { try {
setIsLoading(true); setIsLoading(true);
@ -59,8 +79,8 @@ const loadEmails = async (
let url = `/api/courrier/emails?folder=${encodeURIComponent(folderToUse)}&page=${pageToUse}&perPage=${perPageToUse}`; let url = `/api/courrier/emails?folder=${encodeURIComponent(folderToUse)}&page=${pageToUse}&perPage=${perPageToUse}`;
// Add accountId parameter if specified // Add accountId parameter if specified
if (accountIdToUse) { if (normalizedAccountId) {
url += `&accountId=${encodeURIComponent(accountIdToUse)}`; url += `&accountId=${encodeURIComponent(normalizedAccountId)}`;
} }
// Add cache-busting timestamp // Add cache-busting timestamp
@ -77,27 +97,38 @@ const loadEmails = async (
} catch { } catch {
errorText = `HTTP error: ${response.status}`; errorText = `HTTP error: ${response.status}`;
} }
console.error(`API error: ${errorText}`);
throw new Error(errorText); throw new Error(errorText);
} }
const data = await response.json(); const data = await response.json();
console.log(`Received ${data.emails?.length || 0} emails from API`);
if (pageOverride === 1 || !pageOverride) { if (pageToUse === 1 || !pageOverride) {
// Replace emails when loading first page // Replace emails when loading first page
setEmails(data.emails); console.log(`Setting ${data.emails?.length || 0} emails (replacing existing)`);
setEmails(data.emails || []);
} else { } else {
// Append emails when loading subsequent pages // Append emails when loading subsequent pages
setEmails(prev => [...prev, ...data.emails]); console.log(`Appending ${data.emails?.length || 0} emails to existing list`);
setEmails(prev => [...prev, ...(data.emails || [])]);
} }
// Update pagination info // Update pagination info
setTotalPages(data.totalPages); setTotalPages(data.totalPages || 0);
setMailboxes(data.mailboxes || []); if (data.mailboxes && data.mailboxes.length > 0) {
console.log(`Received ${data.mailboxes.length} mailboxes from API`);
setMailboxes(data.mailboxes);
}
return data; return data;
} catch (error) { } catch (error) {
console.error('Error loading emails:', error); console.error('Error loading emails:', error);
setError(`Failed to load emails: ${error instanceof Error ? error.message : 'Unknown error'}`); setError(`Failed to load emails: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Set empty emails array on error to prevent UI issues
if (pageToUse === 1) {
setEmails([]);
}
return null; return null;
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View File

@ -142,32 +142,64 @@ export default function EmailSidebar({
// Improve the renderFolderButton function to ensure consistent handling // Improve the renderFolderButton function to ensure consistent handling
const renderFolderButton = (folder: string, accountId: string) => { const renderFolderButton = (folder: string, accountId: string) => {
// Ensure folder always has accountId prefix for consistency // Add extra logging to debug folder rendering issues
const prefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`; console.log(`Rendering folder button: ${folder} for account: ${accountId}`);
// Extract the base folder name and account ID for display and checking // Ensure folder has a consistent format
const [folderAccountId, baseFolder] = prefixedFolder.includes(':') let prefixedFolder = folder;
? prefixedFolder.split(':') let baseFolderName = folder;
: [accountId, folder]; let folderAccountId = accountId;
// Extract parts if the folder has a prefix
if (folder.includes(':')) {
const parts = folder.split(':');
folderAccountId = parts[0];
baseFolderName = parts[1];
console.log(`Folder has prefix, extracted: accountId=${folderAccountId}, baseFolder=${baseFolderName}`);
} else {
// Add account prefix if missing
prefixedFolder = `${accountId}:${folder}`;
console.log(`Added prefix to folder: ${prefixedFolder}`);
}
// Only show folders that belong to this account // Only show folders that belong to this account
if (folderAccountId !== accountId) return null; if (folderAccountId !== accountId) {
console.log(`Skipping folder ${folder} - belongs to different account (${folderAccountId})`);
return null;
}
const isSelected = selectedFolders[accountId] === prefixedFolder; // Check if this folder is selected for this account
// Must handle both prefixed and non-prefixed versions in the selected map
const isSelected =
(selectedFolders[accountId] === prefixedFolder) ||
(selectedFolders[accountId] === baseFolderName) ||
(selectedFolders[accountId]?.split(':')[1] === baseFolderName);
if (isSelected) {
console.log(`Folder ${baseFolderName} is selected for account ${accountId}`);
}
// Get unread count
const folderUnreadCount = unreadCount[accountId]?.[baseFolderName] ||
unreadCount[accountId]?.[prefixedFolder] || 0;
return ( return (
<Button <Button
key={folder} key={prefixedFolder}
variant="ghost" variant="ghost"
className={`w-full justify-start text-xs py-1 h-7 ${isSelected ? 'bg-gray-100' : ''}`} className={`w-full justify-start text-xs py-1 h-7 ${isSelected ? 'bg-gray-100' : ''}`}
onClick={() => onFolderChange(folder, accountId)} onClick={() => {
console.log(`Folder button clicked: ${prefixedFolder}`);
onFolderChange(prefixedFolder, accountId);
}}
> >
<div className="flex items-center w-full"> <div className="flex items-center w-full">
{getFolderIcon(baseFolder)} {getFolderIcon(baseFolderName)}
<span className="ml-2 truncate text-gray-700">{formatFolderName(baseFolder)}</span> <span className="ml-2 truncate text-gray-700">{formatFolderName(baseFolderName)}</span>
{baseFolder === 'INBOX' && unreadCount[accountId]?.[folder] > 0 && ( {folderUnreadCount > 0 && (
<span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full"> <span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full">
{unreadCount[accountId][folder]} {folderUnreadCount}
</span> </span>
)} )}
</div> </div>