"use client" import { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, } from "react" import type { Email } from "@/lib/email-data" import { readStateTargets } from "@/lib/mail-thread" import { threadStoreId } from "@/lib/mail-settings/list-row-id" import { resolveOpenEmailView } from "@/lib/mail-settings/resolve-open-email" import { mergeEmailLabelEdits, mergeEmailNotSpam, } from "@/lib/label-edits" import { DEFAULT_INBOX_TAB, } from "@/lib/mail-url" import { mailNavVisitKey, parseMailNavVisitKey, } from "@/lib/mail-folder-display" import { LIST_PAGE_SIZE, escapeHtml, } from "@/components/gmail/email-list/email-list-helpers" import type { Contact } from "@/lib/compose-context" import { buildThreadComposePreset, withTouchFullscreenComposePreset, } from "@/lib/thread-compose-preset" import type { EmailListProps } from "@/components/gmail/email-list/email-list-helpers" import type { EmailListData } from "@/components/gmail/email-list/hooks/use-email-list-data" import type { EmailListLabels } from "@/components/gmail/email-list/hooks/use-email-list-labels" export function useEmailListReading( props: EmailListProps, data: EmailListData, labels: EmailListLabels ) { const { onMailRouteNavigate, onSelectFolder, onXsViewChromeChange, } = props const { openMailId, splitView, isViewMode, showSplitReadingPane, isXs, allEmails, emailById, displayListEmails, listPage, listRowsDep, listViewportRef, conversationMode, labelEdits, notSpamEmailIds, readOverrides, setReadOverrides, markEmailSeen, mailActions, moveTargets, selectedFolder, inboxTab, openComposeWithInitial, } = data const { moveEmailsToTarget } = labels const openEmailView = useMemo(() => { if (!openMailId) return null const resolved = resolveOpenEmailView( openMailId, allEmails, conversationMode ) if (!resolved) return null if (resolved.email.labels?.includes("scheduled")) return null const email = mergeEmailNotSpam( mergeEmailLabelEdits(resolved.email, labelEdits), notSpamEmailIds ) const threadRoot = mergeEmailNotSpam( mergeEmailLabelEdits(resolved.threadRoot, labelEdits), notSpamEmailIds ) return { email, threadRoot, isSingleMessageView: resolved.isSingleMessageView, } }, [openMailId, labelEdits, allEmails, notSpamEmailIds, conversationMode]) const openEmail = openEmailView?.email ?? null const openEmailThreadRoot = openEmailView?.threadRoot ?? null const isSingleMessageView = openEmailView?.isSingleMessageView ?? false const openMailIndex = useMemo( () => openMailId ? displayListEmails.findIndex((e) => e.id === openMailId) : -1, [openMailId, displayListEmails] ) useEffect(() => { if (!openMailId) return const message = emailById.get(openMailId) if (!message) return const targets = readStateTargets(message, conversationMode) for (const id of targets) { markEmailSeen(id) } setReadOverrides((prev) => { let changed = false const next = { ...prev } for (const id of targets) { if (next[id] === undefined) { next[id] = true changed = true } } return changed ? next : prev }) }, [openMailId, markEmailSeen, emailById, conversationMode, setReadOverrides]) const navigateToMail = useCallback( (id: string | null) => { if (id && splitView) { const idx = displayListEmails.findIndex((e) => e.id === id) if (idx >= 0) { const page = Math.floor(idx / LIST_PAGE_SIZE) + 1 onMailRouteNavigate({ mailId: id, page }) return } } onMailRouteNavigate({ mailId: id }) }, [splitView, displayListEmails, onMailRouteNavigate] ) useEffect(() => { if (!openMailId) return const raw = allEmails.find((e) => e.id === openMailId) if (raw?.labels?.includes("scheduled")) { navigateToMail(null) } }, [openMailId, allEmails, navigateToMail]) const pickAdjacentMailId = useCallback( (currentId: string) => { const idx = displayListEmails.findIndex((e) => e.id === currentId) if (idx < 0) return displayListEmails[0]?.id ?? null if (idx < displayListEmails.length - 1) return displayListEmails[idx + 1]!.id if (idx > 0) return displayListEmails[idx - 1]!.id return null }, [displayListEmails] ) const leaveReadingPane = useCallback(() => { if (!splitView) { navigateToMail(null) return } if (!openMailId) return navigateToMail(pickAdjacentMailId(openMailId)) }, [splitView, openMailId, navigateToMail, pickAdjacentMailId]) const goBack = useCallback(() => { if (splitView) leaveReadingPane() else navigateToMail(null) }, [splitView, leaveReadingPane, navigateToMail]) const closeViewIfShowingEmail = useCallback( (emailId: string) => { if (openMailId === emailId) goBack() }, [openMailId, goBack] ) const archiveListRow = useCallback( (email: Email) => { if (email.labels?.includes("scheduled")) { void data.requestArchiveScheduled(email.id) } else { mailActions.hideEmail(email.id) closeViewIfShowingEmail(email.id) } }, [closeViewIfShowingEmail, mailActions, data] ) const deleteListRow = useCallback( (email: Email) => { if (email.labels?.includes("scheduled")) { void data.requestDeleteScheduled(email.id) } else { mailActions.hideEmail(email.id) closeViewIfShowingEmail(email.id) } }, [closeViewIfShowingEmail, mailActions, data] ) const restoreSnoozedRowToMailbox = useCallback( (emailRow: Email) => { void data.requestRestoreSnoozedToInbox(emailRow) if (emailRow.id.startsWith("snz-")) { const baseId = emailRow.id.slice(4) if (baseId.length > 0) mailActions.unhideEmail(baseId) onSelectFolder?.("inbox") } else { onSelectFolder?.("scheduled") } closeViewIfShowingEmail(emailRow.id) }, [ data, mailActions, closeViewIfShowingEmail, onSelectFolder, ] ) const handleCategoryInboxTabClick = useCallback( (tabId: string) => { startTransition(() => { onMailRouteNavigate({ inboxTab: tabId, page: 1, mailId: null, }) }) }, [onMailRouteNavigate] ) const handleBreadcrumbNavigate = useCallback( (visitKey: string) => { if (visitKey === mailNavVisitKey(selectedFolder, inboxTab)) return const { folderId, inboxTab: tab } = parseMailNavVisitKey(visitKey) startTransition(() => { if (folderId === "inbox" && tab && tab !== DEFAULT_INBOX_TAB) { onMailRouteNavigate({ folderId: "inbox", inboxTab: tab, page: 1, mailId: null, }) return } if (onSelectFolder) { onSelectFolder(folderId) return } onMailRouteNavigate({ folderId, inboxTab: DEFAULT_INBOX_TAB, page: 1, mailId: null, }) }) }, [ selectedFolder, inboxTab, onMailRouteNavigate, onSelectFolder, ] ) const goListPrevPage = useCallback(() => { if (listPage <= 1) return onMailRouteNavigate({ page: listPage - 1 }) }, [listPage, onMailRouteNavigate]) const goListNextPage = useCallback(() => { if (listPage >= data.totalPages) return onMailRouteNavigate({ page: listPage + 1 }) }, [listPage, data.totalPages, onMailRouteNavigate]) const goToPrev = useCallback(() => { if (openMailIndex > 0) { const id = displayListEmails[openMailIndex - 1]!.id markEmailSeen(id) setReadOverrides((prev) => ({ ...prev, [id]: true })) navigateToMail(id) } }, [openMailIndex, displayListEmails, navigateToMail, markEmailSeen, setReadOverrides]) const goToNext = useCallback(() => { if (openMailIndex >= 0 && openMailIndex < displayListEmails.length - 1) { const id = displayListEmails[openMailIndex + 1]!.id markEmailSeen(id) setReadOverrides((prev) => ({ ...prev, [id]: true })) navigateToMail(id) } }, [openMailIndex, displayListEmails, navigateToMail, markEmailSeen, setReadOverrides]) const handleOpenEmail = useCallback( (id: string) => { const em = allEmails.find((e) => e.id === id) if (em?.labels?.includes("scheduled")) return markEmailSeen(id) setReadOverrides((prev) => ({ ...prev, [id]: true })) navigateToMail(id) }, [navigateToMail, markEmailSeen, allEmails, setReadOverrides] ) const openDraftInCompose = useCallback( (email: Email) => { markEmailSeen(email.id) setReadOverrides((prev) => ({ ...prev, [email.id]: true })) const to: Contact[] = email.senderEmail ? [{ name: email.sender.trim(), email: email.senderEmail }] : [] const body = email.body ?? (email.preview ? `
${escapeHtml(email.preview)}
` : "") openComposeWithInitial({ to, subject: email.subject, bodyHtml: body, focusToOnMount: false, focusBodyOnMount: true, }) }, [markEmailSeen, openComposeWithInitial, setReadOverrides] ) const handleRowActivate = useCallback( (email: Email) => { if (email.labels?.includes("scheduled")) return if (email.labels?.includes("drafts")) { openDraftInCompose(email) return } handleOpenEmail(email.id) }, [handleOpenEmail, openDraftInCompose] ) const viewModeIsRead = useMemo(() => { if (!openEmail) return true return readOverrides[openEmail.id] !== undefined ? readOverrides[openEmail.id]! : openEmail.read }, [openEmail, readOverrides]) const afterSingleMessageRemoved = useCallback( (removedId: string) => { if (splitView) navigateToMail(pickAdjacentMailId(removedId)) else navigateToMail(null) }, [splitView, navigateToMail, pickAdjacentMailId] ) const singleArchive = useCallback(() => { if (!openMailId) return const id = openMailId mailActions.hideEmail(id) afterSingleMessageRemoved(id) }, [openMailId, afterSingleMessageRemoved, mailActions]) const singleDelete = useCallback(() => { if (!openMailId) return const id = openMailId mailActions.hideEmail(id) afterSingleMessageRemoved(id) }, [openMailId, afterSingleMessageRemoved, mailActions]) const singleSpam = useCallback(() => { if (!openMailId) return const id = openMailId mailActions.hideEmail(id) afterSingleMessageRemoved(id) }, [openMailId, afterSingleMessageRemoved, mailActions]) const singleNotSpam = useCallback(() => { if (!openMailId) return const id = openMailId mailActions.markNotSpam(id) onSelectFolder?.("inbox") afterSingleMessageRemoved(id) }, [openMailId, afterSingleMessageRemoved, onSelectFolder, mailActions]) const singleToggleRead = useCallback(() => { if (!openMailId) return setReadOverrides((prev) => ({ ...prev, [openMailId]: !viewModeIsRead })) }, [openMailId, viewModeIsRead, setReadOverrides]) const singleMoveTo = useCallback( (targetId: string) => { if (!openMailId) return moveEmailsToTarget([openMailId], targetId) const isSystemHide = ["sent", "drafts", "spam", "trash"].includes(targetId) if (isSystemHide || targetId !== "inbox") { afterSingleMessageRemoved(openMailId) } }, [openMailId, afterSingleMessageRemoved, moveEmailsToTarget] ) const singleReply = useCallback(() => { if (!openEmail) return openComposeWithInitial( withTouchFullscreenComposePreset(buildThreadComposePreset(openEmail, "reply")) ) }, [openEmail, openComposeWithInitial]) useEffect(() => { if (!onXsViewChromeChange) return if (!isXs || !isViewMode || !openEmail) { onXsViewChromeChange(null) return } onXsViewChromeChange({ onArchive: singleArchive, onReply: singleReply, moveTargets, onMoveTo: singleMoveTo, }) return () => onXsViewChromeChange(null) }, [ onXsViewChromeChange, isXs, isViewMode, openEmail, singleArchive, singleReply, singleMoveTo, moveTargets, ]) useEffect(() => { if (!splitView) return const firstId = displayListEmails[0]?.id ?? null if (!openMailId) { if (firstId) navigateToMail(firstId) return } const raw = allEmails.find((e) => e.id === openMailId) if (raw?.labels?.includes("scheduled")) { navigateToMail(firstId) return } if (!displayListEmails.some((e) => e.id === openMailId)) { navigateToMail(firstId) } }, [ splitView, selectedFolder, inboxTab, listPage, displayListEmails, openMailId, navigateToMail, allEmails, ]) const handleNavigateToLabel = useCallback( (label: string) => { const folderId = data.sidebarNav.emailLabelToSidebarFolderId[label] ?? label onSelectFolder?.(folderId) }, [onSelectFolder, data.sidebarNav.emailLabelToSidebarFolderId] ) useLayoutEffect(() => { if (!splitView || !openMailId) return const scrollActiveRowIntoView = () => { const root = listViewportRef.current if (!root) return const row = root.querySelector