import { navigationCustomizationAvailable, overflowNavigationTabs, } from "../workspace/navigation-tab-order"; import { NAVIGATION_TAB_ICONS, WORKSPACE_TAB_LABELS, useNavigationTabOrder, useHiddenNavigationTabs, } from "../workspace/navigation-tabs"; import { currentWorkspaceSelections } from "../workspace/workspace-selection-store"; import { rememberedSettingsPage } from "../workspace/workspace-tab-navigation"; import { useSelectWorkspaceTab } from "../workspace/useSelectWorkspaceTab"; import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext"; import { ScreenScrollView as ScrollView, ScreenScrollViewBottomInset, } 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, useMemo, type ComponentProps } from "react"; import { Platform, View } from "react-native"; import { useWorkspaceSurface } from "../workspace/useWorkspaceSurface"; import { useDeviceType } from "../../native/native-layout-metrics"; import { useCompactWorkspaceTabs } from "../workspace/useSystemWorkspaceTabBar"; 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/AdaptiveWorkspaceLayout"; 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 { AndroidSettingsEnvironmentFilter, SettingsEnvironmentFilterHeader, } from "./components/SettingsEnvironmentFilterHeader"; import { useSettingsEnvironmentFilter } from "./settings-environment-filter"; import { filterSettingsSections } from "./settings-search"; import { NativePrimaryColumnContext } from "../../native/v5-workspace-context"; export function SettingsRouteScreen() { const surface = useWorkspaceSurface(); const deviceType = useDeviceType(); const compactTabs = useCompactWorkspaceTabs(); const workspace = useWorkspaceTabs(); const { layout } = useAdaptiveWorkspaceLayout(); const inSidebar = use(NativePrimaryColumnContext) !== null && layout.usesSplitView; // 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); }); const sidebar = useMemo(() => (inSidebar ? { selected } : null), [inSidebar, selected]); const content = hasCloudPublicConfig() ? ( ) : ( ); return ( {Platform.OS === "android" ? ( } search={ workspace ? { value: workspace.state.tabs.settings.query, onChangeText: (query) => workspace.dispatch({ type: "query", tab: "settings", query }), placeholder: "Search settings", } : undefined } > {content} ) : ( content )} ); } 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(); const workspace = useWorkspaceTabs(); const query = workspace?.state.tabs.settings.query ?? ""; 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("SettingsMicrophone", "mic", "Microphone"), ] : []), ], }, { 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), })); }