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

224 lines
7.4 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 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,
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 = email?.labels ?? []
if (isSystemTarget) {
if (targetId === "inbox") {
for (const lab of currentLabels) {
if (allFolderLabels.has(lab.toLowerCase())) {
const cur = nextRem[id] ?? []
if (!cur.some((l: string) => l.toLowerCase() === lab.toLowerCase())) {
nextRem[id] = [...cur, lab]
}
}
}
}
} 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: string) => l.toLowerCase() === lab.toLowerCase())) {
nextRem[id] = [...cur, lab]
}
}
}
if (!currentLabels.some((l) => l.toLowerCase() === folderLabel.toLowerCase())) {
nextAdd[id] = [...(nextAdd[id] ?? []), folderLabel]
}
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) {
for (const lab of e.labels ?? []) {
if (!LABEL_PICKER_EXCLUDE.has(lab)) s.add(lab)
}
}
return [...s].sort((a, b) => a.localeCompare(b, "fr"))
}, [sidebarNav.folderTree, sidebarNav.labelRows, allEmails])
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) {
const base = allEmails.find((e) => e.id === id)
const currentLabels = base?.labels ?? []
if (currentLabels.some((x: string) => 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 labels = e?.labels ?? []
if (labels.some((l: string) => l.toLowerCase() === lc)) n++
}
if (n === 0) return "none"
if (n === ids.length) return "all"
return "some"
},
[allEmails, 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 (e.labels ?? []).some(
(l: string) => 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) {
nextRem[id] = [...(nextRem[id] ?? []), resolved]
}
} else {
const anyMissing = ids.some((id) => !presence(id))
if (anyMissing) {
queueMicrotask(() => sidebarNav.ensureLabelRowForLabelText(resolved))
}
for (const id of ids) {
if (!presence(id)) {
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>