"use client" import { useCallback, useEffect, useMemo, useRef, useState, } from "react" import { Reply, ReplyAll, Forward } from "lucide-react" import { TooltipProvider, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import { avatarColor, cleanSenderName, senderInitial, } from "@/lib/sender-display" import type { ApiMessageSummary, ApiMessageFull } from "@/lib/api/types" import type { Email, EmailAttachment } from "@/lib/email-data" import type { FolderTreeNode, LabelRowItem } from "@/lib/sidebar-nav-data" import { useMessage, useThread } from "@/lib/api/hooks/use-mail-queries" import { useToggleStar, useMarkRead, useUpdateFlags, useUpdateLabels, } from "@/lib/api/hooks/use-mail-mutations" import { useComposeActions, useComposeDrafts, useComposeWindows, DEFAULT_IDENTITIES, type ThreadComposeKind, type ComposeOpenPreset, savedThreadDraftToComposePreset, } from "@/lib/compose-context" import { buildThreadComposePreset, withTouchFullscreenComposePreset, } from "@/lib/thread-compose-preset" import { openConversationPrint } from "@/lib/print-conversation" import { resolveParsedCalendarInvitation } from "@/lib/resolve-email-calendar-invitation" import { ComposeWindow } from "@/components/gmail/compose-modal" import { CalendarInvitationPreview } from "@/components/gmail/calendar-invitation-preview" import { EmailViewSubjectHeader } from "./email-view/email-view-header" import { MAIL_PREVIEW_SCROLL_CLASS, MAIL_REPLY_BAR_CLASS, MAIL_REPLY_BUTTON_CLASS, } from "@/lib/mail-chrome-classes" import { CollapsedMessage, ExpandedMessage, SpamWhyBanner, } from "@/components/gmail/email-view/email-view-messages" function apiToLegacyEmail( msg: ApiMessageSummary, full?: ApiMessageFull | null, thread?: ApiMessageFull[] | null ): Email { const senderName = msg.from[0]?.name ?? "" return { id: msg.id, sender: senderName, senderEmail: msg.from[0]?.address, subject: msg.subject, preview: msg.snippet, body: full?.body_html ?? full?.body_text, date: msg.date, read: msg.flags.includes("read"), starred: msg.flags.includes("starred"), important: msg.flags.includes("important"), spam: msg.flags.includes("spam") || msg.labels.includes("spam"), labels: msg.labels, hasAttachment: msg.has_attachments, conversation: thread ?.filter((m) => m.id !== msg.id) .map((m) => ({ id: m.id, sender: m.from[0]?.name ?? "", senderEmail: m.from[0]?.address ?? "", date: m.date, body: m.body_html ?? m.body_text ?? "", preview: m.snippet, })), } } interface EmailViewProps { email: ApiMessageSummary onNavigateToLabel?: (label: string) => void showLabelChip?: (label: string) => boolean labelBgByText?: Map emailLabelToSidebarFolderId?: Record getNavItemPrefs?: (id: string) => { messages: string } folderTree?: FolderTreeNode[] labelRows?: readonly LabelRowItem[] currentFolderId?: string isSingleMessageView?: boolean } export function EmailView({ email, onNavigateToLabel, showLabelChip, labelBgByText, emailLabelToSidebarFolderId = {}, getNavItemPrefs = () => ({ messages: "show" }), folderTree, labelRows, currentFolderId, isSingleMessageView = false, }: EmailViewProps) { const { data: fullMessage } = useMessage(email.id) const { data: threadMessages } = useThread(email.thread_id ?? null) const toggleStar = useToggleStar() const markRead = useMarkRead() const updateFlags = useUpdateFlags() const updateLabels = useUpdateLabels() const flags = fullMessage?.flags ?? email.flags const isStarred = flags.includes("starred") const isSpam = flags.includes("spam") || email.labels.includes("spam") const initialFlagsRef = useRef(flags) useEffect(() => { initialFlagsRef.current = email.flags }, [email.id, email.flags]) useEffect(() => { if (!initialFlagsRef.current.includes("read")) { markRead.mutate({ id: email.id, flags: initialFlagsRef.current }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [email.id]) const body = fullMessage?.body_html ?? fullMessage?.body_text ?? `

${email.snippet}

` const [showFullThread, setShowFullThread] = useState(false) const priorMessages = useMemo(() => { if (!threadMessages) return [] return threadMessages.filter((m) => m.id !== email.id) }, [threadMessages, email.id]) const priorCount = priorMessages.length const showRepliesCta = isSingleMessageView && !showFullThread && priorCount > 0 const conversation = isSingleMessageView && !showFullThread ? [] : priorMessages const hasConversation = conversation.length > 0 const [expandedIds, setExpandedIds] = useState>(new Set()) const toggleExpanded = (msgId: string) => { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(msgId)) next.delete(msgId) else next.add(msgId) return next }) } const mainSenderName = cleanSenderName(email.from[0]?.name ?? "") const mainSenderAddr = email.from[0]?.address ?? `${mainSenderName.toLowerCase().replace(/\s+/g, ".")}@example.com` const legacyEmail = useMemo( () => apiToLegacyEmail(email, fullMessage, threadMessages), [email, fullMessage, threadMessages] ) const mainMessageAttachments = useMemo((): EmailAttachment[] => { if (email.has_attachments) return [{ name: "Pièce jointe", kind: "other" }] return [] }, [email.has_attachments]) const { composeWindows } = useComposeWindows() const { savedThreadReplyDrafts } = useComposeDrafts() const { openComposeWithInitial } = useComposeActions() const inlineCompose = useMemo( () => composeWindows.find( (c) => c.placement === "inline" && c.threadEmailId === email.id ), [composeWindows, email.id] ) const hasInlineForThread = Boolean(inlineCompose) const showReplyForwardBar = !inlineCompose const previewScrollRef = useRef(null) const threadComposeAnchorRef = useRef(null) const scrollThreadComposeIntoView = useCallback(() => { requestAnimationFrame(() => { requestAnimationFrame(() => { threadComposeAnchorRef.current?.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest", }) }) }) }, []) const openThreadCompose = useCallback( (preset: ComposeOpenPreset) => { const resolved = withTouchFullscreenComposePreset(preset) openComposeWithInitial(resolved) if (resolved.placement === "inline") { scrollThreadComposeIntoView() } }, [openComposeWithInitial, scrollThreadComposeIntoView] ) const savedThreadDraft = savedThreadReplyDrafts[email.id] useEffect(() => { if (!savedThreadDraft || hasInlineForThread) return openThreadCompose(savedThreadDraftToComposePreset(savedThreadDraft)) }, [email.id, savedThreadDraft, hasInlineForThread, openThreadCompose]) const startThreadCompose = useCallback( (kind: ThreadComposeKind) => { openThreadCompose(buildThreadComposePreset(legacyEmail, kind)) }, [legacyEmail, openThreadCompose] ) const selfIdentity = DEFAULT_IDENTITIES[0] const selfName = cleanSenderName(selfIdentity.name) const calendarInvitation = useMemo( () => resolveParsedCalendarInvitation(legacyEmail), [legacyEmail] ) const handleToggleStar = useCallback(() => { toggleStar.mutate({ id: email.id, flags, starred: isStarred }) }, [email.id, flags, isStarred, toggleStar]) const handleNotSpam = useCallback(() => { if (flags.includes("spam")) { updateFlags.mutate({ id: email.id, flags: flags.filter((f) => f !== "spam"), }) } if (email.labels.includes("spam")) { updateLabels.mutate({ id: email.id, labels: email.labels.filter((l) => l !== "spam"), }) } }, [email.id, flags, email.labels, updateFlags, updateLabels]) const handlePrint = useCallback(() => { openConversationPrint(legacyEmail) }, [legacyEmail]) return (
{calendarInvitation ? ( ) : null} {isSpam && } {showRepliesCta ? (
) : null} {hasConversation && conversation.map((msg) => { const isExpanded = expandedIds.has(msg.id) if (isExpanded) { return (
toggleExpanded(msg.id)} onPrintConversation={handlePrint} />
) } return (
toggleExpanded(msg.id)} />
) })} {showReplyForwardBar ? ( ) : null} {inlineCompose ? (
{senderInitial(selfName)}
) : null}
) }