"use client" import { create } from "zustand" import { persist } from "zustand/middleware" import { cloneDefaultFolderTree, cloneDefaultLabelRows, type FolderTreeNode, type LabelRowItem, } from "@/lib/sidebar-nav-data" import { collectFolderIdsInTree, proposeNewFolderId, rekeyFolderSubtreeAt, slugifyNavSegment, } from "@/lib/sidebar-nav-folder-ids" import { buildEmailLabelToSidebarFolderId, buildFolderIdToLabelRecord, } from "@/lib/sidebar-nav-maps" export type LabelListSidebarVisibility = "show" | "showUnread" | "hide" export type LabelInMessageListVisibility = "show" | "hide" export type NavItemPrefs = { sidebar?: LabelListSidebarVisibility messages?: LabelInMessageListVisibility } type NavStoreState = { folderTree: FolderTreeNode[] labelRows: LabelRowItem[] navItemPrefs: Record } function newLabelRowId(label: string): string { const slug = slugifyNavSegment(label) return `lbl-${slug || "libelle"}` } function allLabelTextsLower(tree: FolderTreeNode[], rows: LabelRowItem[]): Set { const s = new Set() const walk = (nodes: FolderTreeNode[]) => { for (const n of nodes) { s.add(n.label.toLowerCase()) if (n.children?.length) walk(n.children) } } walk(tree) for (const r of rows) s.add(r.label.toLowerCase()) return s } function insertFolderChild( nodes: FolderTreeNode[], parentId: string | null, child: FolderTreeNode ): FolderTreeNode[] { if (parentId === null) return [...nodes, child] return nodes.map((n) => { if (n.id === parentId) return { ...n, children: [...(n.children ?? []), child] } if (n.children?.length) return { ...n, children: insertFolderChild(n.children, parentId, child) } return n }) } function uniqueLabelRowId(base: string, existingIds: Set): string { let id = base let n = 0 while (existingIds.has(id)) { n += 1 id = `${base}-${n}` } return id } function findNodeInTree(nodes: FolderTreeNode[], id: string): FolderTreeNode | null { for (const n of nodes) { if (n.id === id) return n if (n.children?.length) { const hit = findNodeInTree(n.children, id) if (hit) return hit } } return null } function collectSubtreeIds(node: FolderTreeNode): Set { const s = new Set([node.id]) if (node.children?.length) { for (const c of node.children) { for (const x of collectSubtreeIds(c)) s.add(x) } } return s } function isDescendantOf(tree: FolderTreeNode[], maybeDescendantId: string, ancestorId: string): boolean { const anc = findNodeInTree(tree, ancestorId) if (!anc) return false return collectSubtreeIds(anc).has(maybeDescendantId) } function extractNode( nodes: FolderTreeNode[], id: string ): { next: FolderTreeNode[]; extracted: FolderTreeNode | null } { for (let i = 0; i < nodes.length; i++) { const n = nodes[i] if (n.id === id) { return { next: [...nodes.slice(0, i), ...nodes.slice(i + 1)], extracted: n } } if (n.children?.length) { const inner = extractNode(n.children, id) if (inner.extracted) { return { next: nodes.map((x, j) => (j === i ? { ...x, children: inner.next } : x)), extracted: inner.extracted, } } } } return { next: nodes, extracted: null } } function updateNodeInTree( nodes: FolderTreeNode[], id: string, patch: Partial> ): FolderTreeNode[] { return nodes.map((n) => { if (n.id === id) return { ...n, ...patch } if (n.children?.length) return { ...n, children: updateNodeInTree(n.children, id, patch) } return n }) } function remapNavItemPrefs( prefs: Record, idMap: Record ): Record { if (Object.keys(idMap).length === 0) return prefs const next = { ...prefs } for (const [oldId, newId] of Object.entries(idMap)) { const slice = next[oldId] if (!slice) continue delete next[oldId] next[newId] = { ...next[newId], ...slice } } return next } type NavStoreActions = { setNavItemSidebarVisibility: (id: string, v: LabelListSidebarVisibility) => void setNavItemMessageVisibility: (id: string, v: LabelInMessageListVisibility) => void ensureLabelRowForLabelText: (raw: string) => void addLabelRowFromSidebar: (raw: string) => void addFolder: (parentId: string | null, name: string) => void addSubfolder: (parentId: string, name: string) => void updateFolderOrLabelColor: (id: string, color: string) => void renameFolderOrLabel: (id: string, newLabel: string) => { idMap: Record; emailRename: { from: string; to: string } | null } removeFolderOrLabelRow: (id: string) => string[] moveFolder: (id: string, newParentId: string | null) => Record addChildLabelRow: (parentLabelRowId: string, childName: string) => void /** Derived selectors */ getFolderIdToLabel: () => Record getEmailLabelToSidebarFolderId: () => Record getNavItemPrefs: (id: string) => Required> } export const useNavStore = create()( persist( (set, get) => ({ folderTree: cloneDefaultFolderTree(), labelRows: cloneDefaultLabelRows(), navItemPrefs: {}, setNavItemSidebarVisibility: (id, v) => set((s) => ({ navItemPrefs: { ...s.navItemPrefs, [id]: { ...s.navItemPrefs[id], sidebar: v } }, })), setNavItemMessageVisibility: (id, v) => set((s) => ({ navItemPrefs: { ...s.navItemPrefs, [id]: { ...s.navItemPrefs[id], messages: v } }, })), ensureLabelRowForLabelText: (raw) => { const label = raw.trim() if (!label) return set((s) => { const known = allLabelTextsLower(s.folderTree, s.labelRows) if (known.has(label.toLowerCase())) return s const ids = new Set(s.labelRows.map((r) => r.id)) const id = uniqueLabelRowId(newLabelRowId(label), ids) return { labelRows: [...s.labelRows, { id, label, color: "bg-gray-500" }] } }) }, addLabelRowFromSidebar: (raw) => { get().ensureLabelRowForLabelText(raw) }, addFolder: (parentId, name) => { const label = name.trim() if (!label) return set((s) => { const fid = proposeNewFolderId( s.folderTree, s.labelRows.map((r) => r.id), parentId, label ) const node: FolderTreeNode = { id: fid, label, color: "bg-slate-400" } return { folderTree: insertFolderChild(s.folderTree, parentId, node) } }) }, addSubfolder: (parentId, name) => { get().addFolder(parentId, name) }, updateFolderOrLabelColor: (id, color) => set((s) => { const inRow = s.labelRows.some((r) => r.id === id) if (inRow) { return { labelRows: s.labelRows.map((r) => (r.id === id ? { ...r, color } : r)) } } return { folderTree: updateNodeInTree(s.folderTree, id, { color }) } }), renameFolderOrLabel: (id, raw) => { const nextLabel = raw.trim() if (!nextLabel) return { idMap: {}, emailRename: null } const snap = get() const snapMap = buildFolderIdToLabelRecord(snap.folderTree, snap.labelRows) const oldLabelForSync = snapMap[id] const emailRename = oldLabelForSync && oldLabelForSync !== nextLabel ? { from: oldLabelForSync, to: nextLabel } : null let resultIdMap: Record = {} set((prev) => { const inRow = prev.labelRows.some((r) => r.id === id) let nextTree = prev.folderTree let nextRows = prev.labelRows let navItemPrefs = prev.navItemPrefs if (inRow) { const usedIds = new Set([ ...collectFolderIdsInTree(prev.folderTree), ...prev.labelRows.filter((r) => r.id !== id).map((r) => r.id), ]) const newRowId = uniqueLabelRowId(newLabelRowId(nextLabel), usedIds) const rowMap: Record = newRowId !== id ? { [id]: newRowId } : {} nextRows = prev.labelRows.map((r) => r.id === id ? { ...r, id: newRowId, label: nextLabel } : r ) if (Object.keys(rowMap).length > 0) { navItemPrefs = remapNavItemPrefs(prev.navItemPrefs, rowMap) resultIdMap = rowMap } } else { nextTree = updateNodeInTree(prev.folderTree, id, { label: nextLabel }) const rk = rekeyFolderSubtreeAt(nextTree, id, prev.labelRows.map((r) => r.id)) const idMap = rk?.idMap ?? {} if (rk && Object.keys(idMap).length > 0) { nextTree = rk.tree navItemPrefs = remapNavItemPrefs(prev.navItemPrefs, idMap) resultIdMap = idMap } } return { folderTree: nextTree, labelRows: nextRows, navItemPrefs } }) return { idMap: resultIdMap, emailRename } }, removeFolderOrLabelRow: (id) => { const snap = get() const snapMap = buildFolderIdToLabelRecord(snap.folderTree, snap.labelRows) const row = snap.labelRows.find((r) => r.id === id) let labelsToRemove: string[] = [] if (row) { const lab = snapMap[id] if (lab) labelsToRemove = [lab] } else { const extracted = extractNode(snap.folderTree, id) if (extracted.extracted) { const uniq = new Set() const collectLabs = (n: FolderTreeNode) => { uniq.add(n.label) n.children?.forEach(collectLabs) } collectLabs(extracted.extracted) labelsToRemove = [...uniq] } } set((prev) => { const rowInner = prev.labelRows.find((r) => r.id === id) if (rowInner) { return { labelRows: prev.labelRows.filter((r) => r.id !== id), navItemPrefs: Object.fromEntries( Object.entries(prev.navItemPrefs).filter(([k]) => k !== id) ), } } const extracted = extractNode(prev.folderTree, id) if (extracted.extracted) { const dropIds = collectSubtreeIds(extracted.extracted) return { folderTree: extracted.next, navItemPrefs: Object.fromEntries( Object.entries(prev.navItemPrefs).filter(([k]) => !dropIds.has(k)) ), } } return prev }) return labelsToRemove }, moveFolder: (id, newParentId) => { let resultIdMap: Record = {} set((prev) => { if (newParentId !== null) { if (newParentId === id) return prev if (isDescendantOf(prev.folderTree, newParentId, id)) return prev } const ex = extractNode(prev.folderTree, id) if (!ex.extracted) return prev let tree = ex.next tree = insertFolderChild(tree, newParentId, ex.extracted) const movedOldId = ex.extracted.id const rk = rekeyFolderSubtreeAt(tree, movedOldId, prev.labelRows.map((r) => r.id)) const idMap = rk?.idMap ?? {} let navItemPrefs = prev.navItemPrefs if (rk && Object.keys(idMap).length > 0) { tree = rk.tree navItemPrefs = remapNavItemPrefs(prev.navItemPrefs, idMap) resultIdMap = idMap } return { folderTree: tree, navItemPrefs } }) return resultIdMap }, addChildLabelRow: (parentLabelRowId, childName) => { const sub = childName.trim() if (!sub) return set((s) => { const parent = s.labelRows.find((r) => r.id === parentLabelRowId) if (!parent) return s const combined = `${parent.label}/${sub}` const known = allLabelTextsLower(s.folderTree, s.labelRows) if (known.has(combined.toLowerCase())) return s const ids = new Set(s.labelRows.map((r) => r.id)) const nid = uniqueLabelRowId(newLabelRowId(combined), ids) return { labelRows: [...s.labelRows, { id: nid, label: combined, color: parent.color ?? "bg-gray-500" }], } }) }, getFolderIdToLabel: () => { const s = get() return buildFolderIdToLabelRecord(s.folderTree, s.labelRows) }, getEmailLabelToSidebarFolderId: () => { const s = get() const fToL = buildFolderIdToLabelRecord(s.folderTree, s.labelRows) return buildEmailLabelToSidebarFolderId(fToL) }, getNavItemPrefs: (id) => { const p = get().navItemPrefs[id] return { sidebar: p?.sidebar ?? "show", messages: p?.messages ?? "show" } }, }), { name: "ultimail-nav-state", version: 1, } ) )