import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; import Constants from "expo-constants"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { useCallback, useMemo, useRef } from "react"; import { Platform, Pressable, Text as RNText, TextInput, View } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ControlPillMenu } from "../../components/ControlPill"; import { SymbolView } from "../../components/AppSymbol"; import { T3Wordmark } from "../../components/T3Wordmark"; import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics"; import { resolveMobileStageLabel } from "../../lib/mobileBranding"; import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, } from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { WorkspaceConnectionTitle } from "./WorkspaceConnectionTitle"; import { buildHomeListFilterMenu, type HomeListFilterMenuEnvironment, type HomeListFilterMenuProject, } from "./home-list-filter-menu"; import { hasCustomHomeListOptions, PROJECT_SORT_OPTIONS, THREAD_SORT_OPTIONS, } from "./home-list-options"; export type HomeHeaderEnvironment = HomeListFilterMenuEnvironment; export function HomeHeader(props: { readonly environments: ReadonlyArray; readonly projects: ReadonlyArray; readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; readonly projectSortOrder: HomeProjectSortOrder; readonly threadSortOrder: SidebarThreadSortOrder; readonly onSearchQueryChange: (query: string) => void; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onProjectChange: (projectKey: string | null) => void; readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void; readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void; readonly onOpenEnvironments: () => void; readonly onOpenSettings: () => void; readonly onStartNewTask: () => void; }) { if (Platform.OS === "android") { return ; } return ; } type HomeHeaderProps = Parameters[0]; function checkedMenuState(checked: boolean) { return checked ? ("on" as const) : undefined; } function AndroidHomeHeader(props: HomeHeaderProps) { const insets = useSafeAreaInsets(); const iconColor = useThemeColor("--color-icon"); const mutedColor = useThemeColor("--color-foreground-muted"); const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); // Thread List v2 lays the list out in fixed creation order, so the // sort/group filter controls would be silently ignored — hide them and // key the "customized" icon state off the environment filter alone. const threadListV2Enabled = useThreadListV2Enabled(); const hasCustomListOptions = threadListV2Enabled ? props.selectedEnvironmentId !== null || props.selectedProjectKey !== null : hasCustomHomeListOptions(props); const menuActions = useMemo( () => [ { id: "environment", title: "Environment", subactions: [ { id: "environment:all", title: "All environments", state: checkedMenuState(props.selectedEnvironmentId === null), }, ...props.environments.map((environment) => ({ id: `environment:${environment.environmentId}`, title: environment.label, state: checkedMenuState(props.selectedEnvironmentId === environment.environmentId), })), ], }, ...(props.projects.length === 0 ? [] : ([ { id: "project", title: "Project", subactions: [ { id: "project:all", title: "All projects", state: checkedMenuState(props.selectedProjectKey === null), }, ...props.projects.map((project) => ({ id: `project:${project.key}`, title: project.label, state: checkedMenuState(props.selectedProjectKey === project.key), })), ], }, ] satisfies MenuAction[])), ...(threadListV2Enabled ? [] : ([ { id: "project-sort", title: "Sort projects", subactions: PROJECT_SORT_OPTIONS.map((option) => ({ id: `project-sort:${option.value}`, title: option.label, state: checkedMenuState(props.projectSortOrder === option.value), })), }, { id: "thread-sort", title: "Sort threads", subactions: THREAD_SORT_OPTIONS.map((option) => ({ id: `thread-sort:${option.value}`, title: option.label, state: checkedMenuState(props.threadSortOrder === option.value), })), }, ] satisfies MenuAction[])), ], [ props.environments, props.projectSortOrder, props.projects, props.selectedEnvironmentId, props.selectedProjectKey, props.threadSortOrder, threadListV2Enabled, ], ); const handleMenuAction = useCallback( (event: { nativeEvent: { event: string } }) => { const id = event.nativeEvent.event; if (id === "environment:all") { props.onEnvironmentChange(null); return; } if (id.startsWith("environment:")) { const environmentId = id.slice("environment:".length); const environment = props.environments.find( (candidate) => candidate.environmentId === environmentId, ); if (environment) { props.onEnvironmentChange(environment.environmentId); } return; } if (id === "project:all") { props.onProjectChange(null); return; } if (id.startsWith("project:")) { const projectKey = id.slice("project:".length); if (props.projects.some((project) => project.key === projectKey)) { props.onProjectChange(projectKey); } return; } const projectSort = PROJECT_SORT_OPTIONS.find( (option) => id === `project-sort:${option.value}`, ); if (projectSort) { props.onProjectSortOrderChange(projectSort.value); return; } const threadSort = THREAD_SORT_OPTIONS.find((option) => id === `thread-sort:${option.value}`); if (threadSort) { props.onThreadSortOrderChange(threadSort.value); return; } }, [props], ); return ( <> {/* Brand slot doubles as the connection status surface: while an environment reconnects, the lockup fades to a status label in place (no layout shift in the list below). */} {/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */} Code {stageLabel} } /> {/* Built identically to the filter button so the two circles match exactly (ControlPill sizes via Tailwind classes and resolves to a different box). */} {props.searchQuery.length > 0 ? ( props.onSearchQueryChange("")} > ) : null} ); } function IosHomeHeader(props: HomeHeaderProps) { const searchBarRef = useRef(null); const iconColor = useThemeColor("--color-icon"); // Thread List v2 lays the list out in fixed creation order, so the // sort/group filter controls would be silently ignored — hide them and // key the "customized" icon state off the environment filter alone. const threadListV2Enabled = useThreadListV2Enabled(); const hasCustomListOptions = threadListV2Enabled ? props.selectedEnvironmentId !== null || props.selectedProjectKey !== null : hasCustomHomeListOptions(props); const focusSearch = useCallback(() => { searchBarRef.current?.focus(); return searchBarRef.current !== null; }, []); useHardwareKeyboardCommand("focusSearch", focusSearch); const filterMenu = buildHomeListFilterMenu({ ...props, listOrganization: !threadListV2Enabled, }); return ( <> [ withNativeGlassHeaderItem({ accessibilityLabel: "Open settings", icon: { name: "ellipsis", type: "sfSymbol" } as const, identifier: "home-settings", label: "", onPress: props.onOpenSettings, type: "button", }), ] : undefined, // The keys below are set per-branch (not `undefined`) so a later // reapply cannot clobber options owned by NativeHeaderToolbar. ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? { unstable_headerToolbarItems: () => [ createNativeMailSearchToolbarItem({ composeButtonId: "home-new-task", composeSystemImageName: "square.and.pencil", filterMenu, filterButtonId: "home-filter", filterSystemImageName: hasCustomListOptions ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease", onComposePress: props.onStartNewTask, onSearchTextChange: props.onSearchQueryChange, placeholder: "Search", searchTextChangeId: "home-search-text", showsSearchDismissButton: true, }), ], } : { // Pre-Liquid-Glass iOS: standard pull-down search in the nav // bar; create + sort live in the plain bottom toolbar below. headerSearchBarOptions: { ref: searchBarRef, autoCapitalize: "none" as const, hideNavigationBar: false, placeholder: "Search", onCancelButtonPress: () => { props.onSearchQueryChange(""); }, onChangeText: (event) => { props.onSearchQueryChange(event.nativeEvent.text); }, }, }), }} /> {NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? null : ( Environment props.onEnvironmentChange(null)} subtitle="Show threads from every environment" > All environments {props.environments.map((environment) => ( props.onEnvironmentChange(environment.environmentId)} > {environment.label} ))} {props.projects.length > 0 ? ( Project props.onProjectChange(null)} subtitle="Show threads from every project" > All projects {props.projects.map((project) => ( props.onProjectChange(project.key)} > {project.label} ))} ) : null} {threadListV2Enabled ? null : ( Sort projects {PROJECT_SORT_OPTIONS.map((option) => ( props.onProjectSortOrderChange(option.value)} > {option.label} ))} )} {threadListV2Enabled ? null : ( Sort threads {THREAD_SORT_OPTIONS.map((option) => ( props.onThreadSortOrderChange(option.value)} > {option.label} ))} )} )} ); }