248 lines
8.1 KiB
TypeScript
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>
|
|
)
|
|
}
|