"use client"; import React, { useCallback } from "react"; import { formatDate, formatDateTime, stripInvisibleLeading } 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, CalendarClock } from "lucide-react"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; import { useUIStore } from "@/stores/ui-store"; import { useEmailStore } from "@/stores/email-store"; import { useAccountStore } from "@/stores/account-store"; import { getThreadColorTag, getEmailColorTags } 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; onEmailDoubleClick?: (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; onDoubleClick?: () => 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, onDoubleClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, ref) { const t = useTranslations('email_viewer'); const isUnread = !email.keywords?.$seen; const isStarred = email.keywords?.$flagged; const isAnswered = email.keywords?.$answered; const isForwarded = email.keywords?.$forwarded; const { selectedMailbox, mailboxes, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore(); // In Sent/Drafts folders, show recipient instead of sender (which is always "me") const currentMailboxRole = mailboxes.find(mb => mb.id === selectedMailbox)?.role; const showRecipient = currentMailboxRole === 'sent' || currentMailboxRole === 'drafts'; const sender = showRecipient ? (email.to?.[0] ?? email.from?.[0]) : email.from?.[0]; const emailKeywords = useSettingsStore((state) => state.emailKeywords); const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); const timeFormat = useSettingsStore((state) => state.timeFormat); const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk); const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk; const isUnifiedView = useEmailStore((state) => state.isUnifiedView); const getAccountById = useAccountStore((state) => state.getAccountById); const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined; const isChecked = selectedEmailIds.has(email.id); const isMobile = useUIStore((state) => state.isMobile); // The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile. const isFocusedMailLayout = mailLayout === 'focus' && !isMobile; const trimmedPreview = stripInvisibleLeading(email.preview ?? ''); const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : ''; const scheduledSendLabel = email.isScheduled && email.scheduledSendAt ? formatDateTime(email.scheduledSendAt, timeFormat) : null; // Resolve color tags using keyword definitions; unknown tags fall back to gray const tagIds = getEmailColorTags(email.keywords); const resolvedKeywordDefs = tagIds.map(id => emailKeywords.find(k => k.id === id) ?? { id, label: id, color: 'gray' }); const resolvedKeywordDef = resolvedKeywordDefs[0] ?? null; const resolvedColorTag = (() => { if (colorTag) return colorTag; return resolvedKeywordDef ? KEYWORD_PALETTE[resolvedKeywordDef.color]?.bg ?? null : null; })(); const { dragHandlers, isDragging } = useEmailDrag({ email, sourceMailboxId: selectedMailbox, }); 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 (
{ if (e.ctrlKey || e.metaKey || e.shiftKey) return; if (!onDoubleClick) return; e.preventDefault(); onDoubleClick(); }} onContextMenu={handleContextMenu} style={{ minHeight: isFocusedMailLayout ? undefined : 'var(--list-item-height)' }} >
{/* Checkbox - only visible when in selection mode */} {selectedEmailIds.size > 0 && ( )} {isUnread && (
)} {density !== 'extra-compact' && ( )}
{isFocusedMailLayout ? (
{isUnifiedView && email.accountId && accountColor && ( )} {sender?.name || sender?.email || 'Unknown'}
{email.subject || '(no subject)'} {inlinePreview && ( {inlinePreview} )}
{isStarred && } {isAnswered && !isForwarded && } {isForwarded && !isAnswered && } {isAnswered && isForwarded && ( <> )} {email.hasAttachment && } {resolvedKeywordDefs.map((kd) => ( ))} {scheduledSendLabel ? ( {scheduledSendLabel} ) : ( {formatDate(email.receivedAt)} )}
) : ( <>
{isUnifiedView && email.accountId && accountColor && ( )} {sender?.name || sender?.email || "Unknown"}
{isStarred && ( )} {isAnswered && !isForwarded && ( )} {isForwarded && !isAnswered && ( )} {isAnswered && isForwarded && ( <> )} {email.hasAttachment && ( )}
{resolvedKeywordDefs.map((kd) => ( {kd.label} ))} {scheduledSendLabel ? ( {scheduledSendLabel} ) : ( {formatDate(email.receivedAt)} )}
{email.subject || "(no subject)"}
{showPreview && density !== 'extra-compact' && density !== 'compact' && (

{trimmedPreview || t('no_preview_available')}

)} )}
{/* Hover Quick Actions */} {!email.isScheduled && ( )}
); } ); export const ThreadListItem = React.forwardRef( function ThreadListItem({ thread, isExpanded, selectedEmailId, isLoading = false, expandedEmails, onToggleExpand, onEmailSelect, onEmailDoubleClick, onContextMenu, onOpenConversation, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam, }, ref) { const t = useTranslations('threads'); const tEmailViewer = useTranslations('email_viewer'); const showPreview = useSettingsStore((state) => state.showPreview); const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); const timeFormat = useSettingsStore((state) => state.timeFormat); const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk); const isMobile = useUIStore((state) => state.isMobile); const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread; // The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile. const isFocusedMailLayout = mailLayout === 'focus' && !isMobile; const trimmedPreview = stripInvisibleLeading(latestEmail.preview ?? ''); const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : ''; const scheduledSendLabel = latestEmail.isScheduled && latestEmail.scheduledSendAt ? formatDateTime(latestEmail.scheduledSendAt, timeFormat) : null; const { selectedMailbox, mailboxes, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection, isUnifiedView } = useEmailStore(); const getAccountById = useAccountStore((state) => state.getAccountById); const threadAccountColor = latestEmail.accountId ? getAccountById(latestEmail.accountId)?.avatarColor : undefined; // In Sent/Drafts folders, show recipient instead of sender (which is always "me") const currentMailboxRole = mailboxes.find(mb => mb.id === selectedMailbox)?.role; const showRecipient = currentMailboxRole === 'sent' || currentMailboxRole === 'drafts'; const displayNames = showRecipient ? Array.from(new Set( thread.emails.flatMap(e => (e.to ?? []).map(r => r.name || r.email.split('@')[0])) )).slice(0, 4) : participantNames; const avatarPerson = showRecipient ? latestEmail.to?.[0] : latestEmail.from?.[0]; const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk; 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) ?? { id: threadColor, label: threadColor, color: 'gray' }) : 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)} onDoubleClick={onEmailDoubleClick ? () => onEmailDoubleClick(latestEmail) : undefined} 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 (
{ if (e.ctrlKey || e.metaKey || e.shiftKey) return; if (!onEmailDoubleClick) return; e.preventDefault(); onEmailDoubleClick(latestEmail); }} onContextMenu={handleContextMenu} style={{ minHeight: isFocusedMailLayout ? undefined : 'var(--list-item-height)' }} >
{/* Checkbox for thread selection - only visible when in selection mode */} {selectedEmailIds.size > 0 && ( )} {!isMobile && !isFocusedMailLayout && ( )} {hasUnread && (
)} {density !== 'extra-compact' && ( )}
{isFocusedMailLayout ? (
{isUnifiedView && latestEmail.accountId && threadAccountColor && ( )} {displayNames.join(', ')} {emailCount}
{latestEmail.subject || '(no subject)'} {inlinePreview && ( {inlinePreview} )}
{hasStarred && } {hasAnswered && !hasForwarded && } {hasForwarded && !hasAnswered && } {hasAnswered && hasForwarded && ( <> )} {hasAttachment && } {keywordDef && ( )} {scheduledSendLabel ? ( {scheduledSendLabel} ) : ( {formatDate(latestEmail.receivedAt)} )}
) : ( <>
{isUnifiedView && latestEmail.accountId && threadAccountColor && ( )} {displayNames.join(", ")} {emailCount}
{hasStarred && ( )} {hasAnswered && !hasForwarded && ( )} {hasForwarded && !hasAnswered && ( )} {hasAnswered && hasForwarded && ( <> )} {hasAttachment && ( )}
{keywordDef && ( {keywordDef.label} )} {scheduledSendLabel ? ( {scheduledSendLabel} ) : ( {formatDate(latestEmail.receivedAt)} )}
{latestEmail.subject || "(no subject)"}
{showPreview && density !== 'extra-compact' && density !== 'compact' && (

{trimmedPreview || tEmailViewer('no_preview_available')}

)} )}
{/* Hover Quick Actions for thread header */} {!latestEmail.isScheduled && ( 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)} onDoubleClick={onEmailDoubleClick ? () => onEmailDoubleClick(email) : undefined} onContextMenu={onContextMenu} /> )) )}
)}
); } );