import { router, useLocalSearchParams } from "expo-router"; import type { PluginIcon, PluginScreenParams, PluginScreenProps } from "@getpaseo/plugin/client"; import type { PluginTheme } from "@getpaseo/plugin"; import { X } from "lucide-react-native"; import { useCallback, useMemo, type ComponentType } from "react"; import { Text, View } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { HeaderIconBadge } from "@/components/headers/header-icon-badge"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; import { CompactHeader } from "@/components/headers/compact-header"; import { ScreenHeader } from "@/components/headers/screen-header"; import { ScreenTitle } from "@/components/headers/screen-title"; import { HostFilter } from "@/components/hosts/host-filter"; import type { HostPickerHost } from "@/components/hosts/host-picker"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useHostRuntimeClient, useHostRuntimeConnectionStatus, useHosts, } from "@/runtime/host-runtime"; import type { Theme } from "@/styles/theme"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { usePluginHostNavigation } from "./host-navigation"; import { PluginIconView } from "./icons"; import { toPluginTheme } from "./theme"; import { useInstalledPlugin, usePluginCatalogStatus, usePluginInstallations } from "./registry"; import { buildPluginSurfaceRoute, pluginScreenParamsFromRoute } from "./routes"; import { legacySidebarItemHostKey, pluginScreensHostKey, rememberPluginContributionHost, } from "./contribution-host"; import { SurfaceErrorBoundary } from "./surface-error-boundary"; import { PluginInstallationProvider } from "./installation-provider"; import { getPluginSurfaceContributionServerIds, resolvePluginScreenTitle, resolvePluginSurfaceContribution, resolvePluginSurfacePlaceholder, type PluginSurfaceContributionIdentity, type PluginSurfacePlaceholderStatus, } from "./surface-contribution"; import { resolvePluginPlatform } from "./platform"; const EMPTY_SHORTCUT_KEYS: ShortcutKey[] = []; const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const pluginThemeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme), }); const ThemedX = withUnistyles(X); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const SURFACE_ERRORS = { offline: "Plugin host is offline.", error: "Could not load plugins. Reconnect to try again.", unavailable: "This plugin surface is unavailable.", } as const; function PluginSurfacePlaceholder({ status }: { status: PluginSurfacePlaceholderStatus }) { if (status === "loading") { return ( ); } return {SURFACE_ERRORS[status]}; } function routeParam(value: string | string[] | undefined): string { return typeof value === "string" ? value : ""; } function PluginHeaderIcon({ icon, color = "" }: { icon: PluginIcon; color?: string }) { return ; } const ThemedPluginHeaderIcon = withUnistyles(PluginHeaderIcon); function SurfaceRenderer({ Surface, plugin, layout, host, params, theme, }: { Surface: ComponentType; plugin: NonNullable>; layout: PluginScreenProps["layout"]; host: PluginScreenProps["host"]; params: PluginScreenParams; theme: PluginTheme; }) { const navigation = usePluginHostNavigation(host.id); return ( ); } const ThemedSurfaceRenderer = withUnistyles(SurfaceRenderer); function PluginHostFilter({ serverId, pluginId, identity, params, hosts, }: { serverId: string; pluginId: string; identity: PluginSurfaceContributionIdentity; params: PluginScreenParams; hosts: HostPickerHost[]; }) { const selectHost = useCallback( (nextServerId: string) => { // A legacy row remembers its own host; a screen's host carries to all the plugin's items. rememberPluginContributionHost( identity.kind === "sidebar" ? legacySidebarItemHostKey(pluginId, identity.id) : pluginScreensHostKey(pluginId), nextServerId, ); router.replace(buildPluginSurfaceRoute(nextServerId, pluginId, identity, params)); }, [identity, params, pluginId], ); return ( ); } export function PluginSurfaceScreen() { const routeParams = useLocalSearchParams<{ serverId?: string | string[]; pluginId?: string | string[]; contributionKind?: string | string[]; contributionId?: string | string[]; }>(); const serverId = routeParam(routeParams.serverId); const pluginId = routeParam(routeParams.pluginId); const contributionKind = routeParam(routeParams.contributionKind); const contributionId = routeParam(routeParams.contributionId); const paramsKey = JSON.stringify(pluginScreenParamsFromRoute(routeParams)); // Keyed by content: the route hands back a new object on every render. const params = useMemo(() => JSON.parse(paramsKey), [paramsKey]); const identity = useMemo(() => { if (contributionKind !== "sidebar" && contributionKind !== "surface") return null; return { kind: contributionKind, id: contributionId }; }, [contributionId, contributionKind]); const plugin = useInstalledPlugin(serverId, pluginId); const installations = usePluginInstallations(pluginId); const hosts = useHosts(); const client = useHostRuntimeClient(serverId); const connectionStatus = useHostRuntimeConnectionStatus(serverId); const catalogStatus = usePluginCatalogStatus(serverId); const placeholderStatus = resolvePluginSurfacePlaceholder({ connectionStatus, catalogStatus }); const compact = useIsCompactFormFactor(); const { sidebarItem, surface } = useMemo( () => resolvePluginSurfaceContribution(plugin, identity), [identity, plugin], ); const hostLabel = hosts.find((host) => host.serverId === serverId)?.label ?? serverId; // The hosts this contribution is installed on; the filter shows when there is a choice. const filterHosts = useMemo(() => { if (!identity) return []; const serverIds = getPluginSurfaceContributionServerIds(installations, pluginId, identity); return hosts.filter((candidate) => serverIds.includes(candidate.serverId)); }, [hosts, identity, installations, pluginId]); const title = useMemo(() => { if (surface && plugin) return resolvePluginScreenTitle(surface, sidebarItem, params, (text) => plugin.i18n.resolve(text, plugin.locale), ); if (sidebarItem && plugin) return plugin.i18n.resolve(sidebarItem.title, plugin.locale); return pluginId || "Plugin"; }, [params, plugin, pluginId, sidebarItem, surface]); const icon = sidebarItem?.icon ?? null; const close = useCallback(() => { if (router.canGoBack()) router.back(); else router.replace(`/h/${encodeURIComponent(serverId)}`); }, [serverId]); const layout = useMemo(() => ({ compact, platform: resolvePluginPlatform() }), [compact]); const host = useMemo(() => ({ id: serverId, label: hostLabel }), [hostLabel, serverId]); const headerLeft = useMemo( () => ( <> {icon ? ( ) : null} {title} ), [icon, title], ); const hostFilter = useMemo( () => identity && filterHosts.length > 1 ? ( ) : null, [filterHosts, identity, params, pluginId, serverId], ); const headerRight = useMemo( () => ( <> {hostFilter} ), [close, hostFilter], ); return ( {compact ? ( ) : ( )} {plugin && surface && client ? ( ) : ( )} ); } const styles = StyleSheet.create((theme) => ({ screen: { flex: 1, backgroundColor: theme.colors.surface0, }, body: { flex: 1, }, loading: { flex: 1, alignItems: "center", justifyContent: "center", }, errorText: { color: theme.colors.statusDanger, padding: theme.spacing[4], }, }));