382 lines
10 KiB
TypeScript
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,
|
|
}
|
|
}
|