"use client"; import { Email, ThreadGroup } from "@/lib/jmap/types"; import { ThreadListItem } from "./thread-list-item"; import { EmailContextMenu } from "./email-context-menu"; import { cn } from "@/lib/utils"; import { Inbox, CheckSquare, Square, Trash2, Mail, MailOpen, Loader2 } from "lucide-react"; import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { useEmailStore } from "@/stores/email-store"; import { useAuthStore } from "@/stores/auth-store"; import { groupEmailsByThread, sortThreadGroups } from "@/lib/thread-utils"; import { useContextMenu } from "@/hooks/use-context-menu"; interface EmailListProps { emails: Email[]; selectedEmailId?: string; onEmailSelect?: (email: Email) => void; className?: string; isLoading?: boolean; // Mobile conversation view handler onOpenConversation?: (thread: ThreadGroup) => void; // Context menu actions onReply?: (email: Email) => void; onReplyAll?: (email: Email) => void; onForward?: (email: Email) => void; onMarkAsRead?: (email: Email, read: boolean) => void; onToggleStar?: (email: Email) => void; onDelete?: (email: Email) => void; onArchive?: (email: Email) => void; onSetColorTag?: (emailId: string, color: string | null) => void; onMoveToMailbox?: (emailId: string, mailboxId: string) => void; } export function EmailList({ emails, selectedEmailId, onEmailSelect, className, isLoading = false, onOpenConversation, onReply, onReplyAll, onForward, onMarkAsRead, onToggleStar, onDelete, onArchive, onSetColorTag, onMoveToMailbox, }: EmailListProps) { const { client } = useAuthStore(); const { selectedEmailIds, selectAllEmails, clearSelection, batchMarkAsRead, batchDelete, batchMoveToMailbox, loadMoreEmails, hasMoreEmails, isLoadingMore, mailboxes, selectedMailbox, expandedThreadIds, threadEmailsCache, isLoadingThread, toggleThreadExpansion, fetchThreadEmails, } = useEmailStore(); // Group emails by thread const threadGroups = useMemo(() => { const groups = groupEmailsByThread(emails); return sortThreadGroups(groups); }, [emails]); // Context menu state const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu(); const [isProcessing, setIsProcessing] = useState(false); const observerTarget = useRef(null); // Loading skeleton component - gentler, no pulsing const LoadingSkeleton = () => (
{[...Array(8)].map((_, i) => (
))}
); const hasSelection = selectedEmailIds.size > 0; const allSelected = emails.length > 0 && emails.every(e => selectedEmailIds.has(e.id)); const handleBatchMarkAsRead = async (read: boolean) => { if (!client || isProcessing) return; setIsProcessing(true); try { await batchMarkAsRead(client, read); } finally { setTimeout(() => setIsProcessing(false), 500); // Small delay for visual feedback } }; const handleBatchDelete = async () => { if (!client || isProcessing || !confirm(`Delete ${selectedEmailIds.size} emails?`)) return; setIsProcessing(true); try { await batchDelete(client); } finally { setTimeout(() => setIsProcessing(false), 500); } }; // Intersection observer for infinite scroll const handleLoadMore = useCallback(() => { if (client && hasMoreEmails && !isLoadingMore && !isLoading) { loadMoreEmails(client); } }, [client, hasMoreEmails, isLoadingMore, isLoading, loadMoreEmails]); // Handle thread expansion and fetch complete thread const handleToggleThreadExpansion = useCallback(async (threadId: string) => { const isExpanded = expandedThreadIds.has(threadId); if (!isExpanded && client) { // Expanding - fetch complete thread emails toggleThreadExpansion(threadId); await fetchThreadEmails(client, threadId); } else { // Collapsing - just toggle toggleThreadExpansion(threadId); } }, [client, expandedThreadIds, toggleThreadExpansion, fetchThreadEmails]); useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { handleLoadMore(); } }, { threshold: 0.1 } ); const currentTarget = observerTarget.current; if (currentTarget) { observer.observe(currentTarget); } return () => { if (currentTarget) { observer.unobserve(currentTarget); } }; }, [handleLoadMore]); return (
{/* Batch Actions Toolbar with smooth transition */}
{selectedEmailIds.size} {selectedEmailIds.size === 1 ? 'email' : 'emails'} selected
{/* List Header */}

{isLoading ? 'Loading...' : threadGroups.length > 0 ? `${threadGroups.length} conversations` : 'No conversations'}

{/* Email List */}
{/* Loading overlay - shows on top of existing emails */} {isLoading && emails.length > 0 && (
Loading...
)} {/* Show skeleton only on initial load (no emails yet) */} {isLoading && emails.length === 0 ? ( ) : emails.length === 0 && !isLoading ? (

No emails in this mailbox

New messages will appear here

) : (
{threadGroups.map((thread) => ( handleToggleThreadExpansion(thread.threadId)} onEmailSelect={(email) => onEmailSelect?.(email)} onContextMenu={openContextMenu} onOpenConversation={onOpenConversation} /> ))} {/* Intersection observer target for infinite scroll - always present */}
{isLoadingMore && hasMoreEmails && (
Loading more emails...
)} {!hasMoreEmails && emails.length > 0 && (
No more emails to load
)}
)}
{/* Context Menu */} {contextMenu.data && ( onReply?.(contextMenu.data!)} onReplyAll={() => onReplyAll?.(contextMenu.data!)} onForward={() => onForward?.(contextMenu.data!)} onMarkAsRead={(read) => onMarkAsRead?.(contextMenu.data!, read)} onToggleStar={() => onToggleStar?.(contextMenu.data!)} onDelete={() => onDelete?.(contextMenu.data!)} onArchive={() => onArchive?.(contextMenu.data!)} onSetColorTag={(color) => onSetColorTag?.(contextMenu.data!.id, color)} onMoveToMailbox={(mailboxId) => onMoveToMailbox?.(contextMenu.data!.id, mailboxId)} // Batch actions onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)} onBatchDelete={() => client && batchDelete(client)} onBatchMoveToMailbox={(mailboxId) => client && batchMoveToMailbox(client, mailboxId)} /> )}
); }