ultisuite-client/components/gmail/email-list/hooks/use-email-list-labels.ts
2026-05-20 18:22:36 +02:00

291 lines
10 KiB
TypeScript

"use client"
import { useCallback, useMemo } from "react"
import type { CatalogLabelPresence } from "@/components/gmail/email-label-picker-block"
import { resolveLabelPickerVisual } from "@/lib/label-picker-visual"
import {
effectiveLabels,
mergeEmailLabelEdits,
mergeEmailNotSpam,
} from "@/lib/label-edits"
import type { FolderTreeNode } from "@/lib/sidebar-nav-data"
import {
LABEL_PICKER_EXCLUDE,
} from "@/lib/mail-list/label-actions"
import {
collectTreeLabels,
} from "@/components/gmail/email-list/email-list-helpers"
import type { EmailListData } from "@/components/gmail/email-list/hooks/use-email-list-data"
export function useEmailListLabels(data: EmailListData) {
const {
allEmails,
sidebarNav,
labelEdits,
notSpamEmailIds,
setLabelEdits,
mailActions,
} = data
const collectAllFolderLabels = useCallback((): Set<string> => {
const s = new Set<string>()
const walk = (nodes: FolderTreeNode[]) => {
for (const n of nodes) {
s.add(n.label.toLowerCase())
if (n.children?.length) walk(n.children)
}
}
walk(sidebarNav.folderTree)
return s
}, [sidebarNav.folderTree])
const moveEmailsToTarget = useCallback(
(emailIds: string[], targetId: string) => {
if (emailIds.length === 0) return
const folderLabel = sidebarNav.folderIdToLabel[targetId]
const isSystemTarget = ["inbox", "sent", "drafts", "spam", "trash"].includes(targetId)
const allFolderLabels = collectAllFolderLabels()
setLabelEdits((prev) => {
const nextAdd = { ...prev.additions }
const nextRem = { ...prev.removals }
for (const id of emailIds) {
const email = allEmails.find((e) => e.id === id)
const currentLabels = effectiveLabels(email, nextAdd, nextRem)
if (isSystemTarget) {
if (targetId === "inbox") {
for (const lab of currentLabels) {
if (allFolderLabels.has(lab.toLowerCase())) {
const cur = nextRem[id] ?? []
if (!cur.some((l) => l.toLowerCase() === lab.toLowerCase())) {
nextRem[id] = [...cur, lab]
}
if (nextAdd[id]?.length) {
nextAdd[id] = nextAdd[id].filter((l) => l.toLowerCase() !== lab.toLowerCase())
if (nextAdd[id].length === 0) delete nextAdd[id]
}
}
}
}
} else if (folderLabel) {
for (const lab of currentLabels) {
if (allFolderLabels.has(lab.toLowerCase()) && lab.toLowerCase() !== folderLabel.toLowerCase()) {
const cur = nextRem[id] ?? []
if (!cur.some((l) => l.toLowerCase() === lab.toLowerCase())) {
nextRem[id] = [...cur, lab]
}
if (nextAdd[id]?.length) {
nextAdd[id] = nextAdd[id].filter((l) => l.toLowerCase() !== lab.toLowerCase())
if (nextAdd[id].length === 0) delete nextAdd[id]
}
}
}
if (!currentLabels.some((l) => l.toLowerCase() === folderLabel.toLowerCase())) {
nextAdd[id] = [...(nextAdd[id] ?? []), folderLabel]
}
if (nextRem[id]?.length) {
nextRem[id] = nextRem[id].filter((l) => l.toLowerCase() !== folderLabel.toLowerCase())
if (nextRem[id].length === 0) delete nextRem[id]
}
const inboxIdx = currentLabels.findIndex((l) => l.toLowerCase() === "inbox")
if (inboxIdx >= 0 || !email?.labels?.length || email.labels.includes("inbox")) {
const cur = nextRem[id] ?? []
if (!cur.some((l) => l.toLowerCase() === "inbox")) {
nextRem[id] = [...cur, "inbox"]
}
}
}
}
return { additions: nextAdd, removals: nextRem }
})
if (!isSystemTarget || targetId === "inbox") {
mailActions.pushRecentMoveTarget(targetId)
}
if (isSystemTarget && targetId !== "inbox") {
mailActions.hideEmails(emailIds)
mailActions.pushRecentMoveTarget(targetId)
}
},
[allEmails, sidebarNav.folderIdToLabel, collectAllFolderLabels, setLabelEdits, mailActions]
)
const catalogLabels = useMemo(() => {
const s = new Set<string>()
for (const l of collectTreeLabels(sidebarNav.folderTree)) s.add(l)
for (const row of sidebarNav.labelRows) s.add(row.label)
for (const e of allEmails) {
const eff = mergeEmailNotSpam(
mergeEmailLabelEdits(e, labelEdits),
notSpamEmailIds
)
for (const lab of eff.labels ?? []) {
if (!LABEL_PICKER_EXCLUDE.has(lab)) s.add(lab)
}
}
return [...s].sort((a, b) => a.localeCompare(b, "fr"))
}, [sidebarNav.folderTree, sidebarNav.labelRows, allEmails, labelEdits, notSpamEmailIds])
const resolveLabelVisual = useCallback(
(label: string) =>
resolveLabelPickerVisual(label, {
folderTree: sidebarNav.folderTree,
labelRows: sidebarNav.labelRows,
emailLabelToSidebarFolderId: sidebarNav.emailLabelToSidebarFolderId,
}),
[
sidebarNav.folderTree,
sidebarNav.labelRows,
sidebarNav.emailLabelToSidebarFolderId,
]
)
const resolveLabelCasing = useCallback(
(raw: string) => {
const t = raw.trim()
if (!t) return ""
const hit = catalogLabels.find((c) => c.toLowerCase() === t.toLowerCase())
return hit ?? t
},
[catalogLabels]
)
const addLabelToEmails = useCallback(
(ids: string[], label: string) => {
const resolved = resolveLabelCasing(label)
if (!resolved || ids.length === 0) return
sidebarNav.ensureLabelRowForLabelText(resolved)
setLabelEdits((prev) => {
const nextAdd = { ...prev.additions }
const nextRem = { ...prev.removals }
for (const id of ids) {
if (nextRem[id]?.length) {
nextRem[id] = nextRem[id].filter(
(x) => x.toLowerCase() !== resolved.toLowerCase()
)
if (nextRem[id].length === 0) delete nextRem[id]
}
const base = allEmails.find((e) => e.id === id)
const merged = effectiveLabels(base, nextAdd, nextRem)
if (merged.some((x) => x.toLowerCase() === resolved.toLowerCase())) {
continue
}
nextAdd[id] = [...(nextAdd[id] ?? []), resolved]
}
return { additions: nextAdd, removals: nextRem }
})
},
[resolveLabelCasing, allEmails, sidebarNav, setLabelEdits]
)
const getCatalogLabelPresence = useCallback(
(ids: string[], catalogLabel: string): CatalogLabelPresence => {
const resolved = resolveLabelCasing(catalogLabel)
if (!resolved || ids.length === 0) return "none"
const lc = resolved.toLowerCase()
let n = 0
for (const id of ids) {
const e = allEmails.find((x) => x.id === id)
const eff = effectiveLabels(e, labelEdits.additions, labelEdits.removals)
if (eff.some((l) => l.toLowerCase() === lc)) n++
}
if (n === 0) return "none"
if (n === ids.length) return "all"
return "some"
},
[allEmails, labelEdits, resolveLabelCasing]
)
const toggleLabelOnEmails = useCallback(
(ids: string[], label: string) => {
const resolved = resolveLabelCasing(label)
if (!resolved || ids.length === 0) return
setLabelEdits((prev) => {
const presence = (id: string) => {
const e = allEmails.find((x) => x.id === id)
if (!e) return false
return effectiveLabels(e, prev.additions, prev.removals).some(
(l) => l.toLowerCase() === resolved.toLowerCase()
)
}
const allHave = ids.every((id) => presence(id))
const nextAdd = { ...prev.additions }
const nextRem = { ...prev.removals }
if (allHave) {
for (const id of ids) {
if (nextAdd[id]?.length) {
const filtered = nextAdd[id].filter(
(l) => l.toLowerCase() !== resolved.toLowerCase()
)
if (filtered.length) nextAdd[id] = filtered
else delete nextAdd[id]
}
const e = allEmails.find((x) => x.id === id)
if (!e) continue
const still = effectiveLabels(e, nextAdd, nextRem).some(
(l) => l.toLowerCase() === resolved.toLowerCase()
)
if (still) {
const cur = nextRem[id] ?? []
if (!cur.some((l) => l.toLowerCase() === resolved.toLowerCase())) {
nextRem[id] = [...cur, resolved]
}
} else if (nextRem[id]?.length) {
const fr = nextRem[id].filter(
(l) => l.toLowerCase() !== resolved.toLowerCase()
)
if (fr.length) nextRem[id] = fr
else delete nextRem[id]
}
}
} else {
const anyMissing = ids.some((id) => !presence(id))
if (anyMissing) {
queueMicrotask(() => sidebarNav.ensureLabelRowForLabelText(resolved))
}
for (const id of ids) {
const e = allEmails.find((x) => x.id === id)
if (!e) continue
const had = effectiveLabels(e, prev.additions, prev.removals).some(
(l) => l.toLowerCase() === resolved.toLowerCase()
)
if (nextRem[id]?.length) {
const fr = nextRem[id].filter(
(l) => l.toLowerCase() !== resolved.toLowerCase()
)
if (fr.length) nextRem[id] = fr
else delete nextRem[id]
}
if (!had) {
if (!nextAdd[id]) nextAdd[id] = []
if (!nextAdd[id].some((l) => l.toLowerCase() === resolved.toLowerCase())) {
nextAdd[id] = [...nextAdd[id], resolved]
}
}
}
}
return { additions: nextAdd, removals: nextRem }
})
},
[allEmails, resolveLabelCasing, sidebarNav, setLabelEdits]
)
return {
collectAllFolderLabels,
moveEmailsToTarget,
catalogLabels,
resolveLabelVisual,
resolveLabelCasing,
addLabelToEmails,
toggleLabelOnEmails,
getCatalogLabelPresence,
}
}
export type EmailListLabels = ReturnType<typeof useEmailListLabels>