786 lines
23 KiB
TypeScript
786 lines
23 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react"
|
|
import { useSearchParams, useRouter } from "next/navigation"
|
|
import { buildLabelTextToNavColorClass } from "@/components/gmail/mail-label-pills"
|
|
import { emails } from "@/lib/email-data"
|
|
import {
|
|
isListRowRead,
|
|
isThreadHeadMessage,
|
|
readStateTargets,
|
|
} 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 {
|
|
emailMatchesFolder,
|
|
emailMatchesInboxPrimaryTab,
|
|
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 { filterEmailsBySearchParams } from "@/lib/mail-search/search-engine"
|
|
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 { computeFolderUnreadCounts } from "@/lib/mail-nav-metrics"
|
|
import {
|
|
mergeEmailLabelEdits,
|
|
mergeEmailNotSpam,
|
|
} from "@/lib/label-edits"
|
|
import type { LabelEditState } from "@/lib/stores/mail-store"
|
|
import { useIsXs } from "@/hooks/use-xs"
|
|
import { useTouchNav } from "@/hooks/use-touch-nav"
|
|
import {
|
|
applyNavRenameToEdits,
|
|
applyNavRemoveLabelToEdits,
|
|
} from "@/lib/mail-list/label-actions"
|
|
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 type { Email, EmailAttachment } from "@/lib/email-data"
|
|
import { cleanSenderName } from "@/lib/sender-display"
|
|
import { threadStoreId } from "@/lib/mail-settings/list-row-id"
|
|
|
|
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,
|
|
sentPlaceholderEmails,
|
|
requestDeleteScheduled,
|
|
requestArchiveScheduled,
|
|
requestSnoozeScheduled,
|
|
requestToggleReadScheduled,
|
|
requestRescheduleScheduled,
|
|
requestGetScheduledEditPayload,
|
|
requestSendScheduledNow,
|
|
requestSnoozeMailboxEmail,
|
|
requestRestoreSnoozedToInbox,
|
|
} = useScheduledMail()
|
|
|
|
const scheduledPersistHydrated = usePersistHydrated(useScheduledStore)
|
|
|
|
const allEmails = useMemo(
|
|
() =>
|
|
scheduledPersistHydrated
|
|
? [...emails, ...scheduledEmails, ...snoozedEmails, ...sentPlaceholderEmails]
|
|
: emails,
|
|
[scheduledPersistHydrated, scheduledEmails, snoozedEmails, sentPlaceholderEmails]
|
|
)
|
|
|
|
const emailById = useMemo(
|
|
() => new Map(allEmails.map((e) => [e.id, e])),
|
|
[allEmails]
|
|
)
|
|
|
|
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 starredEmails = useMailStore((s) => s.starredIds)
|
|
const importantEmails = useMailStore((s) => s.importantIds)
|
|
const readOverrides = useMailStore((s) => s.readOverrides)
|
|
const conversationMode = useMailSettingsStore((s) => s.conversationMode)
|
|
const inboxSort = useMailSettingsStore((s) => s.inboxSort)
|
|
const density = useMailSettingsStore((s) => s.density)
|
|
const isMd = useIsMd()
|
|
const labelEdits = useMailStore((s) => s.labelEdits)
|
|
const mailActions = useRef(useMailStore.getState()).current
|
|
const setReadOverrides = useCallback(
|
|
(updater: (prev: Record<string, boolean>) => Record<string, boolean>) => {
|
|
const current = useMailStore.getState().readOverrides
|
|
const next = updater(current)
|
|
if (next !== current) mailActions.setReadOverrides(next)
|
|
},
|
|
[mailActions]
|
|
)
|
|
const setLabelEdits = useCallback(
|
|
(updater: (prev: LabelEditState) => LabelEditState) => {
|
|
mailActions.setLabelEdits(updater)
|
|
},
|
|
[mailActions]
|
|
)
|
|
|
|
useEffect(() => {
|
|
registerNavEmailSync({
|
|
renameLabel: (from, to) => {
|
|
setLabelEdits((prev) => applyNavRenameToEdits(allEmails, prev, from, to))
|
|
},
|
|
removeLabel: (label) => {
|
|
setLabelEdits((prev) => applyNavRemoveLabelToEdits(allEmails, prev, label))
|
|
},
|
|
})
|
|
return () => registerNavEmailSync(null)
|
|
}, [allEmails, setLabelEdits])
|
|
|
|
const [labelPickerQuery, setLabelPickerQuery] = useState("")
|
|
const hiddenEmailIds = useMailStore((s) => s.hiddenEmailIds)
|
|
const notSpamEmailIds = useMailStore((s) => s.notSpamEmailIds)
|
|
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 new Promise((resolve) => setTimeout(resolve, 900))
|
|
}, [])
|
|
|
|
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) => {
|
|
mailActions.markSeen(id)
|
|
}, [mailActions])
|
|
|
|
const folderFilterCtx = useMemo(
|
|
() => ({
|
|
starredEmailIds: starredEmails,
|
|
importantEmailIds: importantEmails,
|
|
}),
|
|
[starredEmails, importantEmails]
|
|
)
|
|
|
|
const filteredEmails = useMemo(() => {
|
|
const hiddenSet = new Set(hiddenEmailIds)
|
|
const subtreeIdsCache = new Map<string, string[] | null>()
|
|
let visible = allEmails.filter((email) => !hiddenSet.has(email.id))
|
|
const hasLabelEdits =
|
|
labelEdits &&
|
|
(Object.keys(labelEdits.additions).length > 0 ||
|
|
Object.keys(labelEdits.removals).length > 0)
|
|
if (hasLabelEdits || notSpamEmailIds.length > 0) {
|
|
visible = visible.map((e) =>
|
|
mergeEmailNotSpam(mergeEmailLabelEdits(e, labelEdits), notSpamEmailIds)
|
|
)
|
|
}
|
|
|
|
if (isSearchMode && searchParams) {
|
|
return filterEmailsBySearchParams(visible, searchParams, {
|
|
starredIds: starredEmails,
|
|
importantIds: importantEmails,
|
|
})
|
|
}
|
|
|
|
let rows = visible.filter((email) =>
|
|
emailMatchesFolder(
|
|
email,
|
|
selectedFolder,
|
|
folderFilterCtx,
|
|
navMaps,
|
|
subtreeIdsCache
|
|
)
|
|
)
|
|
if (selectedFolder === "inbox") {
|
|
const tab = normalizeInboxTabSegment(inboxTab)
|
|
if (tab === "primary") {
|
|
rows = rows.filter((email) =>
|
|
emailMatchesInboxPrimaryTab(
|
|
email,
|
|
folderFilterCtx,
|
|
navMaps,
|
|
subtreeIdsCache
|
|
)
|
|
)
|
|
} else if (tab !== INBOX_ALL_TAB) {
|
|
rows = rows.filter(
|
|
(email) =>
|
|
emailMatchesFolder(
|
|
email,
|
|
"inbox",
|
|
folderFilterCtx,
|
|
navMaps,
|
|
subtreeIdsCache
|
|
) &&
|
|
emailMatchesFolder(
|
|
email,
|
|
tab,
|
|
folderFilterCtx,
|
|
navMaps,
|
|
subtreeIdsCache
|
|
)
|
|
)
|
|
}
|
|
}
|
|
return rows
|
|
}, [
|
|
selectedFolder,
|
|
inboxTab,
|
|
hiddenEmailIds,
|
|
folderFilterCtx,
|
|
labelEdits,
|
|
notSpamEmailIds,
|
|
allEmails,
|
|
navMaps,
|
|
isSearchMode,
|
|
searchParams,
|
|
starredEmails,
|
|
importantEmails,
|
|
])
|
|
|
|
const displayListEmails = useMemo(() => {
|
|
let rows = filteredEmails
|
|
if (conversationMode) {
|
|
rows = rows.filter(isThreadHeadMessage)
|
|
}
|
|
return sortEmailsForInbox(
|
|
rows,
|
|
inboxSort,
|
|
{
|
|
readOverrides,
|
|
starredIds: starredEmails,
|
|
importantIds: importantEmails,
|
|
},
|
|
{ conversationMode, byId: emailById }
|
|
)
|
|
}, [
|
|
filteredEmails,
|
|
conversationMode,
|
|
inboxSort,
|
|
readOverrides,
|
|
starredEmails,
|
|
importantEmails,
|
|
emailById,
|
|
])
|
|
|
|
const inboxCategoryTabLabel = useMemo(
|
|
() =>
|
|
inboxTabDisplayLabel(
|
|
inboxTab,
|
|
sidebarNav.labelRows,
|
|
sidebarNav.folderIdToLabel
|
|
),
|
|
[inboxTab, sidebarNav.labelRows, sidebarNav.folderIdToLabel]
|
|
)
|
|
|
|
const mobileUnreadCount = useMemo(
|
|
() =>
|
|
displayListEmails.filter(
|
|
(e) => !isListRowRead(e, readOverrides, emailById, conversationMode)
|
|
).length,
|
|
[displayListEmails, readOverrides, emailById, conversationMode]
|
|
)
|
|
|
|
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 totalPages = useMemo(
|
|
() => Math.max(1, Math.ceil(displayListEmails.length / LIST_PAGE_SIZE)),
|
|
[displayListEmails.length]
|
|
)
|
|
|
|
const pagedEmails = useMemo(() => {
|
|
const start = (listPage - 1) * LIST_PAGE_SIZE
|
|
return displayListEmails.slice(start, start + LIST_PAGE_SIZE)
|
|
}, [displayListEmails, listPage])
|
|
|
|
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 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(
|
|
() =>
|
|
computeFolderUnreadCounts(
|
|
allEmails,
|
|
folderFilterCtx,
|
|
hiddenEmailIds,
|
|
readOverrides,
|
|
navMaps,
|
|
labelEdits,
|
|
notSpamEmailIds
|
|
),
|
|
[
|
|
folderFilterCtx,
|
|
hiddenEmailIds,
|
|
readOverrides,
|
|
allEmails,
|
|
navMaps,
|
|
labelEdits,
|
|
notSpamEmailIds,
|
|
]
|
|
)
|
|
|
|
const seenSerialized = useMemo(
|
|
() => [...seenEmailIds].sort().join(","),
|
|
[seenEmailIds]
|
|
)
|
|
|
|
const { unseenInTabById, tabUnseenSenderLineById } = useMemo(() => {
|
|
const seen = new Set(
|
|
seenSerialized.length > 0 ? seenSerialized.split(",") : []
|
|
)
|
|
const hidden = new Set(hiddenEmailIds)
|
|
const visible = allEmails
|
|
.filter((email) => !hidden.has(email.id))
|
|
.map((e) =>
|
|
mergeEmailNotSpam(mergeEmailLabelEdits(e, labelEdits), notSpamEmailIds)
|
|
)
|
|
const inboxPool = visible.filter((e) =>
|
|
emailMatchesFolder(e, "inbox", folderFilterCtx, navMaps)
|
|
)
|
|
const counts: Record<string, number> = {}
|
|
const preview: Record<string, string> = {}
|
|
const tabCache = new Map<string, string[] | null>()
|
|
for (const tab of inboxTabBarItems) {
|
|
const rows = inboxPool.filter((e) => {
|
|
if (tab.id === "primary") {
|
|
return (
|
|
emailMatchesInboxPrimaryTab(e, folderFilterCtx, navMaps, tabCache) &&
|
|
!seen.has(e.id)
|
|
)
|
|
}
|
|
if (tab.id === INBOX_ALL_TAB) {
|
|
return !seen.has(e.id)
|
|
}
|
|
return (
|
|
emailMatchesFolder(e, "inbox", folderFilterCtx, navMaps, tabCache) &&
|
|
emailMatchesFolder(e, tab.id, folderFilterCtx, navMaps, tabCache) &&
|
|
!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 }
|
|
}, [folderFilterCtx, hiddenEmailIds, labelEdits, seenSerialized, allEmails, navMaps, notSpamEmailIds, 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) =>
|
|
readOverrides[email.id] !== undefined ? readOverrides[email.id]! : email.read,
|
|
[readOverrides]
|
|
)
|
|
|
|
const effectiveStarred = useCallback(
|
|
(email: Email) =>
|
|
starredEmails.includes(email.id) || email.starred,
|
|
[starredEmails]
|
|
)
|
|
|
|
const markAllInViewAsRead = useCallback(() => {
|
|
setReadOverrides((prev) => {
|
|
const next = { ...prev }
|
|
for (const e of displayListEmails) {
|
|
for (const id of readStateTargets(e, conversationMode)) {
|
|
next[id] = true
|
|
}
|
|
}
|
|
return next
|
|
})
|
|
}, [displayListEmails, conversationMode, setReadOverrides])
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
export type EmailListData = ReturnType<typeof useEmailListData>
|