"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"; import { useTranslations } from "next-intl"; 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; onMarkAsSpam?: (email: Email) => void; onUndoSpam?: (email: Email) => void; } export function EmailList({ emails, selectedEmailId, onEmailSelect, className, isLoading = false, onOpenConversation, onReply, onReplyAll, onForward, onMarkAsRead, onToggleStar, onDelete, onArchive, onSetColorTag, onMarkAsSpam, onUndoSpam, onMoveToMailbox, }: EmailListProps) { const t = useTranslations('email_list'); const { client } = useAuthStore(); const { selectedEmailIds, selectAllEmails, clearSelection, batchMarkAsRead, batchDelete, batchMoveToMailbox, batchMarkAsSpam, batchUndoSpam, loadMoreEmails, hasMoreEmails, isLoadingMore, totalEmails, 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 ? t('loading') : threadGroups.length > 0 ? (totalEmails !== undefined && totalEmails > threadGroups.length ? t('conversations_count', { count: threadGroups.length, total: totalEmails }) : hasMoreEmails ? t('conversations_count_plus', { count: threadGroups.length }) : t('conversations_count_simple', { count: threadGroups.length })) : t('no_conversations')}

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

{t('no_emails')}

{t('no_emails_description')}

) : (
{threadGroups.map((thread) => ( handleToggleThreadExpansion(thread.threadId)} onEmailSelect={(email) => onEmailSelect?.(email)} onContextMenu={openContextMenu} onOpenConversation={onOpenConversation} /> ))} {/* Intersection observer target for infinite scroll - always present */}
{isLoadingMore && hasMoreEmails && (
{t('loading_more')}
)} {!hasMoreEmails && emails.length > 0 && (
{t('no_more_emails')}
)}
)}
{/* Context Menu */} {contextMenu.data && ( m.id === selectedMailbox)?.role} isMultiSelect={selectedEmailIds.has(contextMenu.data.id)} selectedCount={selectedEmailIds.size} // Single email actions onReply={() => 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)} onMarkAsSpam={() => onMarkAsSpam?.(contextMenu.data!)} onUndoSpam={() => onUndoSpam?.(contextMenu.data!)} // Batch actions onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)} onBatchDelete={() => client && batchDelete(client)} onBatchMoveToMailbox={(mailboxId) => client && batchMoveToMailbox(client, mailboxId)} onBatchMarkAsSpam={async () => { if (client) { const emailIds = Array.from(selectedEmailIds); try { await batchMarkAsSpam(client, emailIds); const { toast } = await import('sonner'); toast.success( t('../email_viewer.spam.toast_batch', { count: emailIds.length }) ); } catch { const { toast } = await import('sonner'); toast.error(t('../email_viewer.spam.error')); } } }} onBatchUndoSpam={async () => { if (client) { const emailIds = Array.from(selectedEmailIds); try { await batchUndoSpam(client, emailIds); const { toast } = await import('sonner'); toast.success( t('../email_viewer.spam.toast_not_spam_batch', { count: emailIds.length }) ); } catch { const { toast } = await import('sonner'); toast.error(t('../email_viewer.spam.error_not_spam')); } } }} /> )}
); }