ultisuite-client/app/mail/mail-app-shell.tsx
R3D347HR4Y 3bbf3691b0
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
bordel c'est beau
2026-06-11 10:10:39 +02:00

283 lines
10 KiB
TypeScript

"use client"
import {
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useState,
} 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 { useMailRoute } from "@/hooks/use-mail-route"
import { parseSearchParams } from "@/lib/mail-search/search-params"
import { searchParamsToDisplayQuery } from "@/lib/mail-search/search-filter"
import { MobileBottomBar } from "@/components/gmail/mobile-bottom-bar"
import { MobileSearchOverlay } from "@/components/gmail/mobile-search-overlay"
import type { MailXsViewChrome } from "@/lib/mail-xs-view-chrome"
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 { PendingComposeBridge } from "@/components/gmail/pending-compose-bridge"
import { SidebarNavProvider } from "@/lib/sidebar-nav-context"
import { mailNavVisitKey } from "@/lib/mail-folder-display"
import { MailDocumentTitle } from "@/components/gmail/mail-document-title"
import { useMailStore } from "@/lib/stores/mail-store"
import { useMailUiStore } from "@/lib/stores/mail-ui-store"
import { DEFAULT_INBOX_TAB } from "@/lib/mail-url"
import { cn } from "@/lib/utils"
import { ThemeProvider } from "@/components/theme-provider"
import { MailThemeApplier } from "@/components/gmail/mail-theme-applier"
import { QuickSettingsRoot } from "@/components/gmail/quick-settings/quick-settings-root"
import { MailSettingsSync } from "@/components/gmail/mail-settings-sync"
import { MailNavSync } from "@/components/gmail/mail-nav-sync"
import { ComposeIdentitiesSync } from "@/components/gmail/compose-identities-sync"
import { MailSignaturesSync } from "@/components/gmail/mail-signatures-sync"
import { MailNotificationsBridge } from "@/components/gmail/mail-notifications-bridge"
import { useWebSocket } from "@/lib/api/ws"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
import { FilePreviewDialog } from "@/components/drive/file-preview-dialog"
import { AiChatPanel } from "@/components/ai/ai-chat-panel"
const MAIL_SETTINGS_PATH = "/mail/settings"
function isMailSettingsPath(pathname: string | null): boolean {
return pathname === MAIL_SETTINGS_PATH || pathname?.startsWith(`${MAIL_SETTINGS_PATH}/`) === true
}
function MailAppInner() {
const router = useRouter()
const { route, navigateRoute, searchParams: currentSearchParams } =
useMailRoute()
const activeSearchQuery =
route.folderId === "search"
? searchParamsToDisplayQuery(parseSearchParams(currentSearchParams))
: ""
const isXs = useIsXs()
const touchNav = useTouchNav()
const splitView = useMailSplitView()
const pushRecentFolderVisit = useMailStore((s) => s.pushRecentFolderVisit)
const sidebarCollapsed = useMailUiStore((s) => s.sidebarCollapsed)
const setSidebarCollapsed = useMailUiStore((s) => s.setSidebarCollapsed)
const mobileSearchOpen = useMailUiStore((s) => s.mobileSearchOpen)
const setMobileSearchOpen = useMailUiStore((s) => s.setMobileSearchOpen)
const folderUnreadCounts = useMailUiStore((s) => s.folderUnreadCounts)
const setFolderUnreadCounts = useMailUiStore((s) => s.setFolderUnreadCounts)
const [xsViewChrome, setXsViewChrome] = useState<MailXsViewChrome | null>(null)
useLayoutEffect(() => {
if (!readTouchNavMatches()) setSidebarCollapsed(false)
}, [setSidebarCollapsed])
useEffect(() => {
if (isXs) setSidebarCollapsed(true)
}, [isXs, setSidebarCollapsed])
useEffect(() => {
if (route.folderId !== "search") {
pushRecentFolderVisit(mailNavVisitKey(route.folderId, route.inboxTab))
}
}, [route.folderId, route.inboxTab, pushRecentFolderVisit])
const handleSelectFolder = useCallback(
(id: string) => {
useMailUiStore.getState().requestSuppressSplitAutoOpen()
navigateRoute({
folderId: id,
inboxTab: DEFAULT_INBOX_TAB,
page: 1,
mailId: null,
})
if (readTouchNavMatches()) setSidebarCollapsed(true)
},
[navigateRoute, setSidebarCollapsed]
)
return (
<SidebarNavProvider
routeFolderId={route.folderId}
onRouteFolderIdChange={(nextFolderId) => {
useMailUiStore.getState().requestSuppressSplitAutoOpen()
navigateRoute({
folderId: nextFolderId,
inboxTab: DEFAULT_INBOX_TAB,
page: 1,
mailId: null,
})
}}
>
<MailDocumentTitle />
<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)}
onOpenMobileSearch={() => setMobileSearchOpen(true)}
/>
</div>
) : null}
<div
className={cn(
"relative flex min-h-0 flex-1 gap-0 overflow-hidden pl-0 pr-0",
splitView ? "bg-mail-surface p-0" : "bg-app-canvas sm:gap-1 sm:pb-1 sm:pl-1 sm:pt-1"
)}
>
{!sidebarCollapsed && touchNav && (
<button
type="button"
aria-label="Fermer le menu"
className="absolute inset-0 z-30 bg-black/20"
onClick={() => setSidebarCollapsed(true)}
/>
)}
{/* xs: overlay (w-0). sm+: spacer matches rail; hover-expand can grow over main without shifting layout */}
<div
className={cn(
"shrink-0 transition-[width] duration-200 ease-linear",
isXs ? "w-0" : sidebarCollapsed ? "w-[68px]" : "w-60"
)}
aria-hidden
/>
<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-mail-surface",
splitView
? "rounded-none shadow-none"
: "rounded-none shadow-none sm:rounded-2xl sm:shadow-sm"
)}
>
<Suspense>
<EmailList
selectedFolder={route.folderId}
inboxTab={route.inboxTab}
listPage={route.page}
openMailId={route.mailId}
splitView={splitView}
onToggleSidebar={() =>
useMailUiStore.getState().toggleSidebarCollapsed()
}
onMailRouteNavigate={navigateRoute}
onSelectFolder={handleSelectFolder}
onFolderUnreadCountsChange={setFolderUnreadCounts}
onXsViewChromeChange={setXsViewChrome}
/>
</Suspense>
</main>
<div
className={cn(
"flex shrink-0 flex-col",
splitView && "border-l border-gray-200"
)}
>
<RightPanel />
</div>
<ContactsPanel />
<AiChatPanel />
</div>
{!splitView ? (
<MobileBottomBar
sidebarOpen={!sidebarCollapsed}
onToggleSidebar={() =>
useMailUiStore.getState().toggleSidebarCollapsed()
}
xsViewChrome={xsViewChrome}
onOpenSearch={() => setMobileSearchOpen(true)}
searchQuery={activeSearchQuery}
onClearSearch={() => router.push("/mail/inbox")}
/>
) : null}
<MobileSearchOverlay
open={mobileSearchOpen}
onClose={() => setMobileSearchOpen(false)}
initialQuery={activeSearchQuery}
/>
</div>
</SidebarNavProvider>
)
}
export function MailAppShell({
children: routeOutlet,
}: {
children: React.ReactNode
}) {
const pathname = usePathname()
const showSettingsPage = isMailSettingsPath(pathname)
useWebSocket()
useEffect(() => {
if (showSettingsPage) {
useMailSettingsStore.getState().setQuickSettingsOpen(false)
}
}, [showSettingsPage])
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 (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<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>
}
>
{showSettingsPage ? (
<SidebarNavProvider>{routeOutlet}</SidebarNavProvider>
) : (
<MailAppInner />
)}
</Suspense>
<MailThemeApplier />
<MailSettingsSync />
<MailNavSync />
<ComposeIdentitiesSync />
<MailSignaturesSync />
<MailNotificationsBridge />
<QuickSettingsRoot />
<MoveDragIndicator />
<PendingComposeBridge />
<ComposeModalManager />
<FilePreviewDialog />
</EmailDragProvider>
</ScheduledMailProvider>
</ComposeProvider>
</ThemeProvider>
)
}