"use client" import { memo, type Dispatch, type MouseEvent, type RefObject, type SetStateAction } from "react" import { Icon } from "@iconify/react" import { Archive, CalendarClock, Clock, FolderInput, Forward, Inbox as InboxIcon, ListTodo, Mail, MailOpen, Paperclip, Pencil, Reply, ReplyAll, Search, Send, ShieldAlert, SquareArrowOutUpRight, Star, Tag, Trash2, VolumeX, } from "lucide-react" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { ContactHoverCard } from "@/components/gmail/contact-hover-card" import { EmailLabelPickerBlock } from "@/components/gmail/email-label-picker-block" import type { CatalogLabelPresence } from "@/components/gmail/email-label-picker-block" import { MailInboxCategoryTabIcons } from "@/components/gmail/mail-inbox-category-tab-icons" import { MailLabelPillStrip, } from "@/components/gmail/mail-label-pills" import { MailListSwipeRow } from "@/components/gmail/mail-list-swipe-row" import { MailDateText } from "@/components/gmail/mail-date-text" import { EmailListAttachmentRow } from "@/components/gmail/email-list/attachments/email-list-attachment-row" import { MoveToContextMenuItems, } from "@/components/gmail/email-list/move-to-menu-items" import type { MoveTarget, MailMoveTargets } from "@/components/gmail/move-to-menu-items" import { cn } from "@/lib/utils" import type { Email } from "@/lib/email-data" import { getThreadMessageCount, isListRowRead, } from "@/lib/mail-thread" import { threadStoreId } from "@/lib/mail-settings/list-row-id" import { resolveSenderEmail } from "@/lib/sender-display" import { VIDEO_CONFERENCE_LOGOS } from "@/lib/calendar-invitation" import { MAIL_MENU_SURFACE_CLASS, MAIL_MENU_SURFACE_WIDE_CLASS, } from "@/lib/mail-chrome-classes" import { readXsMatches } from "@/hooks/use-xs" import type { LabelRowItem, FolderTreeNode } from "@/lib/sidebar-nav-data" import type { LabelEditState } from "@/lib/stores/mail-store" import { contextMenuTargetIdsForRow, formatScheduledDateTimeDisplay, importantSignalIcon, listRowCheckboxClass, listRowQuickHoverTrayToneClass, parseDatetimeLocalToIso, scheduledIsoToDatetimeLocalValue, } from "@/components/gmail/email-list/email-list-helpers" import type { ListMailIndex } from "@/components/gmail/email-list/list-mail-index" type ListRowExtras = { invitationById: Map> attachmentsById: Map categoryTabsById: Map } export type EmailListRowProps = { email: Email allEmails: Email[] emailById: Map listMailIndex: ListMailIndex listRowExtras: ListRowExtras starredEmails: string[] importantEmails: string[] readOverrides: Record conversationMode: boolean savedThreadReplyDrafts: Record selectedEmails: string[] selectedFolder: string splitView: boolean openMailId: string | null isXs: boolean isMd: boolean density: string mobileSelectionMode: boolean touchListSwipeEnabled: boolean openSwipeRowId: string | null setOpenSwipeRowId: (id: string | null) => void listRowLabelBgByTextLower: Map sidebarNav: { emailLabelToSidebarFolderId: Record getNavItemPrefs: (id: string) => { messages: string } labelRows: LabelRowItem[] folderTree: FolderTreeNode[] } rescheduleTarget: { id: string value: string panelOpen: boolean } | null setRescheduleTarget: Dispatch> rescheduleDismissTimeoutsRef: RefObject>> scheduleReschedulePopoverDismiss: (rowId: string) => void rowContextMenuOpenedAtRef: RefObject contextMenuTargetIdsRef: RefObject lastSelectionAnchorIdRef: RefObject setSelectedEmails: Dispatch> setLabelPickerQuery: (q: string) => void labelPickerQuery: string catalogLabels: string[] resolveLabelVisual: (label: string) => ReturnType getCatalogLabelPresence: (ids: string[], catalogLabel: string) => CatalogLabelPresence toggleLabelOnEmails: (ids: string[], label: string) => void addLabelToEmails: (ids: string[], label: string) => void moveTargets: MailMoveTargets moveEmailsToTarget: (emailIds: string[], targetId: string) => void cmScheduledRescheduleValue: string setCmScheduledRescheduleValue: (v: string) => void mailActions: { hideEmail: (id: string) => void markNotSpam: (id: string) => void unhideEmail: (id: string) => void } setReadOverrides: (updater: (prev: Record) => Record) => void onSelectFolder?: (folder: string) => void toggleSelect: (id: string) => void handleRowCheckboxClickCapture: (id: string, e: MouseEvent) => void handleRowActivate: (email: Email) => void startRowDrag: (rowId: string, e: import("react").DragEvent) => void archiveListRow: (email: Email) => void deleteListRow: (email: Email) => void toggleStar: (id: string) => void toggleImportant: (id: string) => void openSwipeRowLabelSheet: (emailId: string) => void handleNavigateToLabel: (label: string) => void handleCategoryInboxTabClick: (tabId: string) => void closeViewIfShowingEmail: (emailId: string) => void restoreSnoozedRowToMailbox: (email: Email) => void handleEditScheduledMail: (id: string) => void requestArchiveScheduled: (id: string) => void | Promise requestDeleteScheduled: (id: string) => void | Promise requestToggleReadScheduled: (id: string, read: boolean) => void | Promise requestSnoozeScheduled: (id: string) => void | Promise requestRescheduleScheduled: (id: string, iso: string) => void | Promise requestSendScheduledNow: (id: string) => void | Promise requestSnoozeMailboxEmail: (email: Email) => void | Promise } function EmailListRowInner(props: EmailListRowProps) { const { email, allEmails, emailById, listMailIndex, listRowExtras, starredEmails, importantEmails, readOverrides, conversationMode, savedThreadReplyDrafts, selectedEmails, selectedFolder, splitView, openMailId, isXs, isMd, density, mobileSelectionMode, touchListSwipeEnabled, openSwipeRowId, setOpenSwipeRowId, listRowLabelBgByTextLower, sidebarNav, rescheduleTarget, setRescheduleTarget, rescheduleDismissTimeoutsRef, scheduleReschedulePopoverDismiss, rowContextMenuOpenedAtRef, contextMenuTargetIdsRef, lastSelectionAnchorIdRef, setSelectedEmails, setLabelPickerQuery, labelPickerQuery, catalogLabels, resolveLabelVisual, getCatalogLabelPresence, toggleLabelOnEmails, addLabelToEmails, moveTargets, moveEmailsToTarget, cmScheduledRescheduleValue, setCmScheduledRescheduleValue, mailActions, setReadOverrides, onSelectFolder, toggleSelect, handleRowCheckboxClickCapture, handleRowActivate, startRowDrag, archiveListRow, deleteListRow, toggleStar, toggleImportant, openSwipeRowLabelSheet, handleNavigateToLabel, handleCategoryInboxTabClick, closeViewIfShowingEmail, restoreSnoozedRowToMailbox, handleEditScheduledMail, requestArchiveScheduled, requestDeleteScheduled, requestToggleReadScheduled, requestSnoozeScheduled, requestRescheduleScheduled, requestSendScheduledNow, requestSnoozeMailboxEmail, } = props const rowThreadId = threadStoreId(email) const isStarred = starredEmails.includes(rowThreadId) || email.starred const isImportant = importantEmails.includes(rowThreadId) || email.important const isSpam = email.spam === true const isDraft = email.labels?.includes("drafts") === true const hasThreadReplyDraft = savedThreadReplyDrafts[rowThreadId] !== undefined const showDraftBadge = isDraft || hasThreadReplyDraft const isRead = isListRowRead( email, readOverrides, emailById, conversationMode ) const senderHoverEmail = resolveSenderEmail(email.sender, email.senderEmail) const threadMessageCount = conversationMode ? getThreadMessageCount(email) : 0 const senderForSearch = email.sender.replace(/\s+/g, " ").trim() const isSelected = selectedEmails.includes(email.id) const isSplitActiveRow = splitView && openMailId === email.id const hasInvitation = email.hasInvitation === true const parsedInvitation = listRowExtras.invitationById.get(email.id) ?? null const attachmentList = listRowExtras.attachmentsById.get(email.id) ?? [] const showAttachmentPills = attachmentList.length > 0 && (!isMd || density === "default") const showListPaperclip = attachmentList.length > 0 && isMd && density !== "default" const isCompactListRow = isMd && density === "compact" const listRowPadTop = !showAttachmentPills ? isCompactListRow ? "pt-0" : "pt-1" : isCompactListRow ? "pt-0" : "pt-0.5" const isScheduled = email.labels?.includes("scheduled") === true const contextTargetIds = contextMenuTargetIdsForRow( email.id, selectedEmails, selectedFolder, allEmails ) const allContextTargetsScheduled = contextTargetIds.length > 0 && contextTargetIds.every((id) => listMailIndex.scheduledIds.has(id) ) const scheduledCtxAnyUnread = allContextTargetsScheduled && contextTargetIds.some((id) => { const em = listMailIndex.emailById.get(id) if (!em) return false return !(readOverrides[id] ?? em.read) }) const isRescheduleOpenThisRow = rescheduleTarget?.id === email.id const spamRowHoverNoArchive = selectedFolder === "spam" const snoozedFolderRow = selectedFolder === "snoozed" return ( { if (open) { rowContextMenuOpenedAtRef.current = Date.now() setSelectedEmails((prev) => { const next = contextMenuTargetIdsForRow( email.id, prev, selectedFolder, allEmails ) contextMenuTargetIdsRef.current = [...next] return next }) } else { setLabelPickerQuery("") } }} > { if (open) setOpenSwipeRowId(email.id) else if (openSwipeRowId === email.id) setOpenSwipeRowId(null) }} onArchive={() => archiveListRow(email)} onDelete={() => deleteListRow(email)} onStar={() => toggleStar(email.id)} onLabel={() => openSwipeRowLabelSheet(email.id)} >
startRowDrag(email.id, e)} onClick={() => { if (readXsMatches() && mobileSelectionMode) { toggleSelect(email.id) lastSelectionAnchorIdRef.current = email.id return } handleRowActivate(email) }} className={cn( "group relative z-0 w-full cursor-pointer pl-3 pr-2 py-2 transition-[background-color,box-shadow] duration-[50ms] ease-out", !splitView && "md:flex md:gap-2 md:px-2 md:py-1.5", !splitView && (isCompactListRow && !showAttachmentPills ? "md:items-center" : "md:items-start"), isCompactListRow && "md:!py-1 md:text-[13px]", isSplitActiveRow ? "z-[1] bg-mail-row-active-split shadow-[inset_3px_0_0_0_#669df6]" : isSelected ? "bg-mail-row-selected" : isRead ? "bg-mail-row-read" : "bg-mail-row-unread", !isSplitActiveRow && "hover:z-1 hover:shadow-[inset_1px_0_0_#d2d5da,inset_-1px_0_0_#d2d5da,0_4px_10px_-3px_rgba(60,64,67,.16),0_2px_5px_0_rgba(60,64,67,.09)]" )} > {/* Compact < md */}
{mobileSelectionMode && (
e.stopPropagation()} onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)} > { toggleSelect(email.id) lastSelectionAnchorIdRef.current = email.id }} />
)}
e.stopPropagation()} onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)} > { toggleSelect(email.id) lastSelectionAnchorIdRef.current = email.id }} />
{isScheduled && ( )} {isScheduled ? ( À : {email.scheduledToName ?? email.sender} ) : ( {showDraftBadge && ( Brouillon )} {email.sender} )} {threadMessageCount > 1 && ( {threadMessageCount} )}
{(parsedInvitation || hasInvitation) && ( )} {attachmentList.length > 0 && ( )} {listRowExtras.categoryTabsById.get(email.id) ? ( ) : null} {isScheduled ? ( formatScheduledDateTimeDisplay(email.scheduledSendAt) ) : ( )}
{email.tag && ( {email.tag} )} {email.subject}

{email.preview}

{/* Desktop >= md */}
e.stopPropagation()} onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)} > { toggleSelect(email.id) lastSelectionAnchorIdRef.current = email.id }} />
{isScheduled && ( )}
{isScheduled ? ( À : {email.scheduledToName ?? email.sender} ) : ( {showDraftBadge && ( Brouillon )} {email.sender} )} {threadMessageCount > 1 && ( {threadMessageCount} )}
{email.tag && ( {email.tag} )} {email.subject} {email.preview}
{showAttachmentPills && ( )}
{isScheduled ? (
{formatScheduledDateTimeDisplay(email.scheduledSendAt)}
{!spamRowHoverNoArchive && ( Archiver )} Supprimer {isRead ? "Marquer comme non lu" : "Marquer comme lu"} Mettre en attente { if (open) { const pending = rescheduleDismissTimeoutsRef.current.get( email.id ) if (pending) { clearTimeout(pending) rescheduleDismissTimeoutsRef.current.delete( email.id ) } setRescheduleTarget({ id: email.id, value: scheduledIsoToDatetimeLocalValue( email.scheduledSendAt ), panelOpen: true, }) } else { setRescheduleTarget((prev) => prev?.id === email.id ? { ...prev, panelOpen: false } : prev ) scheduleReschedulePopoverDismiss(email.id) } }} > Reprogrammer e.stopPropagation()} >

Nouvelle date d'envoi

setRescheduleTarget((prev) => prev?.id === email.id ? { ...prev, value: e.target.value, panelOpen: true, } : prev ) } />
Modifier le mail Envoyer maintenant
) : (
{(parsedInvitation || hasInvitation) && ( )} {listRowExtras.categoryTabsById.get(email.id) ? ( ) : null} {showListPaperclip && ( )}
{!spamRowHoverNoArchive && ( Archiver )} Supprimer {isRead ? "Marquer comme non lu" : "Marquer comme lu"} {spamRowHoverNoArchive && ( Boîte de réception )} {!spamRowHoverNoArchive && (snoozedFolderRow ? ( {email.id.startsWith("snz-") ? "Boîte de réception" : "Planifiés"} ) : ( Mettre en attente ))}
)}
e.preventDefault()} onPointerDownOutside={(event) => { const native = event.detail.originalEvent if ( native.pointerType === "mouse" && native.button === 2 && Date.now() - rowContextMenuOpenedAtRef.current < 450 ) { event.preventDefault() } }} className={cn( cn(MAIL_MENU_SURFACE_WIDE_CLASS, "overflow-visible"), "[&_[data-slot=context-menu-item]]:gap-3 [&_[data-slot=context-menu-item]]:rounded-none [&_[data-slot=context-menu-item]]:px-3 [&_[data-slot=context-menu-item]]:py-2 [&_[data-slot=context-menu-item]]:text-sm", "[&_[data-slot=context-menu-item]:focus]:bg-[#f1f3f4] [&_[data-slot=context-menu-item]:focus]:text-[#3c4043]", "[&_[data-slot=context-menu-sub-trigger]]:gap-3 [&_[data-slot=context-menu-sub-trigger]]:rounded-none [&_[data-slot=context-menu-sub-trigger]]:px-3 [&_[data-slot=context-menu-sub-trigger]]:py-2 [&_[data-slot=context-menu-sub-trigger]]:text-sm", "[&_[data-slot=context-menu-sub-trigger]:focus]:bg-[#f1f3f4] [&_[data-slot=context-menu-sub-trigger]:focus]:text-[#3c4043]", "[&_[data-slot=context-menu-separator]]:mx-0 [&_[data-slot=context-menu-separator]]:my-1 [&_[data-slot=context-menu-separator]]:h-px [&_[data-slot=context-menu-separator]]:bg-[#eceff1]", "[&_[data-slot=context-menu-sub-content]]:min-w-[200px] [&_[data-slot=context-menu-sub-content]]:rounded-lg [&_[data-slot=context-menu-sub-content]]:border [&_[data-slot=context-menu-sub-content]]:border-border [&_[data-slot=context-menu-sub-content]]:bg-popover [&_[data-slot=context-menu-sub-content]]:shadow-lg" )} > {allContextTargetsScheduled ? ( <> { const ids = [...contextMenuTargetIdsRef.current] void Promise.all( ids.map((id) => requestArchiveScheduled(id)) ) }} > Archiver { const ids = [...contextMenuTargetIdsRef.current] void Promise.all( ids.map((id) => requestDeleteScheduled(id)) ) }} > Supprimer { const ids = [...contextMenuTargetIdsRef.current] const markRead = scheduledCtxAnyUnread setReadOverrides((prev) => { const next = { ...prev } for (const id of ids) next[id] = markRead return next }) void Promise.all( ids.map((id) => requestToggleReadScheduled(id, markRead) ) ) }} > {scheduledCtxAnyUnread ? ( ) : ( )} {scheduledCtxAnyUnread ? "Marquer comme lu" : "Marquer comme non lu"} { const ids = [...contextMenuTargetIdsRef.current] void Promise.all( ids.map((id) => requestSnoozeScheduled(id)) ) }} > Mettre en attente { if (!subOpen) return const ids = contextMenuTargetIdsRef.current const first = allEmails.find((e) => e.id === ids[0]) setCmScheduledRescheduleValue( scheduledIsoToDatetimeLocalValue( first?.scheduledSendAt ) ) }} > Reprogrammer
e.stopPropagation()} >

Nouvelle date d'envoi {contextTargetIds.length > 1 ? ` (${contextTargetIds.length} messages)` : null}

setCmScheduledRescheduleValue(e.target.value) } onPointerDown={(e) => e.stopPropagation()} />
1} onSelect={() => { if (contextTargetIds.length !== 1) return void handleEditScheduledMail(contextTargetIds[0]!) }} > Modifier le mail { const ids = [...contextMenuTargetIdsRef.current] void Promise.all( ids.map((id) => requestSendScheduledNow(id)) ) }} > Envoyer maintenant ) : ( <> Répondre Répondre à tous Transférer Transférer en tant que pièce jointe Archiver Supprimer { const newRead = !isRead const ids = contextMenuTargetIdsRef.current setReadOverrides((prev) => { const next = { ...prev } for (const id of ids) { next[id] = newRead } return next }) }} > {!isRead ? ( ) : ( )} {isRead ? "Marquer comme non lu" : "Marquer comme lu"} Mettre en attente Ajouter à Tasks Déplacer vers { moveEmailsToTarget(contextTargetIds, targetId) if (targetId !== "inbox") { setSelectedEmails((prev) => prev.filter((id) => !contextTargetIds.includes(id))) } }} /> Ajouter le libellé getCatalogLabelPresence(contextTargetIds, lab) } onToggleCatalogLabel={(lab) => toggleLabelOnEmails(contextTargetIds, lab) } onCreateLabel={(lab) => { addLabelToEmails(contextTargetIds, lab) setLabelPickerQuery("") }} /> Ignorer la conversation Rech. e-mails de {senderForSearch} Ouvrir dans une nouvelle fenêtre )}
) } export const EmailListRow = memo(EmailListRowInner)