176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
import type { Email } from "@/lib/email-data"
|
||
import {
|
||
folderTree as defaultFolderTree,
|
||
sidebarNavFolderIdToLabel as defaultSidebarNavFolderIdToLabel,
|
||
defaultNavLabelRowsSnapshot,
|
||
type LabelRowItem,
|
||
} from "@/lib/sidebar-nav-data"
|
||
import { collectSubtreeFolderIds } from "@/lib/sidebar-nav-maps"
|
||
import type { FolderTreeNode } from "@/lib/sidebar-nav-maps"
|
||
|
||
export type MailFolderFilterCtx = {
|
||
starredEmailIds: string[]
|
||
importantEmailIds: string[]
|
||
}
|
||
|
||
/** Carte navigation dynamique (sidebar). Si absent, valeurs par défaut du module. */
|
||
export type MailNavFolderMaps = {
|
||
folderIdToLabel: Record<string, string>
|
||
folderTree: FolderTreeNode[]
|
||
labelRows: LabelRowItem[]
|
||
}
|
||
|
||
function hasFutureScheduledSend(email: Email): boolean {
|
||
if (!email.scheduledSendAt) return false
|
||
const t = new Date(email.scheduledSendAt).getTime()
|
||
if (!Number.isFinite(t)) return false
|
||
return t > Date.now()
|
||
}
|
||
|
||
function effectiveStarred(email: Email, ctx: MailFolderFilterCtx): boolean {
|
||
return ctx.starredEmailIds.includes(email.id) || email.starred
|
||
}
|
||
|
||
function effectiveImportant(email: Email, ctx: MailFolderFilterCtx): boolean {
|
||
return ctx.importantEmailIds.includes(email.id) || email.important
|
||
}
|
||
|
||
function isInInbox(email: Email): boolean {
|
||
if (email.deleted) return false
|
||
if (email.spam) return false
|
||
if (hasFutureScheduledSend(email)) return false
|
||
const ls = email.labels
|
||
if (!ls?.length) return true
|
||
return ls.includes("inbox")
|
||
}
|
||
|
||
function labelRowForNavId(
|
||
folderId: string,
|
||
rows: readonly LabelRowItem[]
|
||
): LabelRowItem | undefined {
|
||
return rows.find((r) => r.id === folderId)
|
||
}
|
||
|
||
function resolveNavMaps(maps?: MailNavFolderMaps | null): MailNavFolderMaps {
|
||
if (maps) return maps
|
||
return {
|
||
folderIdToLabel: defaultSidebarNavFolderIdToLabel as Record<string, string>,
|
||
folderTree: defaultFolderTree,
|
||
labelRows: defaultNavLabelRowsSnapshot,
|
||
}
|
||
}
|
||
|
||
function emailHasAnyLabel(email: Email, labels: string[]): boolean {
|
||
const ls = email.labels
|
||
if (!ls?.length) return false
|
||
const lower = new Set(ls.map((x) => x.toLowerCase()))
|
||
return labels.some((lab) => lower.has(lab.toLowerCase()))
|
||
}
|
||
|
||
function matchesFolderLabelRow(
|
||
email: Email,
|
||
folderId: string,
|
||
maps: MailNavFolderMaps
|
||
): boolean {
|
||
const row = labelRowForNavId(folderId, maps.labelRows)
|
||
if (row && row.enabled === false) return false
|
||
const label = maps.folderIdToLabel[folderId]
|
||
if (!label) return false
|
||
return emailHasAnyLabel(email, [label])
|
||
}
|
||
|
||
/** Dossier hiérarchique : ce nœud ou n'importe quel sous-dossier (décompte unique par mail). */
|
||
function matchesLabelNav(
|
||
email: Email,
|
||
folderId: string,
|
||
maps: MailNavFolderMaps,
|
||
subtreeIdsCache?: Map<string, string[] | null>
|
||
): boolean {
|
||
let subtreeIds: string[] | null
|
||
if (subtreeIdsCache) {
|
||
if (!subtreeIdsCache.has(folderId)) {
|
||
subtreeIdsCache.set(
|
||
folderId,
|
||
collectSubtreeFolderIds(maps.folderTree, folderId)
|
||
)
|
||
}
|
||
subtreeIds = subtreeIdsCache.get(folderId) ?? null
|
||
} else {
|
||
subtreeIds = collectSubtreeFolderIds(maps.folderTree, folderId)
|
||
}
|
||
if (subtreeIds) {
|
||
return subtreeIds.some((id) => matchesFolderLabelRow(email, id, maps))
|
||
}
|
||
return matchesFolderLabelRow(email, folderId, maps)
|
||
}
|
||
|
||
/** Onglet Principale : boîte sans libellés système « exclude » actifs. */
|
||
export function emailMatchesInboxPrimaryTab(
|
||
email: Email,
|
||
ctx: MailFolderFilterCtx,
|
||
maps: MailNavFolderMaps,
|
||
subtreeIdsCache?: Map<string, string[] | null>
|
||
): boolean {
|
||
if (!emailMatchesFolder(email, "inbox", ctx, maps, subtreeIdsCache)) return false
|
||
if (email.deleted) return false
|
||
if (hasFutureScheduledSend(email)) return false
|
||
for (const row of maps.labelRows) {
|
||
if (row.enabled === false) continue
|
||
if (!row.excludeFromPrincipal) continue
|
||
if (emailHasAnyLabel(email, [row.label])) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
export function emailMatchesFolder(
|
||
email: Email,
|
||
folderId: string,
|
||
ctx: MailFolderFilterCtx,
|
||
maps?: MailNavFolderMaps | null,
|
||
/** Réutiliser entre appels (ex. `computeFolderUnreadCounts`) pour éviter de rescanner l’arbre à chaque mail. */
|
||
subtreeIdsCache?: Map<string, string[] | null>
|
||
): boolean {
|
||
const nav = resolveNavMaps(maps)
|
||
|
||
if (email.deleted && folderId !== "trash") {
|
||
return false
|
||
}
|
||
|
||
switch (folderId) {
|
||
case "inbox":
|
||
return isInInbox(email)
|
||
case "starred":
|
||
return effectiveStarred(email, ctx)
|
||
case "snoozed":
|
||
return (email.labels?.includes("snoozed") ?? false) && !email.deleted
|
||
case "important":
|
||
return effectiveImportant(email, ctx)
|
||
case "sent":
|
||
return (email.labels?.includes("sent") ?? false) && !email.deleted
|
||
case "drafts":
|
||
return (email.labels?.includes("drafts") ?? false) && !email.deleted
|
||
case "scheduled":
|
||
return !email.deleted && hasFutureScheduledSend(email)
|
||
case "spam":
|
||
return (
|
||
!email.deleted &&
|
||
(email.spam === true || (email.labels?.includes("spam") ?? false))
|
||
)
|
||
case "trash":
|
||
return email.deleted === true
|
||
default:
|
||
break
|
||
}
|
||
|
||
const row = labelRowForNavId(folderId, nav.labelRows)
|
||
if (row && row.enabled === false) return false
|
||
|
||
if (nav.folderIdToLabel[folderId]) {
|
||
return matchesLabelNav(email, folderId, nav, subtreeIdsCache)
|
||
}
|
||
|
||
if (email.labels?.includes(folderId)) return true
|
||
|
||
return false
|
||
}
|