/** * Mail list component with pagination and virtual scrolling. * Shows list of mails in selected folder with seen/flagged indicators. */ import React, { useRef } from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { useVirtualizer } from '@tanstack/react-virtual'; import type { Mail } from '@/api/mail'; import { decodeMimeHeader } from '@/api/mail'; import { EmptyState } from '@/components/ui/EmptyState'; import { Pagination } from '@/components/ui/Pagination'; import { ChevronUp, Loader2, Paperclip, Star } from 'lucide-react'; import { formatSmartDate } from '@/utils/date'; export interface MailListProps { mails: Mail[]; selectedMailId: string | null; onSelectMail: (mail: Mail) => void; loading: boolean; currentPage: number; total: number; pageSize: number; onPageChange: (page: number) => void; selectedMailIds: Set; onToggleSelect: (mailId: string) => void; onSelectAll: () => void; sortBy?: 'date' | 'from' | 'subject'; sortOrder?: 'asc' | 'desc'; onSortChange?: (sortBy: 'date' | 'from' | 'subject', sortOrder: 'asc' | 'desc') => void; } function formatDate(dateStr: string): string { return formatSmartDate(dateStr) || dateStr; } export function MailList({ mails, selectedMailId, onSelectMail, loading, currentPage, total, pageSize, onPageChange, selectedMailIds, onToggleSelect, onSelectAll, sortBy = 'date', sortOrder = 'desc', onSortChange, }: MailListProps) { const { t } = useTranslation(); const totalPages = Math.ceil(total / pageSize); const allSelected = mails.length > 0 && mails.every((m) => selectedMailIds.has(m.id)); const scrollRef = useRef(null); // Auto-skip virtualization for small datasets const shouldVirtualize = mails.length >= 50; const rowVirtualizer = useVirtualizer({ count: shouldVirtualize ? mails.length : 0, getScrollElement: () => scrollRef.current, estimateSize: () => 80, // approx mail item height overscan: 8, enabled: shouldVirtualize, }); const handleSortFieldChange = (e: React.ChangeEvent) => { const newSortBy = e.target.value as 'date' | 'from' | 'subject'; onSortChange?.(newSortBy, sortOrder); }; const handleSortOrderToggle = () => { const newOrder = sortOrder === 'asc' ? 'desc' : 'asc'; onSortChange?.(sortBy, newOrder); }; if (loading) { return (
); } if (mails.length === 0) { return ( ); } const renderMailItem = (mail: Mail) => (
  • onToggleSelect(mail.id)} className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500" aria-label={t('mail.selectMail')} data-testid={`mail-checkbox-${mail.id}`} />
  • ); return (
    {/* Select-all + Sort header in one row */}
    {selectedMailIds.size > 0 && ( {t('mail.selectedCount', { count: selectedMailIds.size })} )}
    {onSortChange && (
    )}
    {/* Mail list — scrolls, pagination stays fixed */} {shouldVirtualize ? (
    • {rowVirtualizer.getVirtualItems().map((virtualRow) => { const mail = mails[virtualRow.index]; return (
      {renderMailItem(mail)}
      ); })}
    ) : (
      {mails.map((mail) => renderMailItem(mail))}
    )} {totalPages > 1 && (
    )}
    ); }