210 lines
6.4 KiB
TypeScript
210 lines
6.4 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
Suspense,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type CSSProperties,
|
|
} from "react"
|
|
import dynamic from "next/dynamic"
|
|
import { useIsXs } from "@/hooks/use-xs"
|
|
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 { 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"
|
|
|
|
const MobileBottomBar = dynamic(
|
|
() =>
|
|
import("@/components/gmail/mobile-bottom-bar").then(
|
|
(m) => m.MobileBottomBar
|
|
),
|
|
{ ssr: false }
|
|
)
|
|
import {
|
|
parseMailSegments,
|
|
buildMailPath,
|
|
DEFAULT_INBOX_TAB,
|
|
type MailRouteState,
|
|
} from "@/lib/mail-url"
|
|
|
|
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 pushRecentFolderVisit = useMailStore((s) => s.pushRecentFolderVisit)
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(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 (isXs) setSidebarCollapsed(true)
|
|
},
|
|
[navigateRoute, isXs]
|
|
)
|
|
|
|
return (
|
|
<SidebarNavProvider
|
|
routeFolderId={route.folderId}
|
|
onRouteFolderIdChange={(nextFolderId) =>
|
|
navigateRoute({
|
|
folderId: nextFolderId,
|
|
inboxTab: DEFAULT_INBOX_TAB,
|
|
page: 1,
|
|
mailId: null,
|
|
})
|
|
}
|
|
>
|
|
<div className="flex h-screen flex-col bg-app-canvas">
|
|
{!isXs && (
|
|
<Header
|
|
isXs={false}
|
|
sidebarCollapsed={sidebarCollapsed}
|
|
onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
/>
|
|
)}
|
|
<div className="relative flex min-h-0 flex-1 gap-0 overflow-hidden bg-app-canvas pl-0 pr-0 pb-1 pt-1 sm:gap-1 sm:pl-1">
|
|
{isXs && !sidebarCollapsed && (
|
|
<button
|
|
type="button"
|
|
aria-label="Fermer le menu"
|
|
className="absolute inset-0 z-30 bg-black/20"
|
|
onClick={() => setSidebarCollapsed(true)}
|
|
/>
|
|
)}
|
|
<div
|
|
className={
|
|
isXs
|
|
? "w-0 shrink-0"
|
|
: sidebarCollapsed
|
|
? "w-[68px] shrink-0"
|
|
: "w-60 shrink-0"
|
|
}
|
|
/>
|
|
<Sidebar
|
|
selectedFolder={route.folderId}
|
|
onSelectFolder={handleSelectFolder}
|
|
collapsed={sidebarCollapsed}
|
|
isXs={isXs}
|
|
folderUnreadCounts={folderUnreadCounts}
|
|
/>
|
|
<main className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-none bg-white shadow-sm sm:rounded-2xl">
|
|
<Suspense>
|
|
<EmailList
|
|
selectedFolder={route.folderId}
|
|
inboxTab={route.inboxTab}
|
|
listPage={route.page}
|
|
openMailId={route.mailId}
|
|
onMailRouteNavigate={navigateRoute}
|
|
onSelectFolder={handleSelectFolder}
|
|
onFolderUnreadCountsChange={setFolderUnreadCounts}
|
|
/>
|
|
</Suspense>
|
|
</main>
|
|
<RightPanel />
|
|
</div>
|
|
{isXs && (
|
|
<MobileBottomBar
|
|
sidebarOpen={!sidebarCollapsed}
|
|
onToggleSidebar={() => setSidebarCollapsed((c) => !c)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</SidebarNavProvider>
|
|
)
|
|
}
|
|
|
|
export function MailAppShell({
|
|
children: _routeOutlet,
|
|
}: {
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<ComposeProvider>
|
|
<ScheduledMailProvider>
|
|
<EmailDragProvider>
|
|
<Suspense
|
|
fallback={
|
|
<div className="flex h-screen flex-col bg-app-canvas">
|
|
<div className="h-14 shrink-0 border-b border-gray-200 bg-white" />
|
|
<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>
|
|
)
|
|
}
|