ultisuite-client/components/gmail/sidebar/use-sidebar-state.ts
2026-05-20 18:22:36 +02:00

382 lines
10 KiB
TypeScript

"use client"
import { useState, useRef, useEffect, useMemo } from "react"
import { useIsXs } from "@/hooks/use-xs"
import { useTouchNav } from "@/hooks/use-touch-nav"
import { readTouchNavMatches } from "@/hooks/use-touch-nav"
import { isSystemNavLabelId } from "@/lib/sidebar-nav-data"
import { useSidebarNav } from "@/lib/sidebar-nav-context"
import { ancestorFolderIdsForTarget } from "@/lib/sidebar-folder-tree-utils"
import {
mainItems,
CATEGORY_IDS_IN_PLUS_ONLY,
sortSystemLabelRows,
} from "@/components/gmail/sidebar/sidebar-nav-constants"
import { folderParentSelectOptions } from "@/components/gmail/sidebar/sidebar-nav-primitives"
import { useSidebarNavDrag } from "@/hooks/use-sidebar-nav-drag"
import {
MAIL_SIDEBAR_PANEL_SURFACE_CLASS,
MAIL_SIDEBAR_PANEL_SURFACE_MOBILE_CLASS,
} from "@/lib/mail-chrome-classes"
export interface SidebarProps {
selectedFolder: string
onSelectFolder: (folder: string) => void
collapsed: boolean
folderUnreadCounts?: Record<string, number>
splitView?: boolean
}
export function useSidebarState({
selectedFolder,
onSelectFolder,
collapsed,
folderUnreadCounts = {},
}: SidebarProps) {
const [hoverExpanded, setHoverExpanded] = useState(false)
const [navMoreOpen, setNavMoreOpen] = useState(false)
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => new Set())
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const sidebarRef = useRef<HTMLElement>(null)
const touchNav = useTouchNav()
const isXs = useIsXs()
const isExpanded = !collapsed || (!touchNav && hoverExpanded)
const isOverlayOpen = touchNav && !collapsed
const nav = useSidebarNav()
const {
folderTree,
labelRows,
folderIdToLabel,
addFolder,
addLabelRowFromSidebar,
getNavItemPrefs,
setNavItemSidebarVisibility,
setNavItemMessageVisibility,
updateFolderOrLabelColor,
renameFolderOrLabel,
removeFolderOrLabelRow,
moveFolder,
reorderLabelRows,
moveFolderRelative,
addSubfolder,
addChildLabelRow,
setLabelRowEnabled,
} = nav
const drag = useSidebarNavDrag({
reorderLabelRows,
moveFolderRelative,
setExpandedFolderIds,
})
const visibleNavLabelRows = useMemo(() => {
return labelRows.filter((row) => {
if (row.enabled === false) return false
if (isSystemNavLabelId(row.id)) return false
const p = getNavItemPrefs(row.id)
if (p.sidebar === "hide") return false
if (
p.sidebar === "showUnread" &&
(folderUnreadCounts[row.id] ?? 0) === 0
) {
return false
}
return true
})
}, [labelRows, getNavItemPrefs, folderUnreadCounts])
const validNavFolderIds = useMemo(() => {
const s = new Set<string>()
for (const i of mainItems) s.add(i.id)
for (const k of Object.keys(folderIdToLabel)) s.add(k)
return s
}, [folderIdToLabel])
useEffect(() => {
if (selectedFolder !== "search" && !validNavFolderIds.has(selectedFolder)) {
onSelectFolder("inbox")
}
}, [validNavFolderIds, selectedFolder, onSelectFolder])
const [folderDialogOpen, setFolderDialogOpen] = useState(false)
const [labelDialogOpen, setLabelDialogOpen] = useState(false)
const [newFolderName, setNewFolderName] = useState("")
const [newFolderParent, setNewFolderParent] = useState("__root__")
const [newLabelName, setNewLabelName] = useState("")
const newFolderNameInputRef = useRef<HTMLInputElement>(null)
const newLabelNameInputRef = useRef<HTMLInputElement>(null)
const folderParentOptions = useMemo(
() => folderParentSelectOptions(folderTree),
[folderTree]
)
const { primaryVisibleCategories, plusOnlyVisibleCategories } = useMemo(() => {
const systemEnabled = sortSystemLabelRows(
labelRows.filter((r) => r.enabled !== false && isSystemNavLabelId(r.id))
).map((r) => ({ id: r.id, label: r.label, icon: r.icon }))
return {
primaryVisibleCategories: systemEnabled.filter(
(c) => !CATEGORY_IDS_IN_PLUS_ONLY.has(c.id)
),
plusOnlyVisibleCategories: systemEnabled.filter((c) =>
CATEGORY_IDS_IN_PLUS_ONLY.has(c.id)
),
}
}, [labelRows])
const disabledSystemNavItems = useMemo(() => {
return sortSystemLabelRows(
labelRows.filter((r) => r.enabled === false && isSystemNavLabelId(r.id))
).map((r) => ({ id: r.id, label: r.label, icon: r.icon }))
}, [labelRows])
const visibleMainItems = useMemo(() => {
const scheduledTotal = folderUnreadCounts.scheduled ?? 0
if (scheduledTotal > 0) return mainItems
return mainItems.filter((item) => item.id !== "scheduled")
}, [folderUnreadCounts.scheduled])
const toggleFolderExpanded = (id: string) => {
setExpandedFolderIds((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const handleSubmitNewFolder = () => {
const name = newFolderName.trim()
if (!name) return
const parentId = newFolderParent === "__root__" ? null : newFolderParent
addFolder(parentId, name)
setNewFolderName("")
setFolderDialogOpen(false)
}
const handleSubmitNewLabel = () => {
const name = newLabelName.trim()
if (!name) return
addLabelRowFromSidebar(name)
setNewLabelName("")
setLabelDialogOpen(false)
}
useEffect(() => {
const row = labelRows.find((r) => r.id === selectedFolder)
if (row && row.enabled === false) {
onSelectFolder("inbox")
}
}, [labelRows, selectedFolder, onSelectFolder])
useEffect(() => {
if (selectedFolder !== "scheduled") return
if ((folderUnreadCounts.scheduled ?? 0) > 0) return
onSelectFolder("inbox")
}, [folderUnreadCounts.scheduled, selectedFolder, onSelectFolder])
useEffect(() => {
if (CATEGORY_IDS_IN_PLUS_ONLY.has(selectedFolder) && !navMoreOpen) {
setNavMoreOpen(true)
}
}, [selectedFolder, navMoreOpen])
useEffect(() => {
const ancestors = ancestorFolderIdsForTarget(folderTree, selectedFolder)
if (ancestors?.length) {
setExpandedFolderIds((prev) => {
const next = new Set(prev)
ancestors.forEach((id) => next.add(id))
return next
})
}
}, [selectedFolder, folderTree])
const handleMouseEnter = () => {
if (readTouchNavMatches()) return
if (collapsed) {
hoverTimeoutRef.current = setTimeout(() => {
setHoverExpanded(true)
}, 300)
}
}
const handleMouseLeave = () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current)
hoverTimeoutRef.current = null
}
if (readTouchNavMatches()) return
setHoverExpanded(false)
}
useEffect(() => {
if (touchNav) setHoverExpanded(false)
}, [touchNav, collapsed])
useEffect(() => {
return () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current)
}
}
}, [])
const navRailInset = "pr-3.5"
const splitViewLogoIconClass = "size-9 shrink-0"
const splitViewLogoHeaderClass =
"box-border min-h-[80px] pt-3 pl-4 pr-3.5 pb-4"
const navDragBindings = useMemo(
() => ({
navDragRef: drag.navDragRef,
navDropPlacementRef: drag.navDropPlacementRef,
beginNavDrag: drag.beginNavDrag,
clearNavDrag: drag.clearNavDrag,
updateNavDropTarget: drag.updateNavDropTarget,
clearNavDropTarget: drag.clearNavDropTarget,
commitNavDrop: drag.commitNavDrop,
}),
[drag]
)
const folderRowProps = useMemo(
() => ({
...navDragBindings,
selectedFolder,
folderUnreadCounts,
expandedFolderIds,
isExpanded,
isOverlayOpen,
touchNav,
folderTree,
onSelectFolder,
toggleFolderExpanded,
getNavItemPrefs,
setNavItemSidebarVisibility,
setNavItemMessageVisibility,
updateFolderOrLabelColor,
renameFolderOrLabel,
removeFolderOrLabelRow,
moveFolder,
addSubfolder,
}),
[
navDragBindings,
selectedFolder,
folderUnreadCounts,
expandedFolderIds,
isExpanded,
isOverlayOpen,
touchNav,
folderTree,
onSelectFolder,
getNavItemPrefs,
setNavItemSidebarVisibility,
setNavItemMessageVisibility,
updateFolderOrLabelColor,
renameFolderOrLabel,
removeFolderOrLabelRow,
moveFolder,
addSubfolder,
]
)
const collapsedFolderOpts = useMemo(
() => ({
getNavItemPrefs,
folderUnreadCounts,
expandedFolderIds,
isExpanded,
selectedFolder,
onSelectFolder,
}),
[
getNavItemPrefs,
folderUnreadCounts,
expandedFolderIds,
isExpanded,
selectedFolder,
onSelectFolder,
]
)
const labelRowProps = useMemo(
() => ({
...navDragBindings,
selectedFolder,
touchNav,
onSelectFolder,
getNavItemPrefs,
setNavItemSidebarVisibility,
setNavItemMessageVisibility,
updateFolderOrLabelColor,
renameFolderOrLabel,
removeFolderOrLabelRow,
addChildLabelRow,
}),
[
navDragBindings,
selectedFolder,
touchNav,
onSelectFolder,
getNavItemPrefs,
setNavItemSidebarVisibility,
setNavItemMessageVisibility,
updateFolderOrLabelColor,
renameFolderOrLabel,
removeFolderOrLabelRow,
addChildLabelRow,
]
)
const panelSurfaceClass =
isOverlayOpen && isXs
? MAIL_SIDEBAR_PANEL_SURFACE_MOBILE_CLASS
: MAIL_SIDEBAR_PANEL_SURFACE_CLASS
return {
sidebarRef,
touchNav,
hoverExpanded,
isExpanded,
isOverlayOpen,
panelSurfaceClass,
navRailInset,
splitViewLogoIconClass,
splitViewLogoHeaderClass,
handleMouseEnter,
handleMouseLeave,
navMoreOpen,
setNavMoreOpen,
visibleMainItems,
primaryVisibleCategories,
plusOnlyVisibleCategories,
disabledSystemNavItems,
folderTree,
folderRowProps,
collapsedFolderOpts,
visibleNavLabelRows,
labelRowProps,
setLabelRowEnabled,
folderDialogOpen,
setFolderDialogOpen,
labelDialogOpen,
setLabelDialogOpen,
newFolderName,
setNewFolderName,
newFolderParent,
setNewFolderParent,
newLabelName,
setNewLabelName,
newFolderNameInputRef,
newLabelNameInputRef,
folderParentOptions,
handleSubmitNewFolder,
handleSubmitNewLabel,
}
}