courrier correct panel 2 scroll up

This commit is contained in:
alma 2025-04-27 15:27:24 +02:00
parent b2c788818f
commit 41559a4d66
4 changed files with 197 additions and 332 deletions

View File

@ -151,36 +151,9 @@ export default function CourrierPage() {
setLoading(true);
// First check if Redis is ready before making API calls
// Use a cache mechanism to reduce frequency of Redis status checks
const redisCheckCacheKey = 'neah_redis_status_check';
const cachedRedisCheck = localStorage.getItem(redisCheckCacheKey);
let redisStatus = { ready: false };
if (cachedRedisCheck) {
try {
const { status, timestamp } = JSON.parse(cachedRedisCheck);
// Only use cache if it's less than 2 minutes old
if (Date.now() - timestamp < 2 * 60 * 1000) {
redisStatus = status;
console.log('Using cached Redis status check');
}
} catch (e) {
// Invalid JSON in cache, ignore and fetch fresh status
}
}
// Only check Redis status if we don't have a recent cached result
if (!redisStatus.ready) {
redisStatus = await fetch('/api/redis/status')
.then(res => res.json())
.catch(() => ({ ready: false }));
// Cache the result
localStorage.setItem(redisCheckCacheKey, JSON.stringify({
status: redisStatus,
timestamp: Date.now()
}));
}
const redisStatus = await fetch('/api/redis/status')
.then(res => res.json())
.catch(() => ({ ready: false }));
if (!isMounted) return;
@ -273,28 +246,15 @@ export default function CourrierPage() {
const nextPage = page + 1;
setPage(nextPage);
console.log(`Requesting page ${nextPage} for folder ${currentFolder}`);
// Immediately trigger a load for this page rather than relying on the useEffect
// This helps ensure we get the data faster
loadEmails(true).catch(error => {
console.error(`Error loading page ${nextPage}:`, error);
});
// Also prefetch additional pages to make scrolling smoother
if (session?.user?.id) {
// Prefetch next page beyond the one we're loading
const pagesToPrefetch = 2; // Prefetch 2 pages ahead
// Small delay to let the current request start first
setTimeout(() => {
console.log(`Prefetching pages ${nextPage + 1} to ${nextPage + pagesToPrefetch}`);
prefetchFolderEmails(session.user.id, currentFolder, pagesToPrefetch, nextPage + 1)
.catch(err => {
console.error(`Error prefetching additional pages for ${currentFolder}:`, err);
});
}, 200);
// Prefetch next 2 pages beyond the current next page
prefetchFolderEmails(session.user.id, currentFolder, 2, nextPage + 1).catch(err => {
console.error(`Error prefetching additional pages for ${currentFolder}:`, err);
});
}
// Note: loadEmails will be called automatically due to the page dependency in useEffect
}
};
@ -400,8 +360,7 @@ export default function CourrierPage() {
return (
<>
<SimplifiedLoadingFix />
{/* Only render RedisCacheStatus in development mode to avoid unnecessary status checks */}
{process.env.NODE_ENV === 'development' && <RedisCacheStatus />}
<RedisCacheStatus />
{/* Main layout */}
<main className="w-full h-screen bg-black">

View File

@ -44,20 +44,10 @@ export default function EmailList({
const [scrollPosition, setScrollPosition] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [lastLoadTime, setLastLoadTime] = useState(0);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const loadMoreTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const prevEmailsLengthRef = useRef(emails.length);
// Clear any pending timeouts on unmount
useEffect(() => {
return () => {
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
if (loadMoreTimeoutRef.current) clearTimeout(loadMoreTimeoutRef.current);
};
}, []);
// Debounced scroll handler for better performance
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
const target = event.target as HTMLDivElement;
@ -71,37 +61,25 @@ export default function EmailList({
clearTimeout(scrollTimeoutRef.current);
}
// If near bottom (within 300px) and more emails are available, load more
// If near bottom (within 200px) and more emails are available, load more
// Added additional checks to prevent loading loop
const isNearBottom = scrollHeight - scrollTop - clientHeight < 300;
// Don't trigger load if we're already loading or if the last load was too recent (throttle)
const now = Date.now();
const timeSinceLastLoad = now - lastLoadTime;
const tooSoonToLoadAgain = timeSinceLastLoad < 2000; // 2 seconds throttle
if (isNearBottom && hasMoreEmails && !isLoading && !isLoadingMore && !tooSoonToLoadAgain) {
const isNearBottom = scrollHeight - scrollTop - clientHeight < 200;
if (isNearBottom && hasMoreEmails && !isLoading && !isLoadingMore) {
setIsLoadingMore(true);
setLastLoadTime(now);
// Use timeout to debounce load requests
scrollTimeoutRef.current = setTimeout(() => {
// Clear the timeout reference before loading
scrollTimeoutRef.current = null;
console.log('Loading more emails from scroll trigger');
onLoadMore();
// Reset loading state after a delay
if (loadMoreTimeoutRef.current) clearTimeout(loadMoreTimeoutRef.current);
loadMoreTimeoutRef.current = setTimeout(() => {
loadMoreTimeoutRef.current = null;
console.log('Resetting loading more state after timeout');
setTimeout(() => {
setIsLoadingMore(false);
}, 2000); // Reduced from 3000ms to 2000ms to avoid long loading states
}, 200); // Reduced from 300ms to 200ms for better responsiveness
}, 1500); // Increased from 1000ms to 1500ms to prevent quick re-triggering
}, 200); // Increased from 100ms to 200ms for better debouncing
}
}, [hasMoreEmails, isLoading, isLoadingMore, onLoadMore, lastLoadTime]);
}, [hasMoreEmails, isLoading, isLoadingMore, onLoadMore]);
// Restore scroll position when emails are loaded
useEffect(() => {
@ -112,13 +90,8 @@ export default function EmailList({
// 4. We're not in the middle of a loading operation
if (emails.length > prevEmailsLengthRef.current &&
scrollRef.current &&
scrollPosition > 0) {
// If emails have been loaded, force reset the loading state
if (isLoadingMore) {
console.log('Emails loaded, resetting loading state');
setIsLoadingMore(false);
}
scrollPosition > 0 &&
!isLoading) {
// Use requestAnimationFrame to ensure the DOM has updated
requestAnimationFrame(() => {
if (scrollRef.current) {
@ -130,26 +103,7 @@ export default function EmailList({
// Always update the reference for next comparison
prevEmailsLengthRef.current = emails.length;
}, [emails.length, scrollPosition, isLoadingMore]);
// Add safety mechanism to reset loading state if we get stuck
useEffect(() => {
// If we have more emails now but still in loading state, reset it
if (emails.length > prevEmailsLengthRef.current && isLoadingMore) {
console.log('Safety reset: Clearing loading state after emails updated');
setIsLoadingMore(false);
}
// Add a timeout-based safety mechanism - reduced from 5000ms to 3000ms
const safetyTimeout = setTimeout(() => {
if (isLoadingMore) {
console.log('Safety timeout: Resetting stuck loading state');
setIsLoadingMore(false);
}
}, 3000);
return () => clearTimeout(safetyTimeout);
}, [emails.length, isLoadingMore]);
}, [emails.length, scrollPosition, isLoading]);
// Add listener for custom reset scroll event
useEffect(() => {
@ -303,20 +257,15 @@ export default function EmailList({
{/* Loading indicator */}
{(isLoading || isLoadingMore) && (
<div className="flex items-center justify-center p-4 bg-gray-50">
<Loader2 className="h-4 w-4 text-blue-500 animate-spin mr-2" />
<span className="text-sm text-gray-500">Loading more emails...</span>
<div className="flex items-center justify-center p-4">
<Loader2 className="h-4 w-4 text-blue-500 animate-spin" />
</div>
)}
{/* Load more button - only show when near bottom but not auto-loading */}
{hasMoreEmails && !isLoading && !isLoadingMore && (
<button
onClick={() => {
console.log('Manual load more triggered');
setLastLoadTime(Date.now());
onLoadMore();
}}
onClick={onLoadMore}
className="w-full py-2 text-gray-500 hover:bg-gray-100 text-sm"
>
Load more emails

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useToast } from './use-toast';
import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter';
@ -52,14 +52,6 @@ export interface EmailData {
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
@ -79,81 +71,175 @@ export const useCourrier = () => {
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}`);
const loadEmails = useCallback(async (isLoadMore = false) => {
if (!session?.user?.id) return;
setIsLoading(true);
setError(null);
// Keep reference to the current page for this request
const currentRequestPage = page;
try {
// First try Redis cache with low timeout
const cachedEmails = await getCachedEmailsWithTimeout(session.user.id, currentFolder, currentRequestPage, perPage, 100);
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;
}
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);
// Build query params
const queryParams = new URLSearchParams({
folder: currentFolder,
page: currentRequestPage.toString(),
perPage: perPage.toString()
});
if (searchQuery) {
queryParams.set('search', searchQuery);
}
},
[session?.user?.id, currentFolder, page, isLoading, toast]
);
// 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 ${currentRequestPage}:`, 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]);
// 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);
loadEmails(isLoadingMore);
// If we're loading the first page, publish an event to reset scroll position
if (page === 1 && typeof window !== 'undefined') {
@ -161,10 +247,8 @@ export const useCourrier = () => {
const event = new CustomEvent('reset-email-scroll');
window.dispatchEvent(event);
}
return () => clearTimeout(loadTimer);
}
}, [currentFolder, page, session?.user?.id, loadEmails]);
}, [currentFolder, page, perPage, session?.user?.id, loadEmails]);
// Fetch a single email's content
const fetchEmailContent = useCallback(async (emailId: string) => {
@ -451,75 +535,6 @@ export const useCourrier = () => {
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
@ -537,7 +552,6 @@ export const useCourrier = () => {
perPage,
totalEmails,
totalPages,
hasMore,
// Functions
loadEmails,
@ -554,6 +568,5 @@ export const useCourrier = () => {
setPage,
setPerPage,
setSearchQuery,
loadMoreEmails,
};
};

View File

@ -19,7 +19,7 @@ export async function getCachedEmailsWithTimeout(
folder: string,
page: number,
perPage: number,
timeoutMs: number = 200
timeoutMs: number = 100
): Promise<any | null> {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
@ -73,45 +73,16 @@ export async function refreshEmailsInBackground(
page: number = 1,
perPage: number = 20
): Promise<void> {
// Track ongoing refreshes to avoid duplicates
const refreshKey = `${userId}:${folder}:${page}`;
// Only use small timeouts for INBOX, other folders can wait longer
const priority = folder.toUpperCase() === 'INBOX' && page <= 2 ? 100 : 500;
// Use setTimeout to ensure this runs after current execution context
setTimeout(async () => {
try {
// Skip if we've recently refreshed this data (use a module-scope cache)
// We don't need to refresh the same data too frequently
const cacheKey = `${userId}:${folder}:${page}:refreshed`;
const lastRefreshed = (window as any)[cacheKey] || 0;
const now = Date.now();
// Don't refresh if it's been less than 30 seconds for inbox, 2 minutes for other folders
const minInterval = folder.toUpperCase() === 'INBOX' ? 30000 : 120000;
if (now - lastRefreshed < minInterval) {
console.log(`Skipping refresh for ${folder}:${page} - last refreshed ${Math.round((now - lastRefreshed)/1000)}s ago`);
return;
}
console.log(`Background refresh for ${userId}:${folder}:${page}:${perPage}`);
const freshData = await getEmails(userId, folder, page, perPage);
console.log(`Background refresh completed for ${userId}:${folder}:${page} with ${freshData.emails.length} emails`);
// Mark as refreshed
(window as any)[cacheKey] = now;
// For inbox first page only, prefetch page 2 but with a longer delay
if (folder.toUpperCase() === 'INBOX' && page === 1) {
setTimeout(() => {
refreshEmailsInBackground(userId, folder, 2, perPage);
}, 1000);
}
console.log(`Background refresh completed for ${userId}:${folder}`);
} catch (error) {
console.error('Background refresh error:', error);
}
}, priority);
}, 100);
}
/**
@ -204,57 +175,30 @@ export async function prefetchFolderEmails(
try {
console.log(`Prefetching ${pages} pages of emails for folder ${folder} starting from page ${startPage}`);
// Limit the number of pages to prefetch to reduce server load
const maxPages = 3;
const actualPages = Math.min(pages, maxPages);
// Calculate the range of pages to prefetch
const pagesToFetch = Array.from(
{ length: actualPages },
{ length: pages },
(_, i) => startPage + i
);
console.log(`Will prefetch pages: ${pagesToFetch.join(', ')}`);
// Fetch pages sequentially with delays to avoid overwhelming the server
// Focus on the first page first, which is most important
const fetchPage = async (pageIndex: number) => {
if (pageIndex >= pagesToFetch.length) return;
const page = pagesToFetch[pageIndex];
try {
// Skip if we've recently prefetched this page
const cacheKey = `${userId}:${folder}:${page}:prefetched`;
const lastPrefetched = (window as any)[cacheKey] || 0;
const now = Date.now();
// Don't prefetch if it's been less than 1 minute
if (now - lastPrefetched < 60000) {
console.log(`Skipping prefetch for ${folder}:${page} - prefetched ${Math.round((now - lastPrefetched)/1000)}s ago`);
// Continue with next page
setTimeout(() => fetchPage(pageIndex + 1), 100);
return;
}
console.log(`Prefetching page ${page} of ${folder}`);
const result = await getEmails(userId, folder, page, 20);
console.log(`Successfully prefetched page ${page} of ${folder} with ${result.emails.length} emails`);
// Mark as prefetched
(window as any)[cacheKey] = now;
// Fetch next page with delay
setTimeout(() => fetchPage(pageIndex + 1), 500);
} catch (err) {
console.error(`Error prefetching page ${page} of ${folder}:`, err);
// Try next page anyway after a longer delay
setTimeout(() => fetchPage(pageIndex + 1), 1000);
}
};
// Fetch multiple pages in parallel
await Promise.allSettled(
pagesToFetch.map(page =>
getEmails(userId, folder, page, 20)
.then(result => {
console.log(`Successfully prefetched and cached page ${page} of ${folder} with ${result.emails.length} emails`);
return result;
})
.catch(err => {
console.error(`Error prefetching page ${page} of ${folder}:`, err);
return null;
})
)
);
// Start fetching the first page
fetchPage(0);
console.log(`Completed prefetching ${pages} pages of ${folder}`);
} catch (error) {
console.error(`Error prefetching folder ${folder}:`, error);
}