ultisuite-client/components/gmail/email-list/email-list-row.tsx
R3D347HR4Y c87670e90f
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
feat(api): offline-first mail sync w/ TanStack Query
Move mail, compose, contacts, and accounts off mocks onto REST + WS.
Add client, auth store, IDB-backed query cache, offline queue, and
sync bar; hybrid Zustand for UI-only state. Settings still local until
backend has preferences API.
2026-05-23 00:04:28 +02:00

1622 lines
57 KiB
TypeScript

"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 {
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<string, ReturnType<typeof import("@/lib/resolve-email-calendar-invitation").resolveParsedCalendarInvitation>>
attachmentsById: Map<string, import("@/lib/email-data").EmailAttachment[]>
categoryTabsById: Map<string, import("@/lib/inbox-category-tabs").InboxCategoryTabIcon[]>
}
export type EmailListRowProps = {
email: Email
allEmails: Email[]
emailById: Map<string, Email>
listMailIndex: ListMailIndex
listRowExtras: ListRowExtras
starredEmails: string[]
importantEmails: string[]
readOverrides: Record<string, boolean>
conversationMode: boolean
savedThreadReplyDrafts: Record<string, unknown>
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<string, string>
sidebarNav: {
emailLabelToSidebarFolderId: Record<string, string>
getNavItemPrefs: (id: string) => { messages: string }
labelRows: LabelRowItem[]
folderTree: FolderTreeNode[]
}
rescheduleTarget: {
id: string
value: string
panelOpen: boolean
} | null
setRescheduleTarget: Dispatch<SetStateAction<{
id: string
value: string
panelOpen: boolean
} | null>>
rescheduleDismissTimeoutsRef: RefObject<Map<string, ReturnType<typeof setTimeout>>>
scheduleReschedulePopoverDismiss: (rowId: string) => void
rowContextMenuOpenedAtRef: RefObject<number>
contextMenuTargetIdsRef: RefObject<string[]>
lastSelectionAnchorIdRef: RefObject<string | null>
setSelectedEmails: Dispatch<SetStateAction<string[]>>
setLabelPickerQuery: (q: string) => void
labelPickerQuery: string
catalogLabels: string[]
resolveLabelVisual: (label: string) => ReturnType<typeof import("@/lib/label-picker-visual").resolveLabelPickerVisual>
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<string, boolean>) => Record<string, boolean>) => 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<HTMLDivElement>) => 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<void>
requestDeleteScheduled: (id: string) => void | Promise<void>
requestToggleReadScheduled: (id: string, read: boolean) => void | Promise<void>
requestSnoozeScheduled: (id: string) => void | Promise<void>
requestRescheduleScheduled: (id: string, iso: string) => void | Promise<void>
requestSendScheduledNow: (id: string) => void | Promise<void>
requestSnoozeMailboxEmail: (email: Email) => void | Promise<void>
}
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 (
<ContextMenu
key={email.id}
modal={false}
onOpenChange={(open) => {
if (open) {
rowContextMenuOpenedAtRef.current = Date.now()
setSelectedEmails((prev) => {
const next = contextMenuTargetIdsForRow(
email.id,
prev,
selectedFolder,
allEmails
)
contextMenuTargetIdsRef.current = [...next]
return next
})
} else {
setLabelPickerQuery("")
}
}}
>
<ContextMenuTrigger asChild>
<MailListSwipeRow
enabled={touchListSwipeEnabled}
emailId={email.id}
isOpen={openSwipeRowId === email.id}
onOpenChange={(open) => {
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)}
>
<div
data-email-row-id={email.id}
data-split-active={isSplitActiveRow ? "" : undefined}
aria-current={isSplitActiveRow ? "true" : undefined}
draggable={!isXs}
onDragStart={isXs ? undefined : (e) => 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 */}
<div
className={cn(
"flex w-full min-w-0 flex-col gap-0.5",
!splitView && "md:hidden",
mobileSelectionMode &&
"max-sm:flex-row max-sm:items-center max-sm:gap-2"
)}
>
{mobileSelectionMode && (
<div
className="flex shrink-0 self-center sm:hidden"
onClick={(e) => e.stopPropagation()}
onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)}
>
<Checkbox
className={listRowCheckboxClass(true)}
checked={isSelected}
onCheckedChange={() => {
toggleSelect(email.id)
lastSelectionAnchorIdRef.current = email.id
}}
/>
</div>
)}
<div
className={cn(
"flex min-w-0 flex-1 flex-col gap-0.5",
mobileSelectionMode && "max-sm:pointer-events-none"
)}
data-selectable-text
>
<div className="flex w-full min-w-0 items-center gap-2">
<div
className="hidden shrink-0 items-center sm:flex"
onClick={(e) => e.stopPropagation()}
onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)}
>
<Checkbox
className={listRowCheckboxClass(false)}
checked={isSelected}
onCheckedChange={() => {
toggleSelect(email.id)
lastSelectionAnchorIdRef.current = email.id
}}
/>
</div>
<div className="flex min-w-0 flex-1 items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-1">
<button
type="button"
onClick={(e) => {
e.stopPropagation()
toggleImportant(email.id)
}}
className={cn(
"flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-full",
isSpam
? "text-[#d93025] hover:bg-[#d93025]/10 hover:text-[#b3261e]"
: "text-[#c2c2c2] hover:bg-black/4 hover:text-[#5f6368]"
)}
aria-label={
isSpam
? "Marqué comme spam"
: isImportant
? "Retirer important"
: "Marquer important"
}
>
<Icon
icon={importantSignalIcon(isSpam, isImportant)}
className={cn(
"size-4 shrink-0",
isSpam && "text-[#d93025]",
!isSpam &&
(isImportant ? "text-[#f4cc70]" : "text-[#c2c2c2]")
)}
aria-hidden
/>
</button>
{isScheduled && (
<span
className="flex h-7 w-6 shrink-0 items-center justify-center text-[#5f6368]"
aria-hidden
>
<Send className="size-3.5" strokeWidth={2} />
</span>
)}
{isScheduled ? (
<span
className={cn(
"min-w-0 truncate text-sm",
!isRead ? "font-semibold text-gray-900" : "font-normal text-gray-700"
)}
>
À : {email.scheduledToName ?? email.sender}
</span>
) : (
<ContactHoverCard displayName={email.sender} email={senderHoverEmail}>
<span
className={cn(
"min-w-0 truncate text-sm",
!isRead ? "font-semibold text-gray-900" : "font-normal text-gray-700"
)}
>
{showDraftBadge && (
<span className="font-medium text-[#d93025]">Brouillon </span>
)}
{email.sender}
</span>
</ContactHoverCard>
)}
{threadMessageCount > 1 && (
<span className="shrink-0 text-sm font-normal text-gray-500">
{threadMessageCount}
</span>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
{(parsedInvitation || hasInvitation) && (
<Icon
icon={
parsedInvitation
? VIDEO_CONFERENCE_LOGOS[
parsedInvitation.conferenceProvider
]
: "mdi:calendar"
}
className="size-4 shrink-0 text-[#5f6368]"
aria-label={
parsedInvitation
? "Invitation visioconférence"
: "Invitation calendrier"
}
/>
)}
{attachmentList.length > 0 && (
<Paperclip
className="size-4 shrink-0 text-[#5f6368]"
strokeWidth={1.75}
aria-label="Pièces jointes"
/>
)}
{listRowExtras.categoryTabsById.get(email.id) ? (
<MailInboxCategoryTabIcons
tabs={listRowExtras.categoryTabsById.get(email.id)!}
onTabClick={handleCategoryInboxTabClick}
/>
) : null}
<span
className={cn(
"shrink-0 text-sm font-semibold tabular-nums tracking-tight",
!isRead ? "text-gray-900" : "text-gray-700"
)}
>
{isScheduled ? (
formatScheduledDateTimeDisplay(email.scheduledSendAt)
) : (
<MailDateText iso={email.date} variant="list" />
)}
</span>
</div>
</div>
</div>
<div className={cn("flex min-w-0 flex-wrap items-center gap-1 sm:pl-6")}>
{email.tag && (
<span className="shrink-0 rounded bg-gray-200 px-1.5 py-0.5 text-xs text-gray-600 opacity-[0.92]">
{email.tag}
</span>
)}
<MailLabelPillStrip
variant="list"
labels={email.labels}
labelBgByText={listRowLabelBgByTextLower}
emailLabelToSidebarFolderId={sidebarNav.emailLabelToSidebarFolderId}
getNavItemPrefs={sidebarNav.getNavItemPrefs}
labelRows={sidebarNav.labelRows}
onLabelNavigate={handleNavigateToLabel}
currentFolderId={selectedFolder}
folderTree={sidebarNav.folderTree}
/>
<span
className={cn(
"min-w-0 flex-1 text-sm leading-snug line-clamp-1",
!isRead ? "font-semibold text-gray-900" : "font-semibold text-[#202124]"
)}
>
{email.subject}
</span>
</div>
<div className={cn("flex min-w-0 items-start gap-1.5 sm:pl-6")}>
<p className="min-w-0 flex-1 text-sm leading-snug text-[#5f6368] line-clamp-1">
{email.preview}
</p>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
toggleStar(email.id)
}}
className="mt-0.5 flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-[#c2c2c2] hover:bg-black/4 hover:text-[#5f6368]"
aria-label={isStarred ? "Retirer des favoris" : "Marquer comme favori"}
>
<Star
strokeWidth={isStarred ? 0 : 1.25}
className={cn(
"size-4",
isStarred
? "fill-[#f4cc70] stroke-none text-[#f4cc70]"
: "fill-transparent stroke-[#c2c2c2]"
)}
/>
</button>
</div>
</div>
</div>
{/* Desktop >= md */}
<div
className={cn(
"hidden w-full items-start gap-2",
!splitView && "md:flex"
)}
>
<div className="flex shrink-0 items-center gap-2">
<div
className="flex shrink-0"
onClick={(e) => e.stopPropagation()}
onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)}
>
<Checkbox
className="size-4 min-h-4 min-w-4 shrink-0 rounded-[2.5px] border-[1.5px] border-[#c2c2c2] bg-transparent shadow-none dark:bg-transparent focus-visible:ring-[#c2c2c2]/30 data-[state=checked]:border-[#1a73e8] data-[state=checked]:bg-[#1a73e8] data-[state=checked]:text-white"
checked={isSelected}
onCheckedChange={() => {
toggleSelect(email.id)
lastSelectionAnchorIdRef.current = email.id
}}
/>
</div>
<div className="flex items-center gap-0">
<button
type="button"
onClick={(e) => {
e.stopPropagation()
toggleStar(email.id)
}}
className="flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-[#c2c2c2] hover:bg-black/4 hover:text-[#5f6368]"
aria-label={isStarred ? "Retirer des favoris" : "Marquer comme favori"}
>
<Star
strokeWidth={isStarred ? 0 : 1.25}
className={cn(
"size-4",
isStarred
? "fill-[#f4cc70] stroke-none text-[#f4cc70]"
: "fill-transparent stroke-[#c2c2c2]"
)}
/>
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
toggleImportant(email.id)
}}
className={cn(
"flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-full",
isSpam
? "text-[#d93025] hover:bg-[#d93025]/10 hover:text-[#b3261e]"
: "text-[#c2c2c2] hover:bg-black/4 hover:text-[#5f6368]"
)}
aria-label={
isSpam
? "Marqué comme spam"
: isImportant
? "Retirer important"
: "Marquer important"
}
>
<Icon
icon={importantSignalIcon(isSpam, isImportant)}
className={cn(
"size-4 shrink-0",
isSpam && "text-[#d93025]",
!isSpam &&
(isImportant ? "text-[#f4cc70]" : "text-[#c2c2c2]")
)}
aria-hidden
/>
</button>
{isScheduled && (
<span
className="flex h-7 w-6 shrink-0 items-center justify-center text-[#5f6368]"
aria-hidden
>
<Send className="size-3.5" strokeWidth={2} />
</span>
)}
</div>
</div>
<div
className={cn(
"w-44 shrink-0 truncate pl-2 lg:w-40",
listRowPadTop,
isCompactListRow &&
"flex min-h-7 items-center leading-tight"
)}
data-selectable-text
>
{isScheduled ? (
<span
className={cn(
"text-sm",
!isRead ? "font-semibold text-gray-900" : "text-gray-700"
)}
>
À : {email.scheduledToName ?? email.sender}
</span>
) : (
<ContactHoverCard displayName={email.sender} email={senderHoverEmail}>
<span className={cn(
"text-sm",
!isRead ? "font-semibold text-gray-900" : "text-gray-700"
)}>
{showDraftBadge && (
<span className="font-medium text-[#d93025]">Brouillon </span>
)}
{email.sender}
</span>
</ContactHoverCard>
)}
{threadMessageCount > 1 && (
<span className="text-sm text-gray-500 ml-1">
{threadMessageCount}
</span>
)}
</div>
<div
className="flex min-w-0 flex-1 flex-col justify-start gap-0.5 px-2 pb-0.5"
data-selectable-text
>
<div
className={cn(
"flex min-w-0 items-center gap-1",
listRowPadTop,
isCompactListRow && "leading-tight"
)}
>
{email.tag && (
<span className="shrink-0 rounded bg-gray-200 px-1.5 py-0.5 text-xs text-gray-600 opacity-[0.92]">
{email.tag}
</span>
)}
<MailLabelPillStrip
variant="list"
labels={email.labels}
labelBgByText={listRowLabelBgByTextLower}
emailLabelToSidebarFolderId={sidebarNav.emailLabelToSidebarFolderId}
getNavItemPrefs={sidebarNav.getNavItemPrefs}
labelRows={sidebarNav.labelRows}
onLabelNavigate={handleNavigateToLabel}
currentFolderId={selectedFolder}
folderTree={sidebarNav.folderTree}
/>
<span
className={cn(
"min-w-0 shrink truncate text-sm",
!isRead ? "font-semibold text-gray-900" : "font-normal text-[#3c4043]"
)}
>
{email.subject}
</span>
<span className="min-w-0 flex-1 truncate text-sm text-gray-500">{email.preview}</span>
</div>
{showAttachmentPills && (
<EmailListAttachmentRow emailId={email.id} attachments={attachmentList} />
)}
</div>
<div
className={cn(
"flex shrink-0 flex-col items-end gap-1 pr-2 text-right md:max-w-[150px] md:min-w-0",
listRowPadTop,
isCompactListRow && !showAttachmentPills
? "self-center"
: "self-start"
)}
>
{isScheduled ? (
<div className="relative flex w-full min-w-0 shrink-0 items-center justify-end">
<span
className={cn(
"block max-w-full truncate text-sm font-semibold tabular-nums text-[#c65308]",
"transition-opacity duration-[50ms] ease-out",
isRescheduleOpenThisRow
? "opacity-0"
: "opacity-100 group-hover:opacity-0"
)}
>
{formatScheduledDateTimeDisplay(email.scheduledSendAt)}
</span>
<div
className={cn(
"pointer-events-none absolute right-0 top-1/2 z-[1] flex w-max -translate-y-1/2 flex-nowrap items-center gap-0.5 rounded-md py-0.5 pl-1 opacity-0 transition-opacity duration-[50ms] ease-out",
listRowQuickHoverTrayToneClass(isSelected, isRead),
isRescheduleOpenThisRow
? "pointer-events-auto opacity-100"
: "group-hover:pointer-events-auto group-hover:opacity-100"
)}
>
{!spamRowHoverNoArchive && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Archiver"
onClick={(e) => {
e.stopPropagation()
void requestArchiveScheduled(email.id)
}}
>
<Archive className="h-[18px] w-[18px]" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Archiver
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Supprimer"
onClick={(e) => {
e.stopPropagation()
void requestDeleteScheduled(email.id)
}}
>
<Trash2 className="h-[18px] w-[18px]" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Supprimer
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label={isRead ? "Marquer comme non lu" : "Marquer comme lu"}
onClick={(e) => {
e.stopPropagation()
const next = !isRead
setReadOverrides((prev) => ({ ...prev, [email.id]: next }))
void requestToggleReadScheduled(email.id, next)
}}
>
{isRead ? (
<Mail className="h-[18px] w-[18px]" strokeWidth={1.5} />
) : (
<MailOpen className="h-[18px] w-[18px]" strokeWidth={1.5} />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
{isRead ? "Marquer comme non lu" : "Marquer comme lu"}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Mettre en attente"
onClick={(e) => {
e.stopPropagation()
void requestSnoozeScheduled(email.id)
}}
>
<Clock className="h-[18px] w-[18px]" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Mettre en attente
</TooltipContent>
</Tooltip>
<Popover
open={
rescheduleTarget?.id === email.id &&
rescheduleTarget.panelOpen
}
onOpenChange={(open) => {
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)
}
}}
>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Reprogrammer"
onClick={(e) => e.stopPropagation()}
>
<CalendarClock
className="h-[18px] w-[18px]"
strokeWidth={1.5}
/>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Reprogrammer
</TooltipContent>
</Tooltip>
<PopoverContent
className="w-[min(100vw-2rem,280px)] p-3"
align="end"
side="bottom"
sideOffset={6}
collisionPadding={12}
onClick={(e) => e.stopPropagation()}
>
<p className="mb-2 text-xs font-medium text-[#3c4043]">
Nouvelle date d&apos;envoi
</p>
<input
type="datetime-local"
className="mb-3 w-full rounded border border-[#dadce0] px-2 py-1.5 text-sm text-[#3c4043]"
value={
rescheduleTarget?.id === email.id
? rescheduleTarget.value
: ""
}
onChange={(e) =>
setRescheduleTarget((prev) =>
prev?.id === email.id
? {
...prev,
value: e.target.value,
panelOpen: true,
}
: prev
)
}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
className="h-8 text-xs"
onClick={() => {
setRescheduleTarget((prev) =>
prev?.id === email.id
? { ...prev, panelOpen: false }
: prev
)
scheduleReschedulePopoverDismiss(email.id)
}}
>
Annuler
</Button>
<Button
type="button"
size="sm"
className="h-8 text-xs"
onClick={() => {
if (rescheduleTarget?.id !== email.id) return
const iso = parseDatetimeLocalToIso(
rescheduleTarget.value
)
if (!iso) return
void requestRescheduleScheduled(email.id, iso)
setRescheduleTarget((prev) =>
prev?.id === email.id
? { ...prev, panelOpen: false }
: prev
)
scheduleReschedulePopoverDismiss(email.id)
}}
>
Valider
</Button>
</div>
</PopoverContent>
</Popover>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Modifier le mail"
onClick={(e) => {
e.stopPropagation()
void handleEditScheduledMail(email.id)
}}
>
<Pencil
className="h-[18px] w-[18px]"
strokeWidth={1.5}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Modifier le mail
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Envoyer maintenant"
onClick={(e) => {
e.stopPropagation()
void requestSendScheduledNow(email.id)
}}
>
<Send
className="h-[18px] w-[18px]"
strokeWidth={1.5}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Envoyer maintenant
</TooltipContent>
</Tooltip>
</div>
</div>
) : (
<div className="relative flex w-full min-w-0 shrink-0 items-center justify-end">
<div
className={cn(
"flex min-w-0 max-w-full items-center justify-end gap-1.5 overflow-hidden",
"transition-opacity duration-[50ms] ease-out",
"group-hover:opacity-0"
)}
>
{(parsedInvitation || hasInvitation) && (
<Icon
icon={
parsedInvitation
? VIDEO_CONFERENCE_LOGOS[
parsedInvitation.conferenceProvider
]
: "mdi:calendar"
}
className="size-[18px] shrink-0 text-[#5f6368]"
aria-label={
parsedInvitation
? "Invitation visioconférence"
: "Invitation calendrier"
}
/>
)}
{listRowExtras.categoryTabsById.get(email.id) ? (
<MailInboxCategoryTabIcons
tabs={listRowExtras.categoryTabsById.get(email.id)!}
onTabClick={handleCategoryInboxTabClick}
iconClassName="size-[18px] shrink-0"
/>
) : null}
{showListPaperclip && (
<Paperclip
className="size-[18px] shrink-0 text-[#5f6368]"
strokeWidth={1.75}
aria-label="Pièces jointes"
/>
)}
<span
className={cn(
"min-w-0 truncate text-sm tabular-nums",
!isRead ? "font-semibold text-gray-900" : "text-gray-600"
)}
>
<MailDateText iso={email.date} variant="list" />
</span>
</div>
<div
className={cn(
"pointer-events-none absolute right-0 top-1/2 z-[1] flex w-max -translate-y-1/2 flex-nowrap items-center gap-0.5 rounded-md py-0.5 pl-1 opacity-0 transition-opacity duration-[50ms] ease-out",
listRowQuickHoverTrayToneClass(isSelected, isRead),
"group-hover:pointer-events-auto group-hover:opacity-100"
)}
>
{!spamRowHoverNoArchive && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Archiver"
onClick={(e) => {
e.stopPropagation()
mailActions.hideEmail(email.id)
closeViewIfShowingEmail(email.id)
}}
>
<Archive className="h-[18px] w-[18px]" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Archiver
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Supprimer"
onClick={(e) => {
e.stopPropagation()
mailActions.hideEmail(email.id)
closeViewIfShowingEmail(email.id)
}}
>
<Trash2 className="h-[18px] w-[18px]" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Supprimer
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label={isRead ? "Marquer comme non lu" : "Marquer comme lu"}
onClick={(e) => {
e.stopPropagation()
const next = !isRead
setReadOverrides((prev) => ({ ...prev, [email.id]: next }))
}}
>
{isRead ? (
<Mail className="h-[18px] w-[18px]" strokeWidth={1.5} />
) : (
<MailOpen className="h-[18px] w-[18px]" strokeWidth={1.5} />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
{isRead ? "Marquer comme non lu" : "Marquer comme lu"}
</TooltipContent>
</Tooltip>
{spamRowHoverNoArchive && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Déplacer vers la boîte de réception"
onClick={(e) => {
e.stopPropagation()
mailActions.markNotSpam(email.id)
onSelectFolder?.("inbox")
closeViewIfShowingEmail(email.id)
}}
>
<InboxIcon
className="h-[18px] w-[18px]"
strokeWidth={1.5}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Boîte de réception
</TooltipContent>
</Tooltip>
)}
{!spamRowHoverNoArchive &&
(snoozedFolderRow ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label={
email.id.startsWith("snz-")
? "Déplacer vers la boîte de réception"
: "Remettre dans les mails planifiés"
}
onClick={(e) => {
e.stopPropagation()
restoreSnoozedRowToMailbox(email)
}}
>
<InboxIcon
className="h-[18px] w-[18px]"
strokeWidth={1.5}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
{email.id.startsWith("snz-")
? "Boîte de réception"
: "Planifiés"}
</TooltipContent>
</Tooltip>
) : (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-[#444746] hover:bg-[#f1f3f4]"
aria-label="Mettre en attente"
onClick={(e) => {
e.stopPropagation()
void requestSnoozeMailboxEmail(email)
if (email.labels?.includes("snoozed")) return
mailActions.hideEmail(email.id)
closeViewIfShowingEmail(email.id)
}}
>
<Clock className="h-[18px] w-[18px]" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Mettre en attente
</TooltipContent>
</Tooltip>
))}
</div>
</div>
)}
</div>
</div>
</div>
</MailListSwipeRow>
</ContextMenuTrigger>
<ContextMenuContent
onCloseAutoFocus={(e) => 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 ? (
<>
<ContextMenuItem
onSelect={() => {
const ids = [...contextMenuTargetIdsRef.current]
void Promise.all(
ids.map((id) => requestArchiveScheduled(id))
)
}}
>
<Archive
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
Archiver
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
const ids = [...contextMenuTargetIdsRef.current]
void Promise.all(
ids.map((id) => requestDeleteScheduled(id))
)
}}
>
<Trash2
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
Supprimer
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
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 ? (
<Mail
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
) : (
<MailOpen
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
)}
{scheduledCtxAnyUnread
? "Marquer comme lu"
: "Marquer comme non lu"}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
const ids = [...contextMenuTargetIdsRef.current]
void Promise.all(
ids.map((id) => requestSnoozeScheduled(id))
)
}}
>
<Clock
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
Mettre en attente
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub
onOpenChange={(subOpen) => {
if (!subOpen) return
const ids = contextMenuTargetIdsRef.current
const first = allEmails.find((e) => e.id === ids[0])
setCmScheduledRescheduleValue(
scheduledIsoToDatetimeLocalValue(
first?.scheduledSendAt
)
)
}}
>
<ContextMenuSubTrigger className="[&>svg:last-child]:text-[#5f6368]">
<CalendarClock
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
Reprogrammer
</ContextMenuSubTrigger>
<ContextMenuSubContent
className={cn(
"min-w-[288px] rounded-lg border border-border bg-popover px-4 py-3.5 text-[#3c4043] shadow-lg"
)}
>
<div
className="flex flex-col gap-3.5"
onPointerDown={(e) => e.stopPropagation()}
>
<p className="text-xs font-medium leading-snug text-[#3c4043]">
Nouvelle date d&apos;envoi
{contextTargetIds.length > 1
? ` (${contextTargetIds.length} messages)`
: null}
</p>
<input
type="datetime-local"
className="w-full rounded border border-[#dadce0] px-2.5 py-1.5 text-sm text-[#3c4043]"
value={cmScheduledRescheduleValue}
onChange={(e) =>
setCmScheduledRescheduleValue(e.target.value)
}
onPointerDown={(e) => e.stopPropagation()}
/>
<button
type="button"
className="rounded bg-[#0b57d0] px-3 py-1.5 text-xs font-medium text-white hover:bg-[#0842a0]"
onPointerDown={(e) => {
e.stopPropagation()
e.preventDefault()
}}
onClick={(e) => {
e.stopPropagation()
const iso = parseDatetimeLocalToIso(
cmScheduledRescheduleValue
)
if (!iso) return
const ids = [
...contextMenuTargetIdsRef.current,
]
void Promise.all(
ids.map((id) =>
requestRescheduleScheduled(id, iso)
)
)
}}
>
Valider
</button>
</div>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem
disabled={contextTargetIds.length > 1}
onSelect={() => {
if (contextTargetIds.length !== 1) return
void handleEditScheduledMail(contextTargetIds[0]!)
}}
>
<Pencil
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
Modifier le mail
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
const ids = [...contextMenuTargetIdsRef.current]
void Promise.all(
ids.map((id) => requestSendScheduledNow(id))
)
}}
>
<Send
strokeWidth={1.5}
className="size-[18px] text-[#5f6368]"
/>
Envoyer maintenant
</ContextMenuItem>
</>
) : (
<>
<ContextMenuItem>
<Reply strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Répondre
</ContextMenuItem>
<ContextMenuItem>
<ReplyAll strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Répondre à tous
</ContextMenuItem>
<ContextMenuItem>
<Forward strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Transférer
</ContextMenuItem>
<ContextMenuItem>
<Paperclip strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Transférer en tant que pièce jointe
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>
<Archive strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Archiver
</ContextMenuItem>
<ContextMenuItem>
<Trash2 strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Supprimer
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
const newRead = !isRead
const ids = contextMenuTargetIdsRef.current
setReadOverrides((prev) => {
const next = { ...prev }
for (const id of ids) {
next[id] = newRead
}
return next
})
}}
>
{!isRead ? (
<Mail strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
) : (
<MailOpen strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
)}
{isRead ? "Marquer comme non lu" : "Marquer comme lu"}
</ContextMenuItem>
<ContextMenuItem>
<Clock strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Mettre en attente
</ContextMenuItem>
<ContextMenuItem>
<ListTodo strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Ajouter à Tasks
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger className="[&>svg:last-child]:text-[#5f6368]">
<FolderInput strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Déplacer vers
</ContextMenuSubTrigger>
<ContextMenuSubContent
className={cn(
cn(MAIL_MENU_SURFACE_CLASS, "max-h-80 overflow-y-auto"),
"[&_[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]"
)}
>
<MoveToContextMenuItems
targets={moveTargets}
onMoveTo={(targetId) => {
moveEmailsToTarget(contextTargetIds, targetId)
if (targetId !== "inbox") {
setSelectedEmails((prev) => prev.filter((id) => !contextTargetIds.includes(id)))
}
}}
/>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSub>
<ContextMenuSubTrigger className="[&>svg:last-child]:text-[#5f6368]">
<Tag strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Ajouter le libellé
</ContextMenuSubTrigger>
<ContextMenuSubContent
className={cn(
"z-[100] flex max-h-72 min-w-[260px] flex-col overflow-hidden rounded-lg border border-border bg-popover p-0 py-0 text-[#3c4043] shadow-lg",
"[&_[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]"
)}
>
<EmailLabelPickerBlock
query={labelPickerQuery}
onQueryChange={setLabelPickerQuery}
catalogLabels={catalogLabels}
resolveLabelVisual={resolveLabelVisual}
Item={ContextMenuItem}
getLabelPresence={(lab) =>
getCatalogLabelPresence(contextTargetIds, lab)
}
onToggleCatalogLabel={(lab) =>
toggleLabelOnEmails(contextTargetIds, lab)
}
onCreateLabel={(lab) => {
addLabelToEmails(contextTargetIds, lab)
setLabelPickerQuery("")
}}
/>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem>
<VolumeX strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Ignorer la conversation
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem title={`Recherche : ${senderForSearch}`}>
<Search strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
<span className="min-w-0 truncate">
Rech. e-mails de {senderForSearch}
</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>
<SquareArrowOutUpRight strokeWidth={1.5} className="size-[18px] text-[#5f6368]" />
Ouvrir dans une nouvelle fenêtre
</ContextMenuItem>
</>
)}
</ContextMenuContent>
</ContextMenu>
)
}
export const EmailListRow = memo(EmailListRowInner)