Neah/hooks/use-courrier.ts

626 lines
20 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useToast } from './use-toast';
import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter';
import { getCachedEmailsWithTimeout, refreshEmailsInBackground } from '@/lib/services/prefetch-service';
import { EmailAddress, EmailAttachment } from '@/lib/types';
export interface Email {
id: string;
from: EmailAddress[];
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
subject: string;
content: {
text: string;
html: string;
};
preview?: string;
date: Date;
flags: {
seen: boolean;
answered: boolean;
flagged: boolean;
draft: boolean;
deleted: boolean;
};
size: number;
hasAttachments: boolean;
folder: string;
contentFetched: boolean;
accountId?: string;
messageId?: string;
attachments?: EmailAttachment[];
}
export interface EmailListResult {
emails: Email[];
totalEmails: number;
page: number;
perPage: number;
totalPages: number;
folder: string;
mailboxes: string[];
}
export interface EmailData {
to: string;
cc?: string;
bcc?: string;
subject: string;
body: string;
attachments?: Array<{
name: string;
content: string;
type: string;
}>;
}
export type MailFolder = string;
// Hook for managing email operations
export const useCourrier = () => {
// State for email data
const [emails, setEmails] = useState<Email[]>([]);
const [selectedEmail, setSelectedEmail] = useState<Email | null>(null);
const [selectedEmailIds, setSelectedEmailIds] = useState<string[]>([]);
const [currentFolder, setCurrentFolder] = useState<MailFolder>('INBOX');
const [mailboxes, setMailboxes] = useState<string[]>([]);
// State for UI
const [isLoading, setIsLoading] = useState(false);
const [isSending, setIsSending] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [totalEmails, setTotalEmails] = useState(0);
const [totalPages, setTotalPages] = useState(0);
// Auth and notifications
const { data: session } = useSession();
const { toast } = useToast();
// Load emails from the server
const loadEmails = useCallback(async (isLoadMore = false, accountId?: string) => {
if (!session?.user?.id) return;
setIsLoading(true);
setError(null);
try {
// Construct query parameters
const queryParams = new URLSearchParams({
folder: currentFolder,
page: page.toString(),
perPage: perPage.toString()
});
if (searchQuery) {
queryParams.set('search', searchQuery);
}
// Add accountId to query params if provided
if (accountId) {
queryParams.set('accountId', accountId);
}
// Try to get cached emails first
const currentRequestPage = page;
const cachedEmails = await getCachedEmailsWithTimeout(
session.user.id,
accountId || 'default',
currentFolder,
currentRequestPage,
perPage,
accountId && accountId !== 'all-accounts' && accountId !== 'loading-account' ? accountId : undefined
);
if (cachedEmails) {
// Ensure cached data has emails array property
if (Array.isArray(cachedEmails.emails)) {
if (isLoadMore) {
// When loading more, always append to the existing list
setEmails(prevEmails => {
// Create a Set of existing email IDs to avoid duplicates
const existingIds = new Set(prevEmails.map(email => email.id));
// Filter out any duplicates before appending
const newEmails = cachedEmails.emails.filter((email: Email) => !existingIds.has(email.id));
// Log pagination info
console.log(`Added ${newEmails.length} cached emails from page ${currentRequestPage} to existing ${prevEmails.length} emails`);
// Combine emails and sort them by date (newest first)
const combinedEmails = [...prevEmails, ...newEmails];
return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime());
});
} else {
// For initial load, replace emails
console.log(`Setting ${cachedEmails.emails.length} cached emails for page ${currentRequestPage}`);
// Ensure emails are sorted by date (newest first)
setEmails(cachedEmails.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()));
}
// Set pagination info from cache if available
if (cachedEmails.totalEmails) setTotalEmails(cachedEmails.totalEmails);
if (cachedEmails.totalPages) setTotalPages(cachedEmails.totalPages);
// Update available mailboxes if provided
if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) {
setMailboxes(cachedEmails.mailboxes);
}
} else if (Array.isArray(cachedEmails)) {
// Direct array response
if (isLoadMore) {
setEmails(prevEmails => {
// Create a Set of existing email IDs to avoid duplicates
const existingIds = new Set(prevEmails.map(email => email.id));
// Filter out any duplicates before appending
const newEmails = cachedEmails.filter((email: Email) => !existingIds.has(email.id));
// Log pagination info
console.log(`Added ${newEmails.length} cached emails from page ${currentRequestPage} to existing ${prevEmails.length} emails`);
// Combine emails and sort them by date (newest first)
const combinedEmails = [...prevEmails, ...newEmails];
return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime());
});
} else {
// For initial load, replace emails
console.log(`Setting ${cachedEmails.length} cached emails for page ${currentRequestPage}`);
// Ensure emails are sorted by date (newest first)
setEmails(cachedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()));
}
} else {
console.warn('Invalid cache format:', cachedEmails);
}
setIsLoading(false);
// Still refresh in background for fresh data
refreshEmailsInBackground(session.user.id, currentFolder, currentRequestPage, perPage).catch(err => {
console.error('Background refresh error:', err);
});
return;
}
// Fetch emails from API
const response = await fetch(`/api/courrier?${queryParams.toString()}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch emails');
}
const data: EmailListResult = await response.json();
// Update state with the fetched data
if (isLoadMore) {
setEmails(prev => {
// Create a Set of existing email IDs to avoid duplicates
const existingIds = new Set(prev.map(email => email.id));
// Filter out any duplicates before appending
const newEmails = data.emails.filter((email: Email) => !existingIds.has(email.id));
// Log pagination info
console.log(`Added ${newEmails.length} fetched emails from page ${currentRequestPage} to existing ${prev.length} emails`);
// Combine emails and sort them by date (newest first)
const combinedEmails = [...prev, ...newEmails];
return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime());
});
} else {
// Ensure we always set an array even if API returns invalid data
console.log(`Setting ${data.emails?.length || 0} fetched emails for page ${currentRequestPage}`);
// Ensure emails are sorted by date (newest first)
if (Array.isArray(data.emails)) {
setEmails(data.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()));
} else {
setEmails([]);
}
}
setTotalEmails(data.totalEmails);
setTotalPages(data.totalPages);
// Update available mailboxes if provided
if (data.mailboxes && data.mailboxes.length > 0) {
setMailboxes(data.mailboxes);
}
// Clear selection if not loading more
if (!isLoadMore) {
setSelectedEmail(null);
setSelectedEmailIds([]);
}
} catch (err) {
console.error(`Error loading emails for page ${page}:`, err);
// Set emails to empty array on error to prevent runtime issues
if (!isLoadMore) {
setEmails([]);
}
setError(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 {
setIsLoading(false);
}
}, [currentFolder, page, perPage, searchQuery, session?.user?.id, toast]);
// Change folder and load emails from that folder
const changeFolder = useCallback(async (folder: string, accountId?: string) => {
console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`);
try {
// Reset selected email
setSelectedEmail(null);
setSelectedEmailIds([]);
// Record the new folder
setCurrentFolder(folder);
// Reset search query when changing folders
setSearchQuery('');
// Reset to page 1
setPage(1);
// Clear existing emails before loading new ones to prevent UI flicker
setEmails([]);
// Show loading state
setIsLoading(true);
// Use a small delay to ensure state updates have propagated
// This helps prevent race conditions when multiple folders are clicked quickly
await new Promise(resolve => setTimeout(resolve, 100));
// Call loadEmails with correct boolean parameter type
await loadEmails(false, accountId);
} catch (error) {
console.error(`Error changing to folder ${folder}:`, error);
setError(error instanceof Error ? error.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}, [loadEmails]);
// Load emails when folder or page changes
useEffect(() => {
if (session?.user?.id) {
// If page is greater than 1, we're loading more emails
const isLoadingMore = page > 1;
loadEmails(isLoadingMore);
// If we're loading the first page, publish an event to reset scroll position
if (page === 1 && typeof window !== 'undefined') {
// Use a custom event to communicate with the EmailList component
const event = new CustomEvent('reset-email-scroll');
window.dispatchEvent(event);
}
}
}, [currentFolder, page, perPage, session?.user?.id, loadEmails]);
// Fetch a single email's content
const fetchEmailContent = useCallback(async (emailId: string, accountId?: string, folderOverride?: string) => {
try {
const folderToUse = folderOverride || currentFolder;
const query = new URLSearchParams({
folder: folderToUse,
});
if (accountId) query.set('accountId', accountId);
const response = await fetch(`/api/courrier/${emailId}?${query.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch email content: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching email content:', error);
throw error;
}
}, [currentFolder]);
// Select an email to view
const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folderOverride: string) => {
setIsLoading(true);
try {
// Find the email in the current list
const email = emails.find(e => e.id === emailId && e.accountId === accountId && e.folder === folderOverride);
if (!email) {
throw new Error('Email not found');
}
// If content is not fetched, get the full content
if (!email.contentFetched) {
const fullEmail = await fetchEmailContent(emailId, accountId, folderOverride);
// Merge the full content with the email
const updatedEmail = {
...email,
content: fullEmail.content,
attachments: fullEmail.attachments,
contentFetched: true
};
// Update the email in the list
setEmails(emails.map(e => e.id === emailId ? updatedEmail : e));
setSelectedEmail(updatedEmail);
} else {
setSelectedEmail(email);
}
// Mark the email as read if it's not already
if (!email.flags.seen) {
markEmailAsRead(emailId, true);
}
} catch (err) {
console.error('Error selecting email:', err);
toast({
variant: "destructive",
title: "Error",
description: "Could not load email content"
});
} finally {
setIsLoading(false);
}
}, [emails, fetchEmailContent, toast]);
// Mark an email as read/unread
const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => {
try {
const response = await fetch(`/api/courrier/${emailId}/mark-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
isRead,
folder: currentFolder
})
});
if (!response.ok) {
throw new Error('Failed to mark email as read');
}
// Update the email in the list
setEmails(emails.map(email =>
email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : email
));
// If the selected email is the one being marked, update it too
if (selectedEmail && selectedEmail.id === emailId) {
setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, seen: isRead } });
}
return true;
} catch (error) {
console.error('Error marking email as read:', error);
return false;
}
}, [emails, selectedEmail, currentFolder]);
// Toggle starred status for an email
const toggleStarred = useCallback(async (emailId: string) => {
const email = emails.find(e => e.id === emailId);
if (!email) return;
const newStarredStatus = !email.flags.flagged;
try {
const response = await fetch(`/api/courrier/${emailId}/star`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
starred: newStarredStatus,
folder: currentFolder
})
});
if (!response.ok) {
throw new Error('Failed to toggle star status');
}
// Update the email in the list
setEmails(emails.map(email =>
email.id === emailId ? { ...email, flags: { ...email.flags, flagged: newStarredStatus } } : email
));
// If the selected email is the one being starred, update it too
if (selectedEmail && selectedEmail.id === emailId) {
setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, flagged: newStarredStatus } });
}
} catch (error) {
console.error('Error toggling star status:', error);
toast({
variant: "destructive",
title: "Error",
description: "Could not update star status"
});
}
}, [emails, selectedEmail, currentFolder, toast]);
// Send an email
const sendEmail = useCallback(async (emailData: EmailData) => {
if (!session?.user?.id) {
toast({
variant: "destructive",
title: "Error",
description: "You must be logged in to send emails"
});
return { success: false, error: "Not authenticated" };
}
setIsSending(true);
try {
const response = await fetch('/api/courrier/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(emailData)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to send email');
}
toast({
title: "Success",
description: "Email sent successfully"
});
return { success: true, messageId: result.messageId };
} catch (error) {
console.error('Error sending email:', 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 {
setIsSending(false);
}
}, [session?.user?.id, toast]);
// Delete selected emails
const deleteEmails = useCallback(async (emailIds: string[]) => {
if (emailIds.length === 0) return;
setIsDeleting(true);
try {
const response = await fetch('/api/courrier/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
emailIds,
folder: currentFolder
})
});
if (!response.ok) {
throw new Error('Failed to delete emails');
}
// Remove the deleted emails from the list
setEmails(emails.filter(email => !emailIds.includes(email.id)));
// Clear selection if the selected email was deleted
if (selectedEmail && emailIds.includes(selectedEmail.id)) {
setSelectedEmail(null);
}
// Clear selected IDs
setSelectedEmailIds([]);
toast({
title: "Success",
description: `${emailIds.length} email(s) deleted`
});
} catch (error) {
console.error('Error deleting emails:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to delete emails"
});
} finally {
setIsDeleting(false);
}
}, [emails, selectedEmail, currentFolder, toast]);
// Toggle selection of an email
const toggleEmailSelection = useCallback((emailId: string) => {
setSelectedEmailIds(prev => {
if (prev.includes(emailId)) {
return prev.filter(id => id !== emailId);
} else {
return [...prev, emailId];
}
});
}, []);
// Select all emails
const toggleSelectAll = useCallback(() => {
if (selectedEmailIds.length === emails.length) {
setSelectedEmailIds([]);
} else {
setSelectedEmailIds(emails.map(email => email.id));
}
}, [emails, selectedEmailIds]);
// Search emails
const searchEmails = useCallback((query: string) => {
setSearchQuery(query);
setPage(1);
loadEmails();
}, [loadEmails]);
// Format an email for reply or forward
const formatEmailForAction = useCallback((email: Email) => {
return {
id: email.id,
subject: email.subject,
from: email.from[0]?.address || '',
to: email.to.map(addr => addr.address).join(', '),
cc: email.cc?.map(addr => addr.address).join(', '),
bcc: email.bcc?.map(addr => addr.address).join(', '),
content: email.content.text || email.content.html || '',
attachments: email.attachments
};
}, []);
// Return all the functionality and state values
return {
// Data
emails,
selectedEmail,
selectedEmailIds,
currentFolder,
mailboxes,
isLoading,
isSending,
isDeleting,
error,
searchQuery,
page,
perPage,
totalEmails,
totalPages,
// Functions
loadEmails,
handleEmailSelect,
markEmailAsRead,
toggleStarred,
sendEmail,
deleteEmails,
toggleEmailSelection,
toggleSelectAll,
changeFolder,
searchEmails,
formatEmailForAction,
setPage,
setPerPage,
setSearchQuery,
// Added email state setter
setEmails,
};
};