"use client"; import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { useTranslations } from "next-intl"; import { Link } from "@/i18n/navigation"; import { Loader2, Puzzle } from "@/components/icons"; import { useAuthStore, redirectToLogin, saveRedirectAfterLogin } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { usePluginStore } from "@/stores/plugin-store"; import { NavigationRail } from "@/components/layout/navigation-rail"; import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { InlineAppView } from "@/components/layout/inline-app-view"; import { MvTopBar } from "@/components/layout/mv-top-bar"; import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot"; import { PluginIframeSlot } from "@/components/plugins/plugin-iframe-slot"; import { useSidebarApps } from "@/hooks/use-sidebar-apps"; import { useIsEmbedded } from "@/hooks/use-is-embedded"; import { useIsMobile } from "@/hooks/use-media-query"; import { useMountainViewShell } from "@/hooks/use-mountain-view-shell"; import { useDocumentTitle } from "@/hooks/use-document-title"; import { findPage, subscribePages } from "@/lib/plugin-sandbox/pages"; import { offersForSlot, subscribe as subscribeSlots } from "@/lib/plugin-sandbox/registry"; import { cn } from "@/lib/utils"; export interface PluginPageAppProps { /** Path segments after `/apps`: `[pluginId, pageId, ...subPath]`. */ linkSegments?: string[]; } /** * A page a plugin added with api.ui.setPages, at /apps//. * The app's own frame (navigation rail, or Mountain View's top bar, and the * phone navigation) stays around it; the plugin's `app-page` slot fills the * content area and gets the rest of the path as `path`. */ export function PluginPageApp({ linkSegments = [] }: PluginPageAppProps) { const t = useTranslations("plugin_pages"); const [pluginId = "", pageId = "", ...rest] = linkSegments; const subPath = rest.join("/"); const { isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore(); const [initialCheckDone, setInitialCheckDone] = useState( () => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client, ); const { quota, isPushConnected } = useEmailStore(); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const isMobile = useIsMobile(); const isEmbedded = useIsEmbedded(); const mvShell = useMountainViewShell(isEmbedded); const pluginsReady = usePluginStore((s) => s.initialized); const page = useSyncExternalStore( subscribePages, () => findPage(pluginId, pageId), () => null, ); const offersPage = useSyncExternalStore( subscribeSlots, () => offersForSlot("app-page").some((o) => o.pluginId === pluginId), () => false, ); useDocumentTitle(page?.title ?? null); useEffect(() => { const state = useAuthStore.getState(); if (state.isAuthenticated && state.client) { setInitialCheckDone(true); return; } checkAuth().finally(() => setInitialCheckDone(true)); }, [checkAuth]); useEffect(() => { if (initialCheckDone && !isAuthenticated && !authLoading) { saveRedirectAfterLogin(); redirectToLogin(); } }, [initialCheckDone, isAuthenticated, authLoading]); // Opened straight from a link or a reload: the plugins have not been // started yet on this page load (mail and settings do the same). useEffect(() => { if (isAuthenticated) void usePluginStore.getState().initializePlugins(); }, [isAuthenticated]); const extraProps = useMemo(() => ({ pageId, path: subPath }), [pageId, subPath]); if (!isAuthenticated) return null; const available = !!page && offersPage; let content: React.ReactNode; if (available) { content = (
); } else if (!pluginsReady) { content = (
); } else { content = (

{t("not_available_title")}

{t("not_available_description")}

{t("back_to_mail")}
); } return (
{mvShell && ( )}
{!isMobile && !isEmbedded && !mvShell && (
)}
{inlineApp && }
{content}
{isMobile && !isEmbedded && ( )}
); }