Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
Suspense,
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useState,
|
|
} from "react"
|
|
import { usePathname } from "next/navigation"
|
|
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 { 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 { 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={() =>
|
|
navigateRoute({
|
|
folderId: "inbox",
|
|
inboxTab: DEFAULT_INBOX_TAB,
|
|
page: 1,
|
|
mailId: null,
|
|
})
|
|
}
|
|
/>
|
|
) : 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>
|
|
)
|
|
}
|