// // Copyright (c) 2025 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { cn, dateFnsLocaleMap, formatBytes } from "@/lib/utils" import { formatDistanceToNow } from "date-fns" import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton" import { EmailEnvelope } from "@/api" import { useMailboxContext } from "../context" import { Checkbox } from "@/components/ui/checkbox" import { MailBulkActions } from "./bulk-actions" import { Badge } from "@/components/ui/badge" import { useTranslation } from 'react-i18next' import { enUS } from "date-fns/locale" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Button } from "@/components/ui/button" interface MailListProps { items: EmailEnvelope[] isLoading: boolean } export function MailList({ items, isLoading, }: MailListProps) { const { t, i18n } = useTranslation() const { currentEnvelope, setCurrentEnvelope, setDeleteIds, setOpen, selected, setSelected } = useMailboxContext() const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS; const handleDelete = (envelope: EmailEnvelope) => { setDeleteIds(new Set([envelope.id])) setOpen("move-to-trash") } const totalSelected = selected.size; const handleToggleAll = () => { const total = selected.size; if (total === items.length && items.length > 0) { setSelected(new Set()); } else { const set = new Set(); for (const item of items) { set.add(item.id); } setSelected(set); } } const hasSelected = (mailId: number) => { return selected.has(mailId); } const toggleSelected = (id: number) => { setSelected(prev => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }); } if (isLoading) { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
) } return (
{items.length > 0 && (
0 ? true : selected.size > 0 ? "indeterminate" : false } onCheckedChange={handleToggleAll} className="h-4 w-4" /> {selected.size > 0 ? `${t('search.bulkActions.selected', { count: selected.size })}` : t('common.selectAll')}
)} {items.map((item, index) => { const hasAttachments = item.attachment_count > 0 const isSelected = currentEnvelope?.id === item.id const isChecked = hasSelected(item.id) return (
{ const target = e.target as HTMLElement if (target.closest('input[type="checkbox"], button')) return setCurrentEnvelope(item); setOpen("display") }} > toggleSelected(item.id)} onClick={(e) => e.stopPropagation()} className="h-4 w-4 shrink-0" />

{item.from}

{item.subject}

{item.subject}

{item.tags?.map((tag, i) => ( {tag} ))}
{hasAttachments && (
{item.attachment_count}
)} {formatBytes(item.size)} {item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true, locale })} e.stopPropagation()} onSelect={(e) => { e.stopPropagation(); setSelected(new Set([item.id])); setOpen("restore"); }} > {t('restore_message.restore_to_imap', 'Restore Mail')} e.stopPropagation()} onSelect={(e) => { e.stopPropagation(); handleDelete(item); }} > {t('common.delete')}
) })} {totalSelected > 0 && }
) }