import { navigationCustomizationAvailable, overflowNavigationTabs, } from "../layout/workspace/navigation-tab-order"; import { NAVIGATION_TAB_ICONS, WORKSPACE_TAB_LABELS, useNavigationTabOrder, useHiddenNavigationTabs, } from "../layout/workspace/navigation-tabs"; import { currentWorkspaceSelections } from "../layout/workspace/workspace-selection-store"; import { rememberedSettingsPage } from "../layout/workspace/workspace-tab-navigation"; import { useSelectWorkspaceTab } from "../layout/workspace/useSelectWorkspaceTab"; import { useWorkspaceTabs } from "../layout/workspace/WorkspaceTabsContext"; import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScrollView"; import { EmptyState } from "../../components/ui/EmptyState"; import { useAuth, useUser } from "@clerk/expo"; import { useNavigation, useNavigationState } from "@react-navigation/native"; import { use, type ComponentProps } from "react"; import { Platform, View } from "react-native"; import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics"; import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-grouping"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { hasCloudPublicConfig } from "../cloud/publicConfig"; import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { SettingsRow } from "./components/SettingsRow"; import { SettingsSection, SettingsSidebarContext } from "./components/SettingsSection"; import { SettingsScreen } from "./components/SettingsScreen"; import type { SettingsPageTarget } from "./components/settings-page-targets"; import { useSettingsEnvironmentMenu } from "./components/SettingsEnvironmentFilterHeader"; import { useSettingsEnvironmentFilter } from "./settings-environment-filter"; import { filterSettingsSections } from "./settings-search"; import { usePaneRole } from "../../components/ui/Pane"; import { settingsSearchQuery } from "../layout/workspace/workspace-tabs-state"; export function SettingsRouteScreen() { const list = useSettingsListPlacement(); return ; } /** The Settings list inside a workspace shell that draws its bar, title and search itself. */ export function WorkspaceSettingsList() { const list = useSettingsListPlacement(); return ( ); } function useSettingsListPlacement() { const sidebar = usePaneRole() === "sidebar"; // The page open beside the list: the route right above the Settings tab's route. While // another tab is open this list stays mounted, hidden, and must keep showing the page Settings // reopens on; otherwise returning shows it unselected for a frame before the page arrives. const selected = useNavigationState((state) => { const list = state.routes.findIndex((route) => route.name === "Settings"); return list < 0 ? rememberedSettingsPage(currentWorkspaceSelections()) : (state.routes[list + 1]?.name ?? null); }); return { sidebar, selected }; } function SettingsListContent() { return hasCloudPublicConfig() ? : ; } export function SettingsListScreen(props: { readonly sidebar: boolean; readonly selected: string | null; }) { const workspace = useWorkspaceTabs(); const environmentMenu = useSettingsEnvironmentMenu(); const settings = workspace?.state.tabs.settings; const searching = settings !== undefined && settings.searchMode !== "resting"; return ( workspace.dispatch({ type: "query", tab: "settings", query }), placeholder: "Search settings", active: searching, onOpen: () => workspace.dispatch({ type: "search", tab: "settings" }), onClose: () => workspace.dispatch({ type: "cancel", tab: "settings" }), } : undefined } > ); } type SettingsListRow = ComponentProps & { readonly id: string }; interface SettingsListSection { readonly title?: string; readonly rows: readonly SettingsListRow[]; } function ConfiguredSettingsRouteScreen() { const navigation = useNavigation(); const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { user } = useUser(); const { savedConnectionsById } = useSavedRemoteConnections(); const accountLabel = !isLoaded ? "Checking" : !isSignedIn ? "Sign in" : (user?.primaryEmailAddress?.emailAddress ?? "Signed in"); return ( navigation.navigate("SettingsAuth"), }, environmentsRow(Object.keys(savedConnectionsById).length), { id: "SettingsNotifications", icon: "bell.badge", label: "Notifications", target: "SettingsNotifications", }, ]} /> ); } function LocalSettingsRouteScreen() { const { savedConnectionsById } = useSavedRemoteConnections(); return ; } function environmentsRow(count: number): SettingsListRow { return { id: "SettingsEnvironments", icon: "desktopcomputer", label: "Environments", value: `${count}`, valuePosition: "trailing", target: "SettingsEnvironments", }; } function SettingsList(props: { readonly connections: readonly SettingsListRow[] }) { // UIKit's scroll-edge material takes the scroll view's own color; like the chat list, match // the sidebar's surface so the bar doesn't darken over content at rest. // Grouped cards only lift off the screen surface; the drawer is as dark as they are. const listSurface = use(SettingsSidebarContext) ? "bg-drawer" : "bg-screen"; const insets = useSafeAreaInsets(); // UIKit insets the scroll view by its column's safe area, which this context can report as 0. const bottomSafeArea = useNativeColumnLayoutMetrics()?.safeArea.bottom ?? insets.bottom; const workspace = useWorkspaceTabs(); const settings = workspace?.state.tabs.settings; const query = settings ? settingsSearchQuery(settings) : ""; const overflowRows = useOverflowRows(); const indexSections = useSettingsIndexSections(); const sections = filterSettingsSections( [{ rows: overflowRows }, { title: "Connections", rows: props.connections }, ...indexSections], query, ); return ( {sections.length ? ( sections.map((section, index) => ( {section.rows.map(({ id, ...row }) => ( ))} )) ) : ( )} ); } function useSettingsIndexSections(): SettingsListSection[] { const { selectedTargets, projectGroups, selectedProjectKey } = useSettingsEnvironmentFilter(); const noServerTargets = selectedTargets.length === 0; const selectedProject = projectGroups.find((group) => group.key === selectedProjectKey); const scopedProjectMembers = selectedProject?.members .map((member) => member.project) .filter((project) => selectedTargets.some((target) => target.environmentId === project.environmentId), ) ?? []; const projectLabel = scopedProjectMembers.length > 0 ? deriveProjectGroupLabel({ representative: scopedProjectMembers[0]!, members: scopedProjectMembers, }) : (selectedProject?.label ?? "Unavailable project"); const page = ( target: SettingsPageTarget, icon: SettingsListRow["icon"], label: string, extra?: Pick, ): SettingsListRow => ({ id: target, icon, label, target, ...extra }); const serverPage = (target: SettingsPageTarget, icon: SettingsListRow["icon"], label: string) => page(target, icon, label, { disabled: noServerTargets }); // The same pages and groups as Settings on web, minus what mobile has no setting for. return [ { rows: [ page("SettingsOrganization", "gearshape", "General"), page("SettingsAppearance", "paintbrush", "Appearance"), page("SettingsFollowUp", "bubble.left.and.bubble.right", "Chat"), ...(navigationCustomizationAvailable() ? [page("SettingsNavigation", "line.3.horizontal", "Navigation tabs")] : []), ...(Platform.OS === "ios" ? [page("SettingsKeyboard", "keyboard", "Keyboard")] : []), page("SettingsVoice", "mic", "Voice"), ], }, { title: "Agents", rows: [ serverPage("SettingsProviderAccounts", "person.crop.circle", "Providers"), serverPage("SettingsThreads", "text.bubble", "Threads"), page("SettingsScheduledTasks", "clock", "Scheduled tasks"), ], }, { title: "Code", rows: [ page( "SettingsProjectOverview", "folder", "Projects", selectedProjectKey !== null ? { value: projectLabel } : undefined, ), serverPage("SettingsEnvironmentSourceControl", "arrow.triangle.branch", "Source control"), serverPage("SettingsWorktrees", "square.on.square", "Worktrees"), ], }, { rows: [ serverPage("SettingsEnvironmentMaintenance", "arrow.clockwise", "Maintenance"), page("SettingsLabs", "sparkles", "Labs"), page("SettingsAbout", "info.circle", "About"), page("SettingsArchive", "archivebox", "Archive"), ], }, ]; } function useOverflowRows(): SettingsListRow[] { const order = useNavigationTabOrder(); const hidden = useHiddenNavigationTabs(); const { layout } = useAdaptiveWorkspaceLayout(); const tabs = Platform.OS === "android" && layout.usesSplitView ? [] : overflowNavigationTabs(order, hidden); const selectTab = useSelectWorkspaceTab(); return tabs.map((tab) => ({ id: tab, icon: NAVIGATION_TAB_ICONS[tab], label: WORKSPACE_TAB_LABELS[tab], onPress: () => selectTab(tab), })); }