// // Copyright (c) 2025-2026 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 { dateFnsLocaleMap, formatBytes } from "@/lib/utils" import { format, formatDistanceToNow } from "date-fns" import { MessageSquareText, Paperclip } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton" import { Checkbox } from "@/components/ui/checkbox" import { EmailEnvelope } from "@/api" import { useSearchContext } from "./context" import { MailBulkActions } from "./bulk-actions" import { useTranslation } from 'react-i18next' import { enUS } from "date-fns/locale" import { ColumnDef } from "@tanstack/react-table" import LongText from "@/components/long-text" import { DataTableColumnHeader } from "./table/data-table-column-header" import { SearchTable } from "./table/table" import { DataTableRowActions } from "./table/data-table-row-actions" import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { DataTableToolbar } from "./table/toolbar" import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" import { useSearchMessages } from "@/hooks/use-search-messages" interface MailListProps { items: EmailEnvelope[] isLoading: boolean onEnvelopeChanged: (envelope: EmailEnvelope) => void setSortBy: (sortBy: "DATE" | "SIZE") => void setSortOrder: (value: "desc" | "asc") => void } export function MailListTable({ items, isLoading, onEnvelopeChanged, setSortBy, setSortOrder }: MailListProps) { const { t, i18n } = useTranslation() const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS const { selected, setSelected } = useSearchContext() const columns: ColumnDef[] = [ { accessorKey: "id", header: () => ( 0 ? true : totalSelected > 0 ? "indeterminate" : false } onCheckedChange={handleToggleAll} className="h-4 w-4" /> ), cell: ({ row }) => ( toggleSelected(row.original.account_id, row.original.id)} onClick={(e) => e.stopPropagation()} className="h-4 w-4 shrink-0" /> ), meta: { className: 'text-left text-sm' }, minSize: 25, maxSize: 25, }, { accessorKey: "source", header: t('search.source'), cell: ({ row }) => { const { from, account_email, mailbox_name, account_id, mailbox_id } = row.original; const { setFilter } = useSearchMessages(); const accountPrefix = account_email.split('@')[0]; return (
{ e.stopPropagation(); setFilter((prev: any) => ({ ...prev, from: from })); }} > {from}
{ e.stopPropagation(); setFilter((prev: any) => ({ ...prev, account_ids: [account_id], mailbox_ids: undefined })); }} > {accountPrefix} / { e.stopPropagation(); setFilter((prev: any) => ({ ...prev, account_ids: [account_id], mailbox_ids: [mailbox_id] })); }} > {mailbox_name}
); }, meta: { className: 'text-left text-xs' }, }, { accessorKey: "to", header: t('search.to'), cell: ({ row }) => { const recipients = row.original.to || []; const { setFilter } = useSearchMessages(); return (
{recipients.map((email, index) => ( {index < recipients.length - 1 && ( , )} ))}
); }, meta: { className: 'text-left text-xs' } }, { accessorKey: "subject", header: t('search.subject'), cell: ({ row }) => {row.original.subject}, meta: { className: 'text-left text-xs' } }, { id: "text_preview", header: t('search.preview'), cell: ({ row }) => { const preview = row.original.preview if (!preview) return null return ( {preview} ) }, meta: { className: "text-center text-xs" }, enableSorting: false, minSize: 100, maxSize: 100, }, { id: "attachment_count", header: () => , cell: ({ row }) => {row.original.regular_attachment_count}, meta: { className: 'text-left text-xs' }, minSize: 30, maxSize: 30, }, { accessorKey: 'size', header: ({ column }) => ( ), cell: ({ row }) => {formatBytes(row.original.size)}, meta: { className: 'text-left text-xs' }, minSize: 80, maxSize: 80, }, { accessorKey: 'date', header: ({ column }) => ( ), cell: ({ row }) => { const date = new Date(row.original.date) const title = format(date, 'yyyy-MM-dd HH:mm:ss') return ( {formatDistanceToNow(date, { addSuffix: true, locale })} {title} ) }, meta: { className: 'text-left text-xs' }, minSize: 100, maxSize: 100, }, { id: 'actions', header: t('users.columns.actions'), cell: DataTableRowActions, meta: { className: 'text-right text-xs' }, minSize: 60, maxSize: 60, }, ] const handleToggleAll = () => { const total = Array.from(selected.values()).reduce((sum, set) => sum + set.size, 0) if (total === items.length && items.length > 0) { setSelected(new Map()) } else { setSelected(prev => { const next = new Map(prev) for (const item of items) { const set = new Set(next.get(item.account_id) || []) set.add(item.id) next.set(item.account_id, set) } return next }) } } const toggleSelected = (accountId: number, mailId: string) => { setSelected(prev => { const next = new Map(prev) const set = new Set(next.get(accountId) || []) if (set.has(mailId)) { set.delete(mailId) if (set.size === 0) next.delete(accountId) else next.set(accountId, set) } else { set.add(mailId) next.set(accountId, set) } return next }) } const totalSelected = Array.from(selected.values()).reduce((sum, set) => sum + set.size, 0) const hasSelected = (accountId: number, mailId: string) => selected.get(accountId)?.has(mailId) ?? false if (isLoading) { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
) } return ( <> { const target = e.target as HTMLElement if (target.closest('input[type="checkbox"], button')) return onEnvelopeChanged(row.original) }} setSortBy={setSortBy} setSortOrder={setSortOrder} > {(table) => { return }} {totalSelected > 0 && } ) }