ultisuite-client/components/gmail/email-list/hooks/use-email-list-data.ts
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

825 lines
26 KiB
TypeScript

"use client"
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import { useSearchParams, useRouter } from "next/navigation"
import { useQueryClient } from "@tanstack/react-query"
import { buildLabelTextToNavColorClass } from "@/components/gmail/mail-label-pills"
import { useMessages, useMailSearch } from "@/lib/api/hooks/use-mail-queries"
import {
useUpdateFlags,
useUpdateLabels,
useDeleteMessage,
} from "@/lib/api/hooks/use-mail-mutations"
import type { ApiMessageSummary, PaginatedResponse } from "@/lib/api/types"
import type { Email, EmailAttachment } from "@/lib/email-data"
import {
isThreadHeadMessage,
} from "@/lib/mail-thread"
import { useScheduledMail } from "@/lib/scheduled-mail-context"
import { useMailStore } from "@/lib/stores/mail-store"
import { useScheduledStore } from "@/lib/stores/scheduled-store"
import { usePersistHydrated } from "@/hooks/use-persist-hydrated"
import { useIsMd } from "@/hooks/use-md-breakpoint"
import { sortEmailsForInbox } from "@/lib/mail-settings/sort-emails"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
import { useActiveAccount } from "@/lib/stores/account-store"
import { useMailSearchStore } from "@/lib/stores/mail-search-store"
import type { MailNavFolderMaps } from "@/lib/mail-folder-filter"
import {
getMailNavFolderLabel,
inboxTabDisplayLabel,
} from "@/lib/sidebar-nav-data"
import { buildInboxCategoryTabIcons } from "@/lib/inbox-category-tabs"
import {
INBOX_ALL_TAB,
SEARCH_FOLDER_ID,
inboxTabShowsInactiveMeta,
normalizeInboxTabSegment,
} from "@/lib/mail-url"
import {
parseSearchParams,
buildSearchUrl,
type SearchParams,
} from "@/lib/mail-search/search-params"
import { useSidebarNav, registerNavEmailSync } from "@/lib/sidebar-nav-context"
import { useMoveTargets } from "@/components/gmail/move-to-menu-items"
import { buildListMailIndex } from "@/components/gmail/email-list/list-mail-index"
import {
useComposeActions,
useComposeDrafts,
} from "@/lib/compose-context"
import {
LIST_PAGE_SIZE,
type EmailListProps,
buildInboxTabBarItems,
} from "@/components/gmail/email-list/email-list-helpers"
import { useMailListPullRefresh } from "@/hooks/use-mail-list-pull-refresh"
import { ensureVcLogosCollection } from "@/lib/register-vc-logos"
import { attachmentsForEmailList } from "@/lib/attachment-display"
import { resolveParsedCalendarInvitation } from "@/lib/resolve-email-calendar-invitation"
import { resolveEmailInboxCategoryTabs } from "@/lib/inbox-category-tabs"
import { cleanSenderName } from "@/lib/sender-display"
import { threadStoreId } from "@/lib/mail-settings/list-row-id"
import { useIsXs } from "@/hooks/use-xs"
import { useTouchNav } from "@/hooks/use-touch-nav"
import type { MessageSearchFilter } from "@/lib/api/types"
function apiMessageToEmail(msg: ApiMessageSummary): Email {
const sender = msg.from[0]?.name || msg.from[0]?.address || ""
const senderEmail = msg.from[0]?.address || ""
return {
id: msg.id,
sender,
senderEmail,
subject: msg.subject,
preview: msg.snippet,
date: msg.date,
read: msg.flags.includes("read"),
starred: msg.flags.includes("starred"),
important: msg.flags.includes("important"),
spam: msg.labels.includes("spam"),
hasAttachment: msg.has_attachments,
labels: msg.labels,
threadHeadId: msg.thread_id ?? msg.id,
threadMessageIds: [msg.id],
isThreadHead: true,
}
}
export function useEmailListData({
selectedFolder,
inboxTab,
listPage,
openMailId,
splitView = false,
onMailRouteNavigate,
onFolderUnreadCountsChange,
}: EmailListProps) {
const isViewMode = openMailId !== null && !splitView
const showSplitReadingPane = splitView && openMailId !== null
const isSearchMode = selectedFolder === SEARCH_FOLDER_ID
const searchRouter = useRouter()
const searchAccount = useActiveAccount()
const setAdvancedOpen = useMailSearchStore((s) => s.setAdvancedOpen)
const urlSearchParams = useSearchParams()
const searchParams = useMemo(
() => (isSearchMode ? parseSearchParams(urlSearchParams) : null),
[isSearchMode, urlSearchParams]
)
const setSearchFilter = useCallback(
(patch: Partial<SearchParams>) => {
if (!searchParams) return
searchRouter.push(buildSearchUrl({ ...searchParams, ...patch }))
},
[searchParams, searchRouter]
)
const toggleSearchFilter = useCallback(
(key: keyof SearchParams, value: string) => {
if (!searchParams) return
const next = { ...searchParams }
if (key === "has") {
const arr = [...next.has]
if (arr.includes(value)) next.has = arr.filter((v) => v !== value)
else next.has = [...arr, value]
} else if (key === "excludeChats") {
next.excludeChats = !next.excludeChats
} else {
const cur = (next as Record<string, unknown>)[key]
;(next as Record<string, unknown>)[key] = cur === value ? "" : value
}
searchRouter.push(buildSearchUrl(next))
},
[searchParams, searchRouter]
)
const { savedThreadReplyDrafts } = useComposeDrafts()
const {
openCompose,
openComposeWithInitial,
closeAllInlineComposes,
pruneInlineComposesToOpenThread,
} = useComposeActions()
const {
scheduledEmails,
snoozedEmails,
requestDeleteScheduled,
requestArchiveScheduled,
requestSnoozeScheduled,
requestToggleReadScheduled,
requestRescheduleScheduled,
requestGetScheduledEditPayload,
requestSendScheduledNow,
requestSnoozeMailboxEmail,
requestRestoreSnoozedToInbox,
} = useScheduledMail()
const scheduledPersistHydrated = usePersistHydrated(useScheduledStore)
const accountId = searchAccount?.id
const queryClient = useQueryClient()
const effectiveApiFolder = useMemo(() => {
if (isSearchMode) return "__search__"
if (selectedFolder === "scheduled" || selectedFolder === "snoozed") return "__local__"
if (selectedFolder !== "inbox") return selectedFolder
const tab = normalizeInboxTabSegment(inboxTab)
if (tab === INBOX_ALL_TAB) return "inbox"
return tab
}, [selectedFolder, inboxTab, isSearchMode])
const searchFilter = useMemo<MessageSearchFilter | null>(() => {
if (!isSearchMode || !searchParams) return null
return {
q: searchParams.q || undefined,
from: searchParams.from || undefined,
label: searchParams.in !== "all" ? searchParams.in : undefined,
account_id: accountId,
date_from: searchParams.after || undefined,
date_to: searchParams.before || undefined,
has_attachment: searchParams.has.includes("attachment") ? true : undefined,
}
}, [isSearchMode, searchParams, accountId])
const messagesQuery = useMessages(
effectiveApiFolder === "__search__" || effectiveApiFolder === "__local__"
? "inbox"
: effectiveApiFolder,
accountId,
listPage
)
const searchQuery = useMailSearch(searchFilter)
const updateFlags = useUpdateFlags()
const updateLabels = useUpdateLabels()
const deleteMessage = useDeleteMessage()
const apiMessages: ApiMessageSummary[] = useMemo(() => {
if (isSearchMode) return searchQuery.data?.data ?? []
if (effectiveApiFolder === "__local__") return []
return messagesQuery.data?.data ?? []
}, [isSearchMode, effectiveApiFolder, searchQuery.data, messagesQuery.data])
const apiEmails: Email[] = useMemo(
() => apiMessages.map(apiMessageToEmail),
[apiMessages]
)
const apiMessagesById = useMemo(
() => new Map(apiMessages.map((m) => [m.id, m])),
[apiMessages]
)
const allEmails = useMemo(() => {
if (selectedFolder === "scheduled" && scheduledPersistHydrated) {
return scheduledEmails.map<Email>((entry) => ({
id: entry.id,
sender: entry.to[0]?.name ?? "Destinataire",
senderEmail: entry.to[0]?.address,
subject: entry.subject || "(Sans objet)",
preview: "",
body: "",
date: entry.scheduled_at ?? entry.created_at,
read: true,
starred: false,
important: false,
labels: ["scheduled"],
scheduledSendAt: entry.scheduled_at,
scheduledToName: entry.to[0]?.name,
}))
}
if (selectedFolder === "snoozed" && scheduledPersistHydrated) {
return snoozedEmails
}
return apiEmails
}, [
selectedFolder,
scheduledPersistHydrated,
scheduledEmails,
snoozedEmails,
apiEmails,
])
const emailById = useMemo(
() => new Map(allEmails.map((e) => [e.id, e])),
[allEmails]
)
const isLoading = isSearchMode ? searchQuery.isLoading : messagesQuery.isLoading
const error = isSearchMode ? searchQuery.error : messagesQuery.error
const isFetching = isSearchMode ? searchQuery.isFetching : messagesQuery.isFetching
const sidebarNav = useSidebarNav()
const navMaps = useMemo<MailNavFolderMaps>(
() => ({
folderIdToLabel: sidebarNav.folderIdToLabel,
folderTree: sidebarNav.folderTree,
labelRows: sidebarNav.labelRows,
}),
[sidebarNav.folderIdToLabel, sidebarNav.folderTree, sidebarNav.labelRows]
)
const inboxCategoryTabIconsCatalog = useMemo(
() => buildInboxCategoryTabIcons(sidebarNav.labelRows),
[sidebarNav.labelRows]
)
const inboxTabBarItems = useMemo(
() => buildInboxTabBarItems(sidebarNav.labelRows),
[sidebarNav.labelRows]
)
const listRowLabelBgByTextLower = useMemo(
() => buildLabelTextToNavColorClass(sidebarNav.folderTree, sidebarNav.labelRows),
[sidebarNav.folderTree, sidebarNav.labelRows]
)
const [rescheduleTarget, setRescheduleTarget] = useState<{
id: string
value: string
panelOpen: boolean
} | null>(null)
const rescheduleDismissTimeoutsRef = useRef<
Map<string, ReturnType<typeof setTimeout>>
>(new Map())
const scheduleReschedulePopoverDismiss = useCallback((rowId: string) => {
const existing = rescheduleDismissTimeoutsRef.current.get(rowId)
if (existing) clearTimeout(existing)
const t = setTimeout(() => {
rescheduleDismissTimeoutsRef.current.delete(rowId)
setRescheduleTarget((p) => (p?.id === rowId ? null : p))
}, 280)
rescheduleDismissTimeoutsRef.current.set(rowId, t)
}, [])
useEffect(() => {
const m = rescheduleDismissTimeoutsRef.current
return () => {
for (const t of m.values()) clearTimeout(t)
m.clear()
}
}, [])
useEffect(() => {
ensureVcLogosCollection()
}, [])
const [cmScheduledRescheduleValue, setCmScheduledRescheduleValue] =
useState("")
const handleEditScheduledMail = useCallback(
async (id: string) => {
const payload = await requestGetScheduledEditPayload(id)
if (!payload) return
openComposeWithInitial({
to: payload.to,
subject: payload.subject,
bodyHtml: payload.bodyHtml,
editingScheduledId: id,
scheduledSendAtIso: payload.sendAtIso,
focusToOnMount: false,
focusBodyOnMount: true,
})
},
[requestGetScheduledEditPayload, openComposeWithInitial]
)
useEffect(() => {
if (!openMailId) {
closeAllInlineComposes()
} else {
const msg = emailById.get(openMailId)
pruneInlineComposesToOpenThread(msg ? threadStoreId(msg) : openMailId)
}
}, [
openMailId,
emailById,
closeAllInlineComposes,
pruneInlineComposesToOpenThread,
])
const conversationMode = useMailSettingsStore((s) => s.conversationMode)
const inboxSort = useMailSettingsStore((s) => s.inboxSort)
const density = useMailSettingsStore((s) => s.density)
const isMd = useIsMd()
const readOverrides = useMemo<Record<string, boolean>>(() => ({}), [])
const starredEmails = useMemo<string[]>(() => [], [])
const importantEmails = useMemo<string[]>(() => [], [])
const labelEdits = useMemo(() => ({ additions: {} as Record<string, string[]>, removals: {} as Record<string, string[]> }), [])
const hiddenEmailIds = useMemo<string[]>(() => [], [])
const notSpamEmailIds = useMemo<string[]>(() => [], [])
const setReadOverrides = useCallback(
(updater: (prev: Record<string, boolean>) => Record<string, boolean>) => {
const changes = updater({})
for (const [id, isRead] of Object.entries(changes)) {
const msg = apiMessagesById.get(id)
if (!msg) continue
const flags = [...msg.flags]
if (isRead && !flags.includes("read")) {
updateFlags.mutate({ id, flags: [...flags, "read"] })
} else if (!isRead && flags.includes("read")) {
updateFlags.mutate({ id, flags: flags.filter((f) => f !== "read") })
}
}
},
[apiMessagesById, updateFlags]
)
const setLabelEdits = useCallback(
(updater: (prev: { additions: Record<string, string[]>; removals: Record<string, string[]> }) => { additions: Record<string, string[]>; removals: Record<string, string[]> }) => {
const result = updater({ additions: {}, removals: {} })
for (const [id, additions] of Object.entries(result.additions)) {
const msg = apiMessagesById.get(id)
if (!msg) continue
const newLabels = [...new Set([...msg.labels, ...additions])]
const removals = result.removals[id] ?? []
const finalLabels = newLabels.filter(
(l) => !removals.some((r) => r.toLowerCase() === l.toLowerCase())
)
updateLabels.mutate({ id, labels: finalLabels })
}
for (const [id, removals] of Object.entries(result.removals)) {
if (result.additions[id]) continue
const msg = apiMessagesById.get(id)
if (!msg) continue
const finalLabels = msg.labels.filter(
(l) => !removals.some((r) => r.toLowerCase() === l.toLowerCase())
)
updateLabels.mutate({ id, labels: finalLabels })
}
},
[apiMessagesById, updateLabels]
)
const mailActions = useMemo(() => ({
markSeen: (id: string) => useMailStore.getState().markSeen(id),
pushRecentMoveTarget: (targetId: string) => useMailStore.getState().pushRecentMoveTarget(targetId),
hideEmail: (id: string) => deleteMessage.mutate({ id }),
hideEmails: (ids: string[]) => { for (const id of ids) deleteMessage.mutate({ id }) },
markNotSpam: (id: string) => {
const msg = apiMessagesById.get(id)
if (!msg) return
const newLabels = msg.labels.filter((l) => l !== "spam")
if (!newLabels.includes("inbox")) newLabels.push("inbox")
updateLabels.mutate({ id, labels: newLabels })
},
unhideEmail: (_id: string) => { /* no-op - API manages visibility */ },
toggleStar: (id: string) => {
const msg = apiMessagesById.get(id)
if (!msg) return
const flags = msg.flags.includes("starred")
? msg.flags.filter((f) => f !== "starred")
: [...msg.flags, "starred"]
updateFlags.mutate({ id, flags })
},
toggleImportant: (id: string) => {
const msg = apiMessagesById.get(id)
if (!msg) return
const flags = msg.flags.includes("important")
? msg.flags.filter((f) => f !== "important")
: [...msg.flags, "important"]
updateFlags.mutate({ id, flags })
},
}), [deleteMessage, updateLabels, updateFlags, apiMessagesById])
useEffect(() => {
registerNavEmailSync({
renameLabel: (_from, _to) => {
queryClient.invalidateQueries({ queryKey: ["messages"] })
},
removeLabel: (_label) => {
queryClient.invalidateQueries({ queryKey: ["messages"] })
},
})
return () => registerNavEmailSync(null)
}, [queryClient])
const [labelPickerQuery, setLabelPickerQuery] = useState("")
const recentMoveTargets = useMailStore((s) => s.recentMoveTargets)
const [mobileVisibleCount, setMobileVisibleCount] = useState(LIST_PAGE_SIZE)
const isXs = useIsXs()
const touchNav = useTouchNav()
const seenEmailIdsRaw = useMailStore((s) => s.seenEmailIds)
const seenEmailIds = useMemo(() => new Set(seenEmailIdsRaw), [seenEmailIdsRaw])
const handleRefreshMessages = useCallback(async () => {
await queryClient.invalidateQueries({ queryKey: ["messages"] })
}, [queryClient])
const {
isRefreshing,
setIsRefreshing,
listViewportRef,
pullContentRef,
pullIconRef,
} = useMailListPullRefresh({
enabled: isXs && !isViewMode,
isViewMode,
onRefresh: handleRefreshMessages,
})
const handleManualRefresh = useCallback(async () => {
if (isRefreshing) return
setIsRefreshing(true)
try {
await handleRefreshMessages()
} finally {
setIsRefreshing(false)
}
}, [isRefreshing, handleRefreshMessages, setIsRefreshing])
const markEmailSeen = useCallback((id: string) => {
useMailStore.getState().markSeen(id)
}, [])
const filteredEmails = useMemo(() => {
return allEmails
}, [allEmails])
const displayListEmails = useMemo(() => {
let rows = filteredEmails
if (conversationMode) {
rows = rows.filter(isThreadHeadMessage)
}
return sortEmailsForInbox(
rows,
inboxSort,
{
readOverrides: {},
starredIds: [],
importantIds: [],
},
{ conversationMode, byId: emailById }
)
}, [
filteredEmails,
conversationMode,
inboxSort,
emailById,
])
const inboxCategoryTabLabel = useMemo(
() =>
inboxTabDisplayLabel(
inboxTab,
sidebarNav.labelRows,
sidebarNav.folderIdToLabel
),
[inboxTab, sidebarNav.labelRows, sidebarNav.folderIdToLabel]
)
const mobileUnreadCount = useMemo(
() => displayListEmails.filter((e) => !e.read).length,
[displayListEmails]
)
const mobileFolderLabel = useMemo(() => {
if (isSearchMode) return "Résultats de recherche"
const inboxTabNorm = normalizeInboxTabSegment(inboxTab)
return selectedFolder === "inbox" && inboxTabNorm !== "primary"
? inboxCategoryTabLabel
: getMailNavFolderLabel(selectedFolder, sidebarNav.folderIdToLabel)
}, [
selectedFolder,
inboxTab,
inboxCategoryTabLabel,
sidebarNav.folderIdToLabel,
isSearchMode,
])
const paginationTotal = useMemo(() => {
if (isSearchMode) return searchQuery.data?.pagination?.total
if (effectiveApiFolder === "__local__") return allEmails.length
return messagesQuery.data?.pagination?.total
}, [isSearchMode, effectiveApiFolder, searchQuery.data, messagesQuery.data, allEmails.length])
const totalPages = useMemo(
() => Math.max(1, Math.ceil((paginationTotal ?? displayListEmails.length) / LIST_PAGE_SIZE)),
[paginationTotal, displayListEmails.length]
)
const pagedEmails = useMemo(() => {
if (effectiveApiFolder !== "__local__" && !isSearchMode) {
return displayListEmails
}
const start = (listPage - 1) * LIST_PAGE_SIZE
return displayListEmails.slice(start, start + LIST_PAGE_SIZE)
}, [displayListEmails, listPage, effectiveApiFolder, isSearchMode])
const listEmails = useMemo(() => {
if (isXs && !isViewMode) {
return displayListEmails.slice(0, mobileVisibleCount)
}
return pagedEmails
}, [isXs, isViewMode, displayListEmails, mobileVisibleCount, pagedEmails])
const listMailIndex = useMemo(() => buildListMailIndex(allEmails), [allEmails])
const folderFilterCtx = useMemo(
() => ({
starredEmailIds: [] as string[],
importantEmailIds: [] as string[],
}),
[]
)
const listRowExtras = useMemo(() => {
const invitationById = new Map<
string,
ReturnType<typeof resolveParsedCalendarInvitation>
>()
const attachmentsById = new Map<string, EmailAttachment[]>()
const categoryTabsById = new Map<
string,
ReturnType<typeof resolveEmailInboxCategoryTabs>
>()
const subtreeIdsCache = new Map<string, string[] | null>()
const showCategoryTabIcons =
selectedFolder === "inbox" &&
normalizeInboxTabSegment(inboxTab) === INBOX_ALL_TAB
for (const e of listEmails) {
invitationById.set(e.id, resolveParsedCalendarInvitation(e))
attachmentsById.set(e.id, attachmentsForEmailList(e))
if (showCategoryTabIcons) {
const tabs = resolveEmailInboxCategoryTabs(
e,
folderFilterCtx,
navMaps,
inboxCategoryTabIconsCatalog,
subtreeIdsCache
)
if (tabs.length > 0) categoryTabsById.set(e.id, tabs)
}
}
return { invitationById, attachmentsById, categoryTabsById }
}, [
listEmails,
selectedFolder,
inboxTab,
folderFilterCtx,
navMaps,
inboxCategoryTabIconsCatalog,
])
useEffect(() => {
if (isXs) return
if (listPage > totalPages) {
onMailRouteNavigate({ page: totalPages })
}
}, [isXs, listPage, totalPages, onMailRouteNavigate])
useEffect(() => {
if (isXs && !isViewMode) return
listViewportRef.current?.scrollTo(0, 0)
}, [listPage, selectedFolder, inboxTab, isXs, isViewMode, listViewportRef])
useEffect(() => {
if (!isXs) return
setMobileVisibleCount(LIST_PAGE_SIZE)
listViewportRef.current?.scrollTo(0, 0)
}, [selectedFolder, inboxTab, isXs, listViewportRef])
useEffect(() => {
const root = listViewportRef.current
if (!root || !isXs || isViewMode) return
const onScroll = () => {
if (mobileVisibleCount >= displayListEmails.length) return
const nearBottom =
root.scrollTop + root.clientHeight >= root.scrollHeight - 120
if (nearBottom) {
setMobileVisibleCount((prev) =>
Math.min(prev + LIST_PAGE_SIZE, displayListEmails.length)
)
}
}
root.addEventListener("scroll", onScroll, { passive: true })
return () => root.removeEventListener("scroll", onScroll)
}, [isXs, isViewMode, mobileVisibleCount, displayListEmails.length, listViewportRef])
const moveTargets = useMoveTargets({
folderTree: sidebarNav.folderTree,
recentMoveTargets,
currentFolderId: selectedFolder,
})
const folderUnreadCounts = useMemo<Record<string, number>>(() => ({}), [])
const seenSerialized = useMemo(
() => [...seenEmailIds].sort().join(","),
[seenEmailIds]
)
const { unseenInTabById, tabUnseenSenderLineById } = useMemo(() => {
const seen = new Set(
seenSerialized.length > 0 ? seenSerialized.split(",") : []
)
const inboxPool = allEmails.filter((e) => !seen.has(e.id))
const counts: Record<string, number> = {}
const preview: Record<string, string> = {}
for (const tab of inboxTabBarItems) {
const rows = inboxPool.filter((e) => !seen.has(e.id))
counts[tab.id] = rows.length
if (inboxTabShowsInactiveMeta(tab.id)) {
const chain: string[] = []
const used = new Set<string>()
for (const e of rows) {
const n = cleanSenderName(e.sender).trim()
if (!n || used.has(n)) continue
used.add(n)
chain.push(n)
if (chain.length >= 6) break
}
preview[tab.id] = chain.join(", ")
}
}
return { unseenInTabById: counts, tabUnseenSenderLineById: preview }
}, [seenSerialized, allEmails, inboxTabBarItems])
useEffect(() => {
onFolderUnreadCountsChange?.(folderUnreadCounts)
}, [folderUnreadCounts, onFolderUnreadCountsChange])
const listToolbarMode = splitView || !isViewMode
const compactInboxTabs = isXs || splitView
const activeInboxTabId = useMemo(
() => normalizeInboxTabSegment(inboxTab),
[inboxTab]
)
const pageIds = useMemo(() => listEmails.map((e) => e.id), [listEmails])
const listRowsDep = listEmails.map((e) => e.id).join(",")
const effectiveRead = useCallback(
(email: Email) => email.read,
[]
)
const effectiveStarred = useCallback(
(email: Email) => email.starred,
[]
)
const markAllInViewAsRead = useCallback(() => {
for (const e of displayListEmails) {
if (e.read) continue
const msg = apiMessagesById.get(e.id)
if (!msg) continue
if (!msg.flags.includes("read")) {
updateFlags.mutate({ id: e.id, flags: [...msg.flags, "read"] })
}
}
}, [displayListEmails, apiMessagesById, updateFlags])
return {
selectedFolder,
inboxTab,
listPage,
openMailId,
splitView,
isViewMode,
showSplitReadingPane,
isSearchMode,
searchRouter,
searchAccount,
setAdvancedOpen,
searchParams,
setSearchFilter,
toggleSearchFilter,
savedThreadReplyDrafts,
openCompose,
openComposeWithInitial,
allEmails,
emailById,
sidebarNav,
navMaps,
inboxCategoryTabIconsCatalog,
inboxTabBarItems,
listRowLabelBgByTextLower,
rescheduleTarget,
setRescheduleTarget,
rescheduleDismissTimeoutsRef,
scheduleReschedulePopoverDismiss,
cmScheduledRescheduleValue,
setCmScheduledRescheduleValue,
handleEditScheduledMail,
starredEmails,
importantEmails,
readOverrides,
conversationMode,
inboxSort,
density,
isMd,
labelEdits,
mailActions,
setReadOverrides,
setLabelEdits,
labelPickerQuery,
setLabelPickerQuery,
hiddenEmailIds,
notSpamEmailIds,
recentMoveTargets,
mobileVisibleCount,
isXs,
touchNav,
seenEmailIds,
isRefreshing,
listViewportRef,
pullContentRef,
pullIconRef,
handleManualRefresh,
markEmailSeen,
folderFilterCtx,
filteredEmails,
displayListEmails,
inboxCategoryTabLabel,
mobileUnreadCount,
mobileFolderLabel,
totalPages,
pagedEmails,
listEmails,
listMailIndex,
listRowExtras,
moveTargets,
folderUnreadCounts,
unseenInTabById,
tabUnseenSenderLineById,
listToolbarMode,
compactInboxTabs,
activeInboxTabId,
pageIds,
listRowsDep,
effectiveRead,
effectiveStarred,
markAllInViewAsRead,
requestDeleteScheduled,
requestArchiveScheduled,
requestSnoozeScheduled,
requestToggleReadScheduled,
requestRescheduleScheduled,
requestSendScheduledNow,
requestSnoozeMailboxEmail,
requestRestoreSnoozedToInbox,
isLoading,
error,
isFetching,
}
}
export type EmailListData = ReturnType<typeof useEmailListData>