"use client" import { createContext, useCallback, useContext, useEffect, useMemo, useRef, type ReactNode, } from "react" import type { FolderTreeNode, LabelRowItem } from "@/lib/sidebar-nav-data" import { isSystemNavLabelId } from "@/lib/sidebar-nav-data" import { useNavStore, type LabelListSidebarVisibility, type LabelInMessageListVisibility, type NavItemPrefs, } from "@/lib/stores/nav-store" import { buildEmailLabelToSidebarFolderId, buildFolderIdToLabelRecord, } from "@/lib/sidebar-nav-maps" import { useSidebarNavApiSync } from "@/lib/hooks/use-sidebar-nav-api" import { isServerNavId } from "@/lib/sidebar-nav-server-id" import { DEFAULT_NAV_COLOR } from "@/lib/nav-color" import { findFolderNodeById } from "@/lib/sidebar-folder-tree-utils" import { folderTreeToReorderItems, labelRowsToReorderItems, } from "@/lib/nav-reorder-plan" export type { LabelListSidebarVisibility, LabelInMessageListVisibility, NavItemPrefs } export type NavEmailSync = { renameLabel: (from: string, to: string) => void removeLabel: (label: string) => void } const navEmailSyncRef = { current: null as NavEmailSync | null } export function registerNavEmailSync(cb: NavEmailSync | null) { navEmailSyncRef.current = cb } type SidebarNavContextValue = { folderTree: FolderTreeNode[] labelRows: LabelRowItem[] folderIdToLabel: Record emailLabelToSidebarFolderId: Record getNavItemPrefs: (id: string) => Required> setNavItemSidebarVisibility: (id: string, v: LabelListSidebarVisibility) => void setNavItemMessageVisibility: (id: string, v: LabelInMessageListVisibility) => void ensureLabelRowForLabelText: (label: string) => void addLabelRowFromSidebar: (label: string, color?: string) => void addFolder: (parentId: string | null, name: string) => void updateFolderOrLabelColor: (id: string, color: string) => void renameFolderOrLabel: (id: string, newLabel: string) => void removeFolderOrLabelRow: (id: string) => void moveFolder: (id: string, newParentId: string | null) => void reorderLabelRows: ( draggedId: string, targetId: string, placement: "before" | "after" ) => void moveFolderRelative: ( draggedId: string, targetId: string, placement: "before" | "after" | "inside" ) => void addSubfolder: (parentId: string, name: string) => void addChildLabelRow: (parentLabelRowId: string, childName: string) => void setLabelRowEnabled: (id: string, enabled: boolean) => void } const SidebarNavContext = createContext(null) type SidebarNavProviderProps = { children: ReactNode routeFolderId?: string | null onRouteFolderIdChange?: (nextFolderId: string) => void } export function SidebarNavProvider({ children, routeFolderId, onRouteFolderIdChange, }: SidebarNavProviderProps) { const routeSyncRef = useRef({ routeFolderId: null as string | null | undefined, onRouteFolderIdChange: undefined as | ((nextFolderId: string) => void) | undefined, }) useEffect(() => { routeSyncRef.current = { routeFolderId, onRouteFolderIdChange } }, [routeFolderId, onRouteFolderIdChange]) const navApi = useSidebarNavApiSync() const scheduleRouteFolderIdSync = useCallback( (idMap: Record) => { if (Object.keys(idMap).length === 0) return queueMicrotask(() => { const { routeFolderId: rid, onRouteFolderIdChange: on } = routeSyncRef.current if (!rid || !on) return const next = idMap[rid] if (next) on(next) }) }, [] ) const folderTree = useNavStore((s) => s.folderTree) const labelRows = useNavStore((s) => s.labelRows) const navItemPrefs = useNavStore((s) => s.navItemPrefs) const navActions = useNavStore.getState() const folderIdToLabel = useMemo( () => buildFolderIdToLabelRecord(folderTree, labelRows), [folderTree, labelRows] ) const emailLabelToSidebarFolderId = useMemo( () => buildEmailLabelToSidebarFolderId(folderIdToLabel), [folderIdToLabel] ) const getNavItemPrefs = useCallback( (id: string): Required> => { const p = navItemPrefs[id] return { sidebar: p?.sidebar ?? "show", messages: p?.messages ?? "show", } }, [navItemPrefs] ) const isFolderId = useCallback( (id: string) => findFolderNodeById(folderTree, id) != null, [folderTree] ) const getLabelRow = useCallback( (id: string) => labelRows.find((r) => r.id === id), [labelRows] ) const addFolder = useCallback( (parentId: string | null, name: string) => { if (navApi.apiEnabled) { navApi.createFolder({ name, color: "bg-slate-400", parent_id: parentId ?? undefined, }) return } navActions.addFolder(parentId, name) }, [navApi] ) const addSubfolder = useCallback( (parentId: string, name: string) => { addFolder(parentId, name) }, [addFolder] ) const addLabelRowFromSidebar = useCallback( (label: string, color = DEFAULT_NAV_COLOR) => { if (navApi.apiEnabled) { navApi.createLabel({ name: label.trim(), color }) return } navActions.addLabelRowFromSidebar(label) }, [navApi] ) const addChildLabelRow = useCallback( (parentLabelRowId: string, childName: string) => { const parent = getLabelRow(parentLabelRowId) const child = childName.trim() if (!child) return const fullName = parent ? `${parent.label}/${child}` : child if (navApi.apiEnabled) { navApi.createLabel({ name: fullName, color: parent?.color ?? DEFAULT_NAV_COLOR, }) return } navActions.addChildLabelRow(parentLabelRowId, childName) }, [getLabelRow, navApi] ) const updateFolderOrLabelColor = useCallback( (id: string, color: string) => { if (navApi.apiEnabled && isServerNavId(id)) { if (isFolderId(id)) { const node = findFolderNodeById(folderTree, id) if (node) { navApi.updateFolder({ id, name: node.label, color, parent_id: undefined, }) return } } const row = getLabelRow(id) if (row && !isSystemNavLabelId(id)) { navApi.updateLabel({ id, name: row.label, color }) return } } navActions.updateFolderOrLabelColor(id, color) }, [folderTree, getLabelRow, isFolderId, navApi] ) const renameFolderOrLabel = useCallback( (id: string, newLabel: string) => { if (navApi.apiEnabled && isServerNavId(id)) { if (isFolderId(id)) { const node = findFolderNodeById(folderTree, id) if (node) { navApi.updateFolder({ id, name: newLabel.trim(), color: node.color ?? "bg-slate-400", }) return } } const row = getLabelRow(id) if (row && !isSystemNavLabelId(id)) { const oldLabel = row.label navApi.updateLabel({ id, name: newLabel.trim(), color: row.color }) queueMicrotask(() => { navEmailSyncRef.current?.renameLabel(oldLabel, newLabel.trim()) }) return } } const { idMap, emailRename } = useNavStore.getState().renameFolderOrLabel(id, newLabel) scheduleRouteFolderIdSync(idMap) if (emailRename) { queueMicrotask(() => { navEmailSyncRef.current?.renameLabel(emailRename.from, emailRename.to) }) } }, [folderTree, getLabelRow, isFolderId, navApi, scheduleRouteFolderIdSync] ) const removeFolderOrLabelRow = useCallback( (id: string) => { if (navApi.apiEnabled && isServerNavId(id)) { if (isFolderId(id)) { navApi.deleteFolder(id) return } const row = getLabelRow(id) if (row && !isSystemNavLabelId(id)) { navApi.deleteLabel(id) queueMicrotask(() => { navEmailSyncRef.current?.removeLabel(row.label) }) return } } const labelsToRemove = useNavStore.getState().removeFolderOrLabelRow(id) if (labelsToRemove.length > 0) { queueMicrotask(() => { for (const lab of labelsToRemove) { navEmailSyncRef.current?.removeLabel(lab) } }) } }, [getLabelRow, isFolderId, navApi] ) const syncLabelOrder = useCallback(() => { if (!navApi.apiEnabled) return const items = labelRowsToReorderItems(useNavStore.getState().labelRows) navApi.reorderLabels(items) }, [navApi]) const syncFolderOrder = useCallback(() => { if (!navApi.apiEnabled) return const items = folderTreeToReorderItems(useNavStore.getState().folderTree) navApi.reorderFolders(items) }, [navApi]) const moveFolder = useCallback( (id: string, newParentId: string | null) => { if (navApi.apiEnabled && isServerNavId(id)) { const node = findFolderNodeById(folderTree, id) if (node) { useNavStore.getState().moveFolder(id, newParentId) queueMicrotask(syncFolderOrder) return } } const idMap = useNavStore.getState().moveFolder(id, newParentId) scheduleRouteFolderIdSync(idMap) }, [folderTree, navApi, scheduleRouteFolderIdSync, syncFolderOrder] ) const moveFolderRelative = useCallback( ( draggedId: string, targetId: string, placement: "before" | "after" | "inside" ) => { if (navApi.apiEnabled && isServerNavId(draggedId)) { const idMap = useNavStore .getState() .moveFolderRelative(draggedId, targetId, placement) scheduleRouteFolderIdSync(idMap) queueMicrotask(syncFolderOrder) return } const idMap = useNavStore .getState() .moveFolderRelative(draggedId, targetId, placement) scheduleRouteFolderIdSync(idMap) }, [navApi, scheduleRouteFolderIdSync, syncFolderOrder] ) const reorderLabelRows = useCallback( ( draggedId: string, targetId: string, placement: "before" | "after" ) => { useNavStore.getState().reorderLabelRows(draggedId, targetId, placement) queueMicrotask(syncLabelOrder) }, [syncLabelOrder] ) const value = useMemo( () => ({ folderTree, labelRows, folderIdToLabel, emailLabelToSidebarFolderId, getNavItemPrefs, setNavItemSidebarVisibility: navActions.setNavItemSidebarVisibility, setNavItemMessageVisibility: navActions.setNavItemMessageVisibility, ensureLabelRowForLabelText: navActions.ensureLabelRowForLabelText, addLabelRowFromSidebar, addFolder, updateFolderOrLabelColor, renameFolderOrLabel, removeFolderOrLabelRow, moveFolder, reorderLabelRows, moveFolderRelative, addSubfolder, addChildLabelRow, setLabelRowEnabled: navActions.setLabelRowEnabled, }), [ folderTree, labelRows, folderIdToLabel, emailLabelToSidebarFolderId, getNavItemPrefs, navActions, addLabelRowFromSidebar, addFolder, updateFolderOrLabelColor, renameFolderOrLabel, removeFolderOrLabelRow, moveFolder, reorderLabelRows, moveFolderRelative, addSubfolder, addChildLabelRow, ] ) return ( {children} ) } export function useSidebarNav(): SidebarNavContextValue { const ctx = useContext(SidebarNavContext) if (!ctx) { throw new Error("useSidebarNav must be used within SidebarNavProvider") } return ctx } export function folderMoveParentOptions( tree: FolderTreeNode[], excludeId: string ): { value: string; label: string; depth: number }[] { function findNode(nodes: FolderTreeNode[], id: string): FolderTreeNode | null { for (const n of nodes) { if (n.id === id) return n if (n.children?.length) { const hit = findNode(n.children, id) if (hit) return hit } } return null } function collectIds(node: FolderTreeNode): Set { const s = new Set([node.id]) if (node.children?.length) { for (const c of node.children) { for (const x of collectIds(c)) s.add(x) } } return s } const ex = findNode(tree, excludeId) const banned = ex ? collectIds(ex) : new Set([excludeId]) const out: { value: string; label: string; depth: number }[] = [ { value: "__root__", label: "Racine", depth: 0 }, ] const walk = (nodes: FolderTreeNode[], depth: number) => { for (const n of nodes) { if (banned.has(n.id)) continue out.push({ value: n.id, label: `${"\u2003".repeat(depth * 2)}${n.label}`, depth, }) if (n.children?.length) walk(n.children, depth + 1) } } walk(tree, 0) return out }