"use client"; import React, { useCallback } from "react"; import { formatDate } from "@/lib/utils"; import { Email, ThreadGroup } from "@/lib/jmap/types"; import { cn } from "@/lib/utils"; import { Avatar } from "@/components/ui/avatar"; import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward } from "lucide-react"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; import { useUIStore } from "@/stores/ui-store"; import { useEmailStore } from "@/stores/email-store"; import { getThreadColorTag, getEmailColorTag } from "@/lib/thread-utils"; import { useEmailDrag } from "@/hooks/use-email-drag"; import { useLongPress } from "@/hooks/use-long-press"; import { ThreadEmailItem } from "./thread-email-item"; import { EmailHoverActions } from "./email-hover-actions"; import { useTranslations } from "next-intl"; interface ThreadListItemProps { thread: ThreadGroup; isExpanded: boolean; selectedEmailId?: string; isLoading?: boolean; expandedEmails?: Email[]; onToggleExpand: () => void; onEmailSelect: (email: Email) => void; onContextMenu?: (e: React.MouseEvent, email: Email) => void; onOpenConversation?: (thread: ThreadGroup) => void; onToggleStar?: (email: Email) => void; onMarkAsRead?: (email: Email, read: boolean) => void; onDelete?: (email: Email) => void; onArchive?: (email: Email) => void; onSetColorTag?: (emailId: string, color: string | null) => void; onMarkAsSpam?: (email: Email) => void; } interface SingleEmailItemProps { email: Email; selected: boolean; onClick: () => void; onContextMenu?: (e: React.MouseEvent, email: Email) => void; showPreview: boolean; colorTag: string | null; onToggleStar?: () => void; onMarkAsRead?: (read: boolean) => void; onDelete?: () => void; onArchive?: () => void; onSetColorTag?: (color: string | null) => void; onMarkAsSpam?: () => void; } const SingleEmailItem = React.forwardRef( function SingleEmailItem({ email, selected, onClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, ref) { const isUnread = !email.keywords?.$seen; const isStarred = email.keywords?.$flagged; const isAnswered = email.keywords?.$answered; const isForwarded = email.keywords?.$forwarded; const sender = email.from?.[0]; const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore(); const emailKeywords = useSettingsStore((state) => state.emailKeywords); const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); const isChecked = selectedEmailIds.has(email.id); const isFocusedMailLayout = mailLayout === 'focus'; const inlinePreview = showPreview && email.preview ? ` ${email.preview}` : ''; // Resolve color and keyword definition from keyword definitions if not passed directly const tagId = getEmailColorTag(email.keywords); const resolvedKeywordDef = tagId ? emailKeywords.find(k => k.id === tagId) : null; const resolvedColorTag = (() => { if (colorTag) return colorTag; return resolvedKeywordDef ? KEYWORD_PALETTE[resolvedKeywordDef.color]?.bg ?? null : null; })(); const { dragHandlers, isDragging } = useEmailDrag({ email, sourceMailboxId: selectedMailbox, }); const isMobile = useUIStore((state) => state.isMobile); const { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed } = useLongPress( useCallback((pos) => { onContextMenu?.( { preventDefault: () => {}, stopPropagation: () => {}, clientX: pos.clientX, clientY: pos.clientY } as React.MouseEvent, email ); }, [onContextMenu, email]), isMobile ); const longPressHandlers = { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel }; const handleCheckboxClick = (e: React.MouseEvent) => { e.stopPropagation(); toggleEmailSelection(email.id); }; const handleContextMenu = (e: React.MouseEvent) => { onContextMenu?.(e, email); }; const handleClick = (e: React.MouseEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); toggleEmailSelection(email.id); } else if (e.shiftKey) { e.preventDefault(); selectRangeEmails(email.id); } else { if (selectedEmailIds.size > 0) clearSelection(); onClick(); } }; return (
{/* Checkbox - only visible when in selection mode */} {selectedEmailIds.size > 0 && ( )} {isUnread && (
)} {!isFocusedMailLayout && density !== 'extra-compact' && ( )}
{isFocusedMailLayout ? (
{sender?.name || sender?.email || 'Unknown'}
{email.subject || '(no subject)'} {inlinePreview && ( {inlinePreview} )}
{isStarred && } {isAnswered && !isForwarded && } {isForwarded && !isAnswered && } {isAnswered && isForwarded && ( <> )} {email.hasAttachment && } {resolvedKeywordDef && } {formatDate(email.receivedAt)}
) : ( <>
{sender?.name || sender?.email || "Unknown"}
{isStarred && ( )} {isAnswered && !isForwarded && ( )} {isForwarded && !isAnswered && ( )} {isAnswered && isForwarded && ( <> )} {email.hasAttachment && ( )}
{resolvedKeywordDef && ( {resolvedKeywordDef.label} )} {formatDate(email.receivedAt)}
{email.subject || "(no subject)"}
{showPreview && density !== 'extra-compact' && (

{email.preview || "No preview available"}

)} )}
{/* Hover Quick Actions */}
); } ); export const ThreadListItem = React.forwardRef( function ThreadListItem({ thread, isExpanded, selectedEmailId, isLoading = false, expandedEmails, onToggleExpand, onEmailSelect, onContextMenu, onOpenConversation, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam, }, ref) { const t = useTranslations('threads'); const showPreview = useSettingsStore((state) => state.showPreview); const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); const isMobile = useUIStore((state) => state.isMobile); const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread; const isFocusedMailLayout = mailLayout === 'focus'; const inlinePreview = showPreview && latestEmail.preview ? ` ${latestEmail.preview}` : ''; const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore(); const { dragHandlers, isDragging: isThreadDragging } = useEmailDrag({ email: latestEmail, sourceMailboxId: selectedMailbox, threadEmails: thread.emails, }); const { onTouchStart: threadOnTouchStart, onTouchEnd: threadOnTouchEnd, onTouchMove: threadOnTouchMove, onTouchCancel: threadOnTouchCancel, isPressed: isThreadPressed } = useLongPress( useCallback((pos) => { onContextMenu?.( { preventDefault: () => {}, stopPropagation: () => {}, clientX: pos.clientX, clientY: pos.clientY } as React.MouseEvent, latestEmail ); }, [onContextMenu, latestEmail]), isMobile ); const threadLongPressHandlers = { onTouchStart: threadOnTouchStart, onTouchEnd: threadOnTouchEnd, onTouchMove: threadOnTouchMove, onTouchCancel: threadOnTouchCancel }; const threadColor = getThreadColorTag(thread.emails); const emailKeywordDefs = useSettingsStore((state) => state.emailKeywords); const keywordDef = threadColor ? emailKeywordDefs.find(k => k.id === threadColor) : null; const colorTag = keywordDef ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null; const isSelected = selectedEmailId === latestEmail.id || thread.emails.some(e => e.id === selectedEmailId); const isChecked = thread.emails.some(e => selectedEmailIds.has(e.id)); if (emailCount === 1) { return ( onEmailSelect(latestEmail)} onContextMenu={onContextMenu} showPreview={showPreview} colorTag={colorTag} onToggleStar={onToggleStar ? () => onToggleStar(latestEmail) : undefined} onMarkAsRead={onMarkAsRead ? (read) => onMarkAsRead(latestEmail, read) : undefined} onDelete={onDelete ? () => onDelete(latestEmail) : undefined} onArchive={onArchive ? () => onArchive(latestEmail) : undefined} onSetColorTag={onSetColorTag ? (color) => onSetColorTag(latestEmail.id, color) : undefined} onMarkAsSpam={onMarkAsSpam ? () => onMarkAsSpam(latestEmail) : undefined} /> ); } const emailsToShow = expandedEmails || thread.emails; const handleThreadCheckboxClick = (e: React.MouseEvent) => { e.stopPropagation(); // Toggle selection for all emails in this thread const allSelected = thread.emails.every(em => selectedEmailIds.has(em.id)); const newSelection = new Set(selectedEmailIds); thread.emails.forEach(em => { if (allSelected) { newSelection.delete(em.id); } else { newSelection.add(em.id); } }); useEmailStore.setState({ selectedEmailIds: newSelection, lastSelectedEmailId: latestEmail.id }); }; const handleHeaderClick = (e: React.MouseEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); // Ctrl+Click: toggle selection for all thread emails thread.emails.forEach(em => toggleEmailSelection(em.id)); return; } if (e.shiftKey) { e.preventDefault(); selectRangeEmails(latestEmail.id); return; } if (isMobile && onOpenConversation) { onOpenConversation(thread); return; } const target = e.target as HTMLElement; if (target.closest('[data-expand-toggle]')) { onToggleExpand(); } else { if (selectedEmailIds.size > 0) clearSelection(); if (!isExpanded) { onToggleExpand(); } onEmailSelect(latestEmail); } }; const handleContextMenu = (e: React.MouseEvent) => { onContextMenu?.(e, latestEmail); }; return (
{/* Checkbox for thread selection - only visible when in selection mode */} {selectedEmailIds.size > 0 && ( )} {!isMobile && !isFocusedMailLayout && ( )} {hasUnread && (
)} {!isFocusedMailLayout && density !== 'extra-compact' && ( )}
{isFocusedMailLayout ? (
{participantNames.join(', ')} {emailCount}
{latestEmail.subject || '(no subject)'} {inlinePreview && ( {inlinePreview} )}
{hasStarred && } {hasAnswered && !hasForwarded && } {hasForwarded && !hasAnswered && } {hasAnswered && hasForwarded && ( <> )} {hasAttachment && } {keywordDef && } {formatDate(latestEmail.receivedAt)}
) : ( <>
{participantNames.join(", ")} {emailCount}
{hasStarred && ( )} {hasAnswered && !hasForwarded && ( )} {hasForwarded && !hasAnswered && ( )} {hasAnswered && hasForwarded && ( <> )} {hasAttachment && ( )}
{keywordDef && ( {keywordDef.label} )} {formatDate(latestEmail.receivedAt)}
{latestEmail.subject || "(no subject)"}
{showPreview && density !== 'extra-compact' && (

{latestEmail.preview || "No preview available"}

)} )}
{/* Hover Quick Actions for thread header */} onToggleStar(latestEmail) : undefined} onMarkAsRead={onMarkAsRead ? (read) => onMarkAsRead(latestEmail, read) : undefined} onDelete={onDelete ? () => onDelete(latestEmail) : undefined} onArchive={onArchive ? () => onArchive(latestEmail) : undefined} onSetColorTag={onSetColorTag ? (color) => onSetColorTag(latestEmail.id, color) : undefined} onMarkAsSpam={onMarkAsSpam ? () => onMarkAsSpam(latestEmail) : undefined} />
{isExpanded && !isMobile && !isFocusedMailLayout && (
{isLoading ? (
{t('loading')}
) : ( emailsToShow.map((email, index) => ( onEmailSelect(email)} onContextMenu={onContextMenu} /> )) )}
)}
); } );