Some checks failed
E2E / Playwright e2e (push) Has been cancelled
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.
224 lines
7.4 KiB
TypeScript
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>
|