| author | |
| committer | |
| log | 833a03afe32b2f332ab19ce72c0e0dc3cf72c341 |
| tree | 071e0ee5a2f0b2c66d37bb50a1570859360d2cb0 |
| parent | 4fec120b75bb397bda3c09b7317f79c1d26811c0 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>5 files changed, 321 insertions(+), 111 deletions(-)
apps/mobile/src/features/threads/ThreadHeader.tsx created+115| ... | ... | @@ -0,0 +1,115 @@ |
| 1 | import { StackActions, useNavigation } from "@react-navigation/native"; | |
| 2 | import { useMemo } from "react"; | |
| 3 | import { ScreenHeader } from "../../components/ScreenHeader"; | |
| 4 | import { ScreenHeaderButton } from "../../components/ScreenHeaderButton"; | |
| 5 | import type { ScreenHeaderAction } from "../../components/ScreenHeader.types"; | |
| 6 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; | |
| 7 | import type { ThreadInspectorMode } from "./thread-inspector-content-stack"; | |
| 8 | import { useThreadHeaderOptions } from "./useThreadHeaderOptions"; | |
| 9 | ||
| 10 | export function ThreadHeader( | |
| 11 | props: Parameters<typeof useThreadHeaderOptions>[0] & { | |
| 12 | readonly hasThreadCwd: boolean; | |
| 13 | readonly hasWorkspaceRoot: boolean; | |
| 14 | readonly fileInspectorSupported: boolean; | |
| 15 | readonly inspectorMode: ThreadInspectorMode | null; | |
| 16 | readonly onToggleInspector: () => void; | |
| 17 | readonly onOpenGitInspector: () => void; | |
| 18 | readonly onOpenFilesInspector: () => void; | |
| 19 | }, | |
| 20 | ) { | |
| 21 | const navigation = useNavigation(); | |
| 22 | const { layout, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); | |
| 23 | const { onOpenTerminal, onMergeBack } = props.gitControls; | |
| 24 | const native = useThreadHeaderOptions(props); | |
| 25 | const androidHeaderActions = useMemo<ReadonlyArray<ScreenHeaderAction>>(() => { | |
| 26 | const actions: ScreenHeaderAction[] = []; | |
| 27 | if (props.onReturnToThread) { | |
| 28 | actions.push({ | |
| 29 | accessibilityLabel: "Return to chat", | |
| 30 | icon: "chevron.left", | |
| 31 | onPress: props.onReturnToThread, | |
| 32 | }); | |
| 33 | } | |
| 34 | if (props.hasThreadCwd) { | |
| 35 | const filesVisible = props.inspectorMode === "files" && panes.auxiliaryPaneVisible; | |
| 36 | actions.push({ | |
| 37 | accessibilityLabel: filesVisible ? "Close files" : "Open files", | |
| 38 | selected: filesVisible, | |
| 39 | icon: "folder", | |
| 40 | onPress: filesVisible ? toggleAuxiliaryPane : props.onOpenFilesInspector, | |
| 41 | }); | |
| 42 | } | |
| 43 | if (props.hasWorkspaceRoot && props.gitControls.canOpenTerminal) { | |
| 44 | actions.push({ | |
| 45 | accessibilityLabel: "Open terminal", | |
| 46 | icon: "terminal", | |
| 47 | onPress: () => onOpenTerminal(null), | |
| 48 | }); | |
| 49 | } | |
| 50 | actions.push({ | |
| 51 | accessibilityLabel: "Open git controls", | |
| 52 | icon: "point.topleft.down.curvedto.point.bottomright.up", | |
| 53 | onPress: props.onOpenGitInspector, | |
| 54 | }); | |
| 55 | if (onMergeBack) { | |
| 56 | actions.push({ | |
| 57 | accessibilityLabel: "Merge back to source", | |
| 58 | icon: "arrow.triangle.merge", | |
| 59 | onPress: onMergeBack, | |
| 60 | }); | |
| 61 | } | |
| 62 | return actions; | |
| 63 | }, [ | |
| 64 | props.inspectorMode, | |
| 65 | panes.auxiliaryPaneVisible, | |
| 66 | props.onOpenFilesInspector, | |
| 67 | onOpenTerminal, | |
| 68 | onMergeBack, | |
| 69 | props.onOpenGitInspector, | |
| 70 | toggleAuxiliaryPane, | |
| 71 | props.onReturnToThread, | |
| 72 | props.hasThreadCwd, | |
| 73 | props.hasWorkspaceRoot, | |
| 74 | props.gitControls.canOpenTerminal, | |
| 75 | ]); | |
| 76 | ||
| 77 | return ( | |
| 78 | <> | |
| 79 | <ScreenHeader | |
| 80 | title={props.title} | |
| 81 | subtitle={props.subtitle} | |
| 82 | sidebar={native.sidebar} | |
| 83 | options={native.options} | |
| 84 | optionsVersion={native.optionsVersion} | |
| 85 | trailing={ | |
| 86 | props.fileInspectorSupported && props.hasThreadCwd ? ( | |
| 87 | <ScreenHeaderButton | |
| 88 | accessibilityLabel={ | |
| 89 | props.inspectorMode !== null && panes.auxiliaryPaneVisible | |
| 90 | ? "Hide inspector" | |
| 91 | : "Show inspector" | |
| 92 | } | |
| 93 | icon="sidebar.right" | |
| 94 | selected={props.inspectorMode !== null && panes.auxiliaryPaneVisible} | |
| 95 | onPress={props.onToggleInspector} | |
| 96 | /> | |
| 97 | ) : null | |
| 98 | } | |
| 99 | onBack={ | |
| 100 | layout.usesSplitView | |
| 101 | ? undefined | |
| 102 | : () => { | |
| 103 | // A deep link or cold start has no previous route; Home is the way out. | |
| 104 | // Read the history at press time: it changes without re-rendering this screen. | |
| 105 | if (navigation.canGoBack()) navigation.goBack(); | |
| 106 | else navigation.dispatch(StackActions.replace("Home")); | |
| 107 | } | |
| 108 | } | |
| 109 | actions={androidHeaderActions} | |
| 110 | hideBottomBorder | |
| 111 | /> | |
| 112 | {native.fallback} | |
| 113 | </> | |
| 114 | ); | |
| 115 | } |
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+1-111| ... | ... | @@ -2,10 +2,6 @@ import { makeTurnCommandMetadata } from "../../lib/commandMetadata"; |
| 2 | 2 | import { buildProjectThreadStartTurnInput } from "../../lib/projectThreadStartTurn"; |
| 3 | 3 | import { useWorktreeSetup } from "./use-worktree-setup"; |
| 4 | 4 | import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup"; |
| 5 | import { ScreenHeader } from "../../components/ScreenHeader"; | |
| 6 | import { ScreenHeaderButton } from "../../components/ScreenHeaderButton"; | |
| 7 | import type { ScreenHeaderAction } from "../../components/ScreenHeader.types"; | |
| 8 | import { useThreadHeaderOptions } from "./useThreadHeaderOptions"; | |
| 9 | 5 | import { |
| 10 | 6 | StackActions, |
| 11 | 7 | useFocusEffect, |
| ... | ... | @@ -88,113 +84,7 @@ import { |
| 88 | 84 | type ThreadInspectorMode, |
| 89 | 85 | } from "./thread-inspector-content-stack"; |
| 90 | 86 | import { threadRouteIsHydrating } from "./thread-route-hydration"; |
| 91 | ||
| 92 | function ThreadHeader( | |
| 93 | props: Parameters<typeof useThreadHeaderOptions>[0] & { | |
| 94 | readonly hasThreadCwd: boolean; | |
| 95 | readonly hasWorkspaceRoot: boolean; | |
| 96 | readonly fileInspectorSupported: boolean; | |
| 97 | readonly inspectorMode: ThreadInspectorMode | null; | |
| 98 | readonly onToggleInspector: () => void; | |
| 99 | readonly onOpenGitInspector: () => void; | |
| 100 | readonly onOpenFilesInspector: () => void; | |
| 101 | }, | |
| 102 | ) { | |
| 103 | const navigation = useNavigation(); | |
| 104 | const { layout, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); | |
| 105 | const { onOpenTerminal, onMergeBack } = props.gitControls; | |
| 106 | const native = useThreadHeaderOptions(props); | |
| 107 | const androidHeaderActions = useMemo<ReadonlyArray<ScreenHeaderAction>>(() => { | |
| 108 | const actions: ScreenHeaderAction[] = []; | |
| 109 | if (props.onReturnToThread) { | |
| 110 | actions.push({ | |
| 111 | accessibilityLabel: "Return to chat", | |
| 112 | icon: "chevron.left", | |
| 113 | onPress: props.onReturnToThread, | |
| 114 | }); | |
| 115 | } | |
| 116 | if (props.hasThreadCwd) { | |
| 117 | const filesVisible = props.inspectorMode === "files" && panes.auxiliaryPaneVisible; | |
| 118 | actions.push({ | |
| 119 | accessibilityLabel: filesVisible ? "Close files" : "Open files", | |
| 120 | selected: filesVisible, | |
| 121 | icon: "folder", | |
| 122 | onPress: filesVisible ? toggleAuxiliaryPane : props.onOpenFilesInspector, | |
| 123 | }); | |
| 124 | } | |
| 125 | if (props.hasWorkspaceRoot && props.gitControls.canOpenTerminal) { | |
| 126 | actions.push({ | |
| 127 | accessibilityLabel: "Open terminal", | |
| 128 | icon: "terminal", | |
| 129 | onPress: () => onOpenTerminal(null), | |
| 130 | }); | |
| 131 | } | |
| 132 | actions.push({ | |
| 133 | accessibilityLabel: "Open git controls", | |
| 134 | icon: "point.topleft.down.curvedto.point.bottomright.up", | |
| 135 | onPress: props.onOpenGitInspector, | |
| 136 | }); | |
| 137 | if (onMergeBack) { | |
| 138 | actions.push({ | |
| 139 | accessibilityLabel: "Merge back to source", | |
| 140 | icon: "arrow.triangle.merge", | |
| 141 | onPress: onMergeBack, | |
| 142 | }); | |
| 143 | } | |
| 144 | return actions; | |
| 145 | }, [ | |
| 146 | props.inspectorMode, | |
| 147 | panes.auxiliaryPaneVisible, | |
| 148 | props.onOpenFilesInspector, | |
| 149 | onOpenTerminal, | |
| 150 | onMergeBack, | |
| 151 | props.onOpenGitInspector, | |
| 152 | toggleAuxiliaryPane, | |
| 153 | props.onReturnToThread, | |
| 154 | props.hasThreadCwd, | |
| 155 | props.hasWorkspaceRoot, | |
| 156 | props.gitControls.canOpenTerminal, | |
| 157 | ]); | |
| 158 | ||
| 159 | return ( | |
| 160 | <> | |
| 161 | <ScreenHeader | |
| 162 | title={props.title} | |
| 163 | subtitle={props.subtitle} | |
| 164 | sidebar={native.sidebar} | |
| 165 | options={native.options} | |
| 166 | optionsVersion={props.gitControls.projectScripts} | |
| 167 | trailing={ | |
| 168 | props.fileInspectorSupported && props.hasThreadCwd ? ( | |
| 169 | <ScreenHeaderButton | |
| 170 | accessibilityLabel={ | |
| 171 | props.inspectorMode !== null && panes.auxiliaryPaneVisible | |
| 172 | ? "Hide inspector" | |
| 173 | : "Show inspector" | |
| 174 | } | |
| 175 | icon="sidebar.right" | |
| 176 | selected={props.inspectorMode !== null && panes.auxiliaryPaneVisible} | |
| 177 | onPress={props.onToggleInspector} | |
| 178 | /> | |
| 179 | ) : null | |
| 180 | } | |
| 181 | onBack={ | |
| 182 | layout.usesSplitView | |
| 183 | ? undefined | |
| 184 | : () => { | |
| 185 | // A deep link or cold start has no previous route; Home is the way out. | |
| 186 | // Read the history at press time: it changes without re-rendering this screen. | |
| 187 | if (navigation.canGoBack()) navigation.goBack(); | |
| 188 | else navigation.dispatch(StackActions.replace("Home")); | |
| 189 | } | |
| 190 | } | |
| 191 | actions={androidHeaderActions} | |
| 192 | hideBottomBorder | |
| 193 | /> | |
| 194 | {native.fallback} | |
| 195 | </> | |
| 196 | ); | |
| 197 | } | |
| 87 | import { ThreadHeader } from "./ThreadHeader"; | |
| 198 | 88 | |
| 199 | 89 | interface ThreadInspectorSelection { |
| 200 | 90 | readonly routeThreadIdentity: string | null; |
apps/mobile/src/features/threads/useThreadHeaderOptions.android.ts+2| ... | ... | @@ -5,6 +5,8 @@ export function useThreadHeaderOptions( |
| 5 | 5 | ): ReturnType<typeof useIosThreadHeaderOptions> { |
| 6 | 6 | return { |
| 7 | 7 | options: { contentStyle: { backgroundColor: props.headerColor } }, |
| 8 | // Android renders header actions as children, not native item factories. | |
| 9 | optionsVersion: [], | |
| 8 | 10 | sidebar: true, |
| 9 | 11 | fallback: null, |
| 10 | 12 | }; |
apps/mobile/src/features/threads/useThreadHeaderOptions.test.tsx created+190| ... | ... | @@ -0,0 +1,190 @@ |
| 1 | // @vitest-environment jsdom | |
| 2 | import type { VcsStatusResult } from "@t3tools/contracts"; | |
| 3 | import { act } from "react"; | |
| 4 | import { createRoot, type Root } from "react-dom/client"; | |
| 5 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 6 | ||
| 7 | // Native header item factories are only read when options are applied, so the fake | |
| 8 | // navigation snapshots the right items at each setOptions, as native-stack does. | |
| 9 | const harness = vi.hoisted(() => ({ | |
| 10 | setOptionsCalls: 0, | |
| 11 | renderedRightItems: [] as Array<ReadonlyArray<Record<string, unknown>>>, | |
| 12 | navigatedRoutes: [] as string[], | |
| 13 | })); | |
| 14 | ||
| 15 | vi.mock("@react-navigation/native", () => { | |
| 16 | const navigation = { | |
| 17 | canGoBack: () => true, | |
| 18 | dispatch: () => {}, | |
| 19 | goBack: () => {}, | |
| 20 | navigate: (route: string) => { | |
| 21 | harness.navigatedRoutes.push(route); | |
| 22 | }, | |
| 23 | addListener: () => () => {}, | |
| 24 | setOptions: (options: { unstable_headerRightItems?: () => Array<Record<string, unknown>> }) => { | |
| 25 | harness.setOptionsCalls += 1; | |
| 26 | harness.renderedRightItems.push(options.unstable_headerRightItems?.() ?? []); | |
| 27 | }, | |
| 28 | }; | |
| 29 | return { | |
| 30 | StackActions: { replace: () => ({}) }, | |
| 31 | useNavigation: () => navigation, | |
| 32 | }; | |
| 33 | }); | |
| 34 | vi.mock("../../state/session", () => ({ useEnvironmentScope: () => true })); | |
| 35 | vi.mock("react-native", () => ({ Alert: { alert: () => {} }, Linking: {}, Platform: {} })); | |
| 36 | vi.mock("../layout/AdaptiveWorkspaceLayout", () => ({ | |
| 37 | useAdaptiveWorkspaceLayout: () => ({ | |
| 38 | layout: { usesSplitView: false }, | |
| 39 | panes: { primarySidebarVisible: false, auxiliaryPaneVisible: false }, | |
| 40 | togglePrimarySidebar: () => {}, | |
| 41 | toggleAuxiliaryPane: () => {}, | |
| 42 | }), | |
| 43 | })); | |
| 44 | vi.mock("../layout/native-mail-search-toolbar", () => ({ | |
| 45 | NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED: true, | |
| 46 | createNativeMailSearchToolbarItem: () => ({}), | |
| 47 | })); | |
| 48 | vi.mock("../settings/appearance/AppearancePreferencesProvider", () => ({ | |
| 49 | useAppearancePreferences: () => ({ themeVariables: {} }), | |
| 50 | })); | |
| 51 | ||
| 52 | import { ThreadHeader } from "./ThreadHeader"; | |
| 53 | ||
| 54 | const projectScripts: never[] = []; | |
| 55 | ||
| 56 | function status( | |
| 57 | files: ReadonlyArray<string>, | |
| 58 | overrides: Partial<VcsStatusResult> = {}, | |
| 59 | ): VcsStatusResult { | |
| 60 | return { | |
| 61 | isRepo: true, | |
| 62 | hasPrimaryRemote: false, | |
| 63 | isDefaultRef: true, | |
| 64 | refName: "master", | |
| 65 | hasWorkingTreeChanges: files.length > 0, | |
| 66 | workingTree: { | |
| 67 | files: files.map((path) => ({ path, insertions: 0, deletions: 0 })), | |
| 68 | insertions: 0, | |
| 69 | deletions: 0, | |
| 70 | }, | |
| 71 | hasUpstream: false, | |
| 72 | aheadCount: 0, | |
| 73 | behindCount: 0, | |
| 74 | pr: null, | |
| 75 | ...overrides, | |
| 76 | } as unknown as VcsStatusResult; | |
| 77 | } | |
| 78 | ||
| 79 | // Mirrors the route screen: every render passes fresh gitControls callbacks. | |
| 80 | function Header(props: { readonly gitStatus: VcsStatusResult | null; readonly title?: string }) { | |
| 81 | return ( | |
| 82 | <ThreadHeader | |
| 83 | title={props.title ?? "Thread"} | |
| 84 | subtitle="" | |
| 85 | headerColor="#000" | |
| 86 | usesNativeHeaderGlass | |
| 87 | gitControls={{ | |
| 88 | environmentId: "environment-1", | |
| 89 | threadId: "thread-1", | |
| 90 | currentBranch: "master", | |
| 91 | gitStatus: props.gitStatus, | |
| 92 | gitOperationLabel: null, | |
| 93 | onPull: async () => {}, | |
| 94 | onRunAction: async () => null, | |
| 95 | canOpenTerminal: true, | |
| 96 | canOperateTerminal: true, | |
| 97 | canOpenFiles: true, | |
| 98 | projectScripts, | |
| 99 | terminalSessions: [], | |
| 100 | onOpenTerminal: () => {}, | |
| 101 | onOpenNewTerminal: () => {}, | |
| 102 | onRunProjectScript: async () => {}, | |
| 103 | }} | |
| 104 | hasThreadCwd | |
| 105 | hasWorkspaceRoot | |
| 106 | fileInspectorSupported={false} | |
| 107 | inspectorMode={null} | |
| 108 | onToggleInspector={() => {}} | |
| 109 | onOpenGitInspector={() => {}} | |
| 110 | onOpenFilesInspector={() => {}} | |
| 111 | /> | |
| 112 | ); | |
| 113 | } | |
| 114 | ||
| 115 | function renderedGitMenuItems(): Array<{ | |
| 116 | description?: unknown; | |
| 117 | label?: unknown; | |
| 118 | onPress?: () => void; | |
| 119 | }> { | |
| 120 | const items = harness.renderedRightItems.at(-1) ?? []; | |
| 121 | const git = items.find((item) => item.identifier === "thread-right-git") as | |
| 122 | | { menu: { items: Array<{ description?: unknown; label?: unknown; onPress?: () => void }> } } | |
| 123 | | undefined; | |
| 124 | return git?.menu.items ?? []; | |
| 125 | } | |
| 126 | ||
| 127 | function renderedGitStatusDescription(): unknown { | |
| 128 | return renderedGitMenuItems()[0]?.description; | |
| 129 | } | |
| 130 | ||
| 131 | let root: Root | null = null; | |
| 132 | ||
| 133 | function render(element: React.ReactElement) { | |
| 134 | (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; | |
| 135 | root ??= createRoot(document.createElement("div")); | |
| 136 | act(() => root!.render(element)); | |
| 137 | } | |
| 138 | ||
| 139 | afterEach(() => { | |
| 140 | act(() => root?.unmount()); | |
| 141 | root = null; | |
| 142 | harness.setOptionsCalls = 0; | |
| 143 | harness.renderedRightItems = []; | |
| 144 | harness.navigatedRoutes = []; | |
| 145 | }); | |
| 146 | ||
| 147 | describe("ThreadHeader", () => { | |
| 148 | it("re-applies native header items when Git status changes", () => { | |
| 149 | render(<Header gitStatus={status([])} />); | |
| 150 | expect(renderedGitStatusDescription()).toBe("Clean"); | |
| 151 | ||
| 152 | // A refreshed status (e.g. after reconnect) must reach the Git popover even | |
| 153 | // though nothing else in the header changed. | |
| 154 | render(<Header gitStatus={status(["audit.txt"])} />); | |
| 155 | expect(renderedGitStatusDescription()).toBe("1 changed"); | |
| 156 | ||
| 157 | render(<Header gitStatus={status([])} />); | |
| 158 | expect(renderedGitStatusDescription()).toBe("Clean"); | |
| 159 | }); | |
| 160 | ||
| 161 | it("does not re-apply options for re-renders with equivalent header content", () => { | |
| 162 | render(<Header gitStatus={status([])} />); | |
| 163 | const applied = harness.setOptionsCalls; | |
| 164 | ||
| 165 | // Streaming re-renders the route with fresh callbacks and equal Git content. | |
| 166 | for (let index = 0; index < 20; index += 1) { | |
| 167 | render(<Header gitStatus={status([])} />); | |
| 168 | } | |
| 169 | expect(harness.setOptionsCalls).toBe(applied); | |
| 170 | }); | |
| 171 | ||
| 172 | it("re-applies menu actions when the default ref changes behind the same label", async () => { | |
| 173 | // An unpushed commit on a branch with an open PR shows "Push" whether or not the | |
| 174 | // branch is the default one, but only the default branch asks for confirmation. | |
| 175 | const pushable = (isDefaultRef: boolean) => | |
| 176 | status([], { | |
| 177 | isDefaultRef, | |
| 178 | aheadCount: 1, | |
| 179 | hasPrimaryRemote: true, | |
| 180 | pr: { state: "open", url: "https://example.com/pr/1" }, | |
| 181 | } as Partial<VcsStatusResult>); | |
| 182 | render(<Header gitStatus={pushable(false)} />); | |
| 183 | expect(renderedGitMenuItems()[1]?.label).toBe("Push"); | |
| 184 | ||
| 185 | render(<Header gitStatus={pushable(true)} />); | |
| 186 | expect(renderedGitMenuItems()[1]?.label).toBe("Push"); | |
| 187 | await act(async () => renderedGitMenuItems()[1]?.onPress?.()); | |
| 188 | expect(harness.navigatedRoutes).toEqual(["GitConfirm"]); | |
| 189 | }); | |
| 190 | }); |
apps/mobile/src/features/threads/useThreadHeaderOptions.tsx+13| ... | ... | @@ -108,8 +108,21 @@ export function useThreadHeaderOptions(props: { |
| 108 | 108 | unstable_headerSubtitle: props.usesNativeHeaderGlass ? props.subtitle : undefined, |
| 109 | 109 | contentStyle: undefined, |
| 110 | 110 | }; |
| 111 | const { environmentId, threadId, gitStatus } = props.gitControls; | |
| 111 | 112 | return { |
| 112 | 113 | options, |
| 114 | // Header item factories are stabilized, so the native header only re-reads them when | |
| 115 | // this version changes. Keying on the items keeps the Git menu status live; the menu | |
| 116 | // callbacks also read state the items do not display (a "Push" item runs `push` or | |
| 117 | // `commit_push` depending on the default ref), so that state is keyed too. | |
| 118 | optionsVersion: [ | |
| 119 | layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems, | |
| 120 | environmentId, | |
| 121 | threadId, | |
| 122 | gitStatus?.isDefaultRef, | |
| 123 | gitStatus?.refName, | |
| 124 | gitStatus?.pr?.url, | |
| 125 | ], | |
| 113 | 126 | sidebar: false, |
| 114 | 127 | fallback: |
| 115 | 128 | !layout.usesSplitView && !props.usesNativeHeaderGlass ? ( |