// // 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 { useState } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { ChevronDown, ChevronUp, Loader2, MessageSquareText } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { get_thread_messages } from '@/api/mailbox/envelope/api'; import { MailMessageView } from './mail-message-view'; import { useSearchContext } from './context'; import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; interface MailThreadDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export function MailThreadDialog({ open, onOpenChange }: MailThreadDialogProps) { const { currentEnvelope } = useSearchContext(); const [expandedIds, setExpandedIds] = useState>(new Set()); const { t } = useTranslation(); const threadId = currentEnvelope?.thread_id; const accountId = currentEnvelope?.account_id; const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error, } = useInfiniteQuery({ queryKey: ['thread', accountId, threadId], queryFn: ({ pageParam = 1 }) => get_thread_messages(accountId!, threadId!, pageParam, 10), getNextPageParam: (lastPage) => lastPage.current_page && lastPage.total_pages ? lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined : undefined, enabled: open && !!accountId && !!threadId, initialPageParam: 1, }); const allMessages = data?.pages.flatMap((page) => page.items) ?? []; const totalCount = data?.pages[0]?.total_items ?? 0; const toggleExpand = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }; return ( {/* Header */}
{t('search.thread.title', { count: totalCount })}
{/* Body */}
{isLoading && } {isError && (
{t('search.thread.error')}: {(error as Error)?.message}
)} {!isLoading && allMessages.length === 0 && (
{t('search.thread.empty')}
)} {allMessages .sort((a, b) => a.date - b.date) .map((msg) => { const isExpanded = expandedIds.has(msg.id); const preview = msg.preview; const date = new Date(msg.date); const formattedDate = isNaN(date.getTime()) ? t('search.thread.invalidDate') : format(date, 'yyyy-MM-dd HH:mm:ss'); return ( toggleExpand(msg.id)} >
{msg.from} {msg.to.join(', ')}

{msg.subject || t('search.thread.noSubject')}

{!isExpanded && preview && (

{preview}

)}
{formattedDate} {isExpanded ? ( ) : ( )}
{isExpanded && (
)}
); })} {hasNextPage && (
)}
); } // Skeleton function ThreadSkeleton() { return (
{[...Array(3)].map((_, i) => ( ))}
); }