ultisuite-client/app/mail/mail-app-shell.tsx
R3D347HR4Y ae54fa29e4 Hehe
2026-05-18 17:47:32 +02:00

248 lines
8.1 KiB
TypeScript

"use client"
import {
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
type CSSProperties,
} from "react"
import { useIsXs } from "@/hooks/use-xs"
import { readTouchNavMatches, useTouchNav } from "@/hooks/use-touch-nav"
import { useMailSplitView } from "@/hooks/use-mail-split-view"
import { MobileBottomBar } from "@/components/gmail/mobile-bottom-bar"
import { Toaster } from "sonner"
import { useRouter, usePathname } from "next/navigation"
import { Sidebar } from "@/components/gmail/sidebar"
import { Header } from "@/components/gmail/header"
import { EmailList } from "@/components/gmail/email-list"
import { RightPanel } from "@/components/gmail/right-panel"
import { ContactsPanel } from "@/components/gmail/contacts/contacts-panel"
import { EmailDragProvider } from "@/lib/drag-context"
import { MoveDragIndicator } from "@/components/gmail/move-drag-indicator"
import { ComposeProvider } from "@/lib/compose-context"
import { ScheduledMailProvider } from "@/lib/scheduled-mail-context"
import { ComposeModalManager } from "@/components/gmail/compose-modal"
import { SidebarNavProvider } from "@/lib/sidebar-nav-context"
import { mailNavVisitKey } from "@/lib/mail-folder-display"
import { useMailStore } from "@/lib/stores/mail-store"
import {
parseMailSegments,
buildMailPath,
DEFAULT_INBOX_TAB,
type MailRouteState,
} from "@/lib/mail-url"
import { cn } from "@/lib/utils"
function segmentsFromPathname(pathname: string | null): string[] | undefined {
if (!pathname?.startsWith("/mail")) return undefined
const rest = pathname.slice("/mail".length).replace(/^\//, "")
if (!rest) return []
return rest.split("/").filter(Boolean)
}
function MailAppInner() {
const router = useRouter()
const pathname = usePathname()
const segments = useMemo(() => segmentsFromPathname(pathname), [pathname])
const route = useMemo(() => parseMailSegments(segments), [segments])
const isXs = useIsXs()
const touchNav = useTouchNav()
const splitView = useMailSplitView()
const pushRecentFolderVisit = useMailStore((s) => s.pushRecentFolderVisit)
/** Start closed so narrow viewports match SSR/CSS before JS runs; desktop opens in layout. */
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
useLayoutEffect(() => {
if (!readTouchNavMatches()) setSidebarCollapsed(false)
}, [])
useEffect(() => {
if (isXs) setSidebarCollapsed(true)
}, [isXs])
useEffect(() => {
pushRecentFolderVisit(mailNavVisitKey(route.folderId, route.inboxTab))
}, [route.folderId, route.inboxTab, pushRecentFolderVisit])
const [folderUnreadCounts, setFolderUnreadCounts] = useState<
Record<string, number>
>({})
const navigateRoute = useCallback(
(patch: Partial<MailRouteState>) => {
const next: MailRouteState = {
folderId: patch.folderId ?? route.folderId,
inboxTab:
patch.inboxTab !== undefined && patch.inboxTab !== null
? patch.inboxTab
: route.inboxTab,
page: patch.page !== undefined ? patch.page : route.page,
mailId: patch.mailId !== undefined ? patch.mailId : route.mailId,
}
router.push(buildMailPath(next), { scroll: false })
},
[router, route]
)
const handleSelectFolder = useCallback(
(id: string) => {
navigateRoute({
folderId: id,
inboxTab: DEFAULT_INBOX_TAB,
page: 1,
mailId: null,
})
if (readTouchNavMatches()) setSidebarCollapsed(true)
},
[navigateRoute]
)
return (
<SidebarNavProvider
routeFolderId={route.folderId}
onRouteFolderIdChange={(nextFolderId) =>
navigateRoute({
folderId: nextFolderId,
inboxTab: DEFAULT_INBOX_TAB,
page: 1,
mailId: null,
})
}
>
<div className="ultimail-app flex h-dvh max-h-dvh flex-col overflow-hidden bg-app-canvas">
{!splitView ? (
<div className="hidden sm:block">
<Header
isXs={false}
sidebarCollapsed={sidebarCollapsed || touchNav}
onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)}
/>
</div>
) : null}
<div
className={cn(
"relative flex min-h-0 flex-1 gap-0 overflow-hidden pl-0 pr-0",
splitView ? "bg-white p-0" : "bg-app-canvas pb-1 pt-1 sm:gap-1 sm:pl-1"
)}
>
{!sidebarCollapsed && touchNav && (
<button
type="button"
aria-label="Fermer le menu"
className="absolute inset-0 z-30 bg-black/20"
onClick={() => setSidebarCollapsed(true)}
/>
)}
<div
className={
touchNav && isXs
? "w-0 shrink-0"
: touchNav || sidebarCollapsed
? "w-0 shrink-0 sm:w-[68px]"
: "w-0 shrink-0 sm:w-60"
}
/>
<Sidebar
selectedFolder={route.folderId}
onSelectFolder={handleSelectFolder}
collapsed={sidebarCollapsed}
folderUnreadCounts={folderUnreadCounts}
splitView={splitView}
/>
<main
className={cn(
"flex min-h-0 flex-1 flex-col overflow-hidden bg-white",
splitView ? "rounded-none shadow-none" : "rounded-none shadow-sm sm:rounded-2xl"
)}
>
<Suspense>
<EmailList
selectedFolder={route.folderId}
inboxTab={route.inboxTab}
listPage={route.page}
openMailId={route.mailId}
splitView={splitView}
onToggleSidebar={() => setSidebarCollapsed((c) => !c)}
onMailRouteNavigate={navigateRoute}
onSelectFolder={handleSelectFolder}
onFolderUnreadCountsChange={setFolderUnreadCounts}
/>
</Suspense>
</main>
<div
className={cn(
"flex shrink-0 flex-col",
splitView && "border-l border-gray-200"
)}
>
<RightPanel />
</div>
<ContactsPanel />
</div>
{!splitView ? (
<MobileBottomBar
sidebarOpen={!sidebarCollapsed}
onToggleSidebar={() => setSidebarCollapsed((c) => !c)}
/>
) : null}
</div>
</SidebarNavProvider>
)
}
export function MailAppShell({
children: _routeOutlet,
}: {
children: React.ReactNode
}) {
useEffect(() => {
const blockPinch = (event: Event) => event.preventDefault()
document.addEventListener("gesturestart", blockPinch, { passive: false })
document.addEventListener("gesturechange", blockPinch, { passive: false })
document.addEventListener("gestureend", blockPinch, { passive: false })
return () => {
document.removeEventListener("gesturestart", blockPinch)
document.removeEventListener("gesturechange", blockPinch)
document.removeEventListener("gestureend", blockPinch)
}
}, [])
return (
<ComposeProvider>
<ScheduledMailProvider>
<EmailDragProvider>
<Suspense
fallback={
<div className="flex h-dvh max-h-dvh flex-col overflow-hidden bg-app-canvas">
<div className="hidden h-14 shrink-0 border-b border-gray-200 bg-white sm:block" />
<div className="min-h-0 flex-1 bg-app-canvas" />
</div>
}
>
<MailAppInner />
</Suspense>
<MoveDragIndicator />
<ComposeModalManager />
<Toaster
position="bottom-right"
offset={{ right: 16, bottom: 16 }}
mobileOffset={{ right: 16, left: 16, bottom: 16 }}
style={
{
// Default Sonner --width is 356px; widen and clamp so wide custom toasts stay on-screen.
["--width"]: "min(420px, calc(100vw - 2.5rem))",
} as CSSProperties
}
theme="light"
richColors
closeButton
/>
</EmailDragProvider>
</ScheduledMailProvider>
</ComposeProvider>
)
}