Neah/hooks/use-courrier.ts

559 lines
16 KiB
TypeScript

import { useState, useCallback, useEffect, useRef } 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';
export interface EmailAddress {
name: string;
address: string;
}
export interface Email {
id: string;
from: EmailAddress[];
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
subject: string;
content: string;
preview?: string;
date: string;
read: boolean;
starred: boolean;
attachments?: { filename: string; contentType: string; size: number; content?: string }[];
folder: string;
hasAttachments: boolean;
contentFetched?: boolean;
}
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;
// Near the top of the file, before the useCourrier hook
interface EmailResponse {
emails: Email[];
total: number;
totalPages: number;
hasMore: boolean;
}
// 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);
const [hasMore, setHasMore] = useState(false);
// Auth and notifications
const { data: session } = useSession();
const { toast } = useToast();
// Add the missing refs
const loadingRequestsRef = useRef<Set<string>>(new Set());
const loadMoreRef = useRef<number>(0);
// Load emails from the server
const loadEmails = useCallback(
async (folder = currentFolder, pageToLoad = page, resetList = true, isInitial = false) => {
if (!session?.user?.id || isLoading) return;
// Track this request to avoid duplicates
const requestKey = `${folder}_${pageToLoad}`;
if (loadingRequestsRef.current.has(requestKey)) {
console.log(`Skipping duplicate request for ${requestKey}`);
return;
}
loadingRequestsRef.current.add(requestKey);
setIsLoading(true);
try {
// Get emails for the current folder
const response = await getEmails(session.user.id, folder, pageToLoad);
// Update state based on response
if (resetList) {
setEmails(response.emails);
} else {
setEmails(prev => [...prev, ...response.emails]);
}
setTotalEmails(response.total);
setTotalPages(response.totalPages);
setHasMore(response.hasMore);
setPage(pageToLoad);
if (folder !== currentFolder) {
setCurrentFolder(folder);
}
// Clear errors
setError(null);
} catch (error) {
console.error('Error loading emails:', error);
setError(error instanceof Error ? error.message : 'Failed to load emails');
toast({
variant: "destructive",
title: "Error",
description: "Failed to load emails"
});
} finally {
setIsLoading(false);
// Clear the loading request tracker
loadingRequestsRef.current.delete(requestKey);
}
},
[session?.user?.id, currentFolder, page, isLoading, toast]
);
// 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;
// Add a small delay to prevent rapid consecutive loads
const loadTimer = setTimeout(() => {
loadEmails(currentFolder, page, false, false);
}, 50);
// 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);
}
return () => clearTimeout(loadTimer);
}
}, [currentFolder, page, session?.user?.id, loadEmails]);
// Fetch a single email's content
const fetchEmailContent = useCallback(async (emailId: string) => {
try {
const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(currentFolder)}`);
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) => {
setIsLoading(true);
try {
// Find the email in the current list
const email = emails.find(e => e.id === emailId);
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);
// 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.read) {
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, read: isRead } : email
));
// If the selected email is the one being marked, update it too
if (selectedEmail && selectedEmail.id === emailId) {
setSelectedEmail({ ...selectedEmail, read: 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.starred;
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, starred: newStarredStatus } : email
));
// If the selected email is the one being starred, update it too
if (selectedEmail && selectedEmail.id === emailId) {
setSelectedEmail({ ...selectedEmail, starred: 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]);
// Change the current folder
const changeFolder = useCallback((folder: MailFolder) => {
setCurrentFolder(folder);
setPage(1);
setSelectedEmail(null);
setSelectedEmailIds([]);
}, []);
// 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, type: 'reply' | 'reply-all' | 'forward') => {
if (!email) return null;
return formatEmailForReplyOrForward(email, type);
}, []);
/**
* Fetches emails from the API
*/
const getEmails = async (userId: string, folder: string, page: number): Promise<EmailResponse> => {
// Build query params
const queryParams = new URLSearchParams({
folder: folder,
page: page.toString(),
perPage: perPage.toString()
});
if (searchQuery) {
queryParams.set('search', searchQuery);
}
// 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 = await response.json();
return {
emails: Array.isArray(data.emails) ? data.emails : [],
total: data.totalEmails || 0,
totalPages: data.totalPages || 0,
hasMore: data.totalPages > page
};
};
/**
* Prefetches emails for a specific folder
*/
const prefetchFolderEmails = async (userId: string, folder: string, startPage: number, endPage: number) => {
try {
for (let p = startPage; p <= endPage; p++) {
await getEmails(userId, folder, p);
// Add small delay between requests
if (p < endPage) await new Promise(r => setTimeout(r, 500));
}
} catch (error) {
console.error("Error prefetching emails:", error);
}
};
// Update loadMoreEmails
const loadMoreEmails = useCallback(async () => {
if (isLoading || !hasMore || !session) {
return;
}
// Don't allow loading more if we've loaded too recently
const now = Date.now();
const lastLoadTime = loadMoreRef.current || 0;
if (now - lastLoadTime < 1000) { // Throttle to once per second
console.log('Throttling loadMoreEmails - too many requests');
return;
}
// Track when we last attempted to load more
loadMoreRef.current = now;
// Load the next page
console.log(`Loading more emails for ${currentFolder}, page ${page + 1}`);
return loadEmails(currentFolder, page + 1, false, false);
}, [isLoading, hasMore, session, currentFolder, page, loadEmails]);
// Return all the functionality and state values
return {
// Data
emails,
selectedEmail,
selectedEmailIds,
currentFolder,
mailboxes,
isLoading,
isSending,
isDeleting,
error,
searchQuery,
page,
perPage,
totalEmails,
totalPages,
hasMore,
// Functions
loadEmails,
handleEmailSelect,
markEmailAsRead,
toggleStarred,
sendEmail,
deleteEmails,
toggleEmailSelection,
toggleSelectAll,
changeFolder,
searchEmails,
formatEmailForAction,
setPage,
setPerPage,
setSearchQuery,
loadMoreEmails,
};
};