From 2d2efff28c4fc47745538570fcb7dd07272e3e4b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 24 Aug 2026 02:52:26 -0700 Subject: [PATCH] fix(mobile): restore Android tablet thread controls, clean up header (#5385) --- .../src/components/CompactBrandTitle.tsx | 5 +- .../src/features/home/AndroidHomeFab.tsx | 4 +- .../src/features/home/HomeRouteScreen.tsx | 31 ++-- .../layout/AdaptiveWorkspaceLayout.tsx | 33 +++-- .../threads/ThreadNavigationSidebar.tsx | 133 +++--------------- .../threads/sidebar-filter-button.tsx | 20 +-- .../threads/sidebar-header-actions.tsx | 22 +-- 7 files changed, 73 insertions(+), 175 deletions(-) diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx index 28f7cfe57a7fbb1a1965e2184403867d3f4ebfd3..bfba418c9fce310915efc1d3aae43feae3babbc5 100644 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -33,6 +33,7 @@ export function brandTitleOffset(nativeLeadingItem: boolean): number { */ export function CompactBrandTitle( props: { + readonly allowFontScaling?: boolean; readonly nativeLeadingItem?: boolean; } = {}, ) { @@ -57,6 +58,7 @@ export function CompactBrandTitle( > ; + return ; } export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { diff --git a/apps/mobile/src/features/home/AndroidHomeFab.tsx b/apps/mobile/src/features/home/AndroidHomeFab.tsx index 5c7d5a0b988c3849c4f9d4c9a97d04cfb04cb05a..c57964fce4a373751ac7474dee927338a1e36d60 100644 --- a/apps/mobile/src/features/home/AndroidHomeFab.tsx +++ b/apps/mobile/src/features/home/AndroidHomeFab.tsx @@ -6,8 +6,8 @@ import { SymbolView } from "../../components/AppSymbol"; import { useThemeColor } from "../../lib/useThemeColor"; /** - * Android-only wrapper that overlays a bottom-right new-task FAB on the home - * screen. Other platforms render children unchanged. + * Android-only wrapper that overlays a bottom-right new-task FAB on a thread + * list. Other platforms render children unchanged. */ export function AndroidHomeFabLayout(props: { readonly onStartNewTask: () => void; diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 8061b1d1e85b8dfc014e9357e35426bba0fbb935..beabf66d9ea9f5d9338fd2c6c69327d48a68b5b2 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -2,6 +2,7 @@ import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { useNavigation } from "@react-navigation/native"; import { useEffect, useMemo, useState } from "react"; +import { Platform } from "react-native"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { useProjects, useThreadShells } from "../../state/entities"; @@ -106,7 +107,11 @@ export function HomeRouteScreen() { return ( <> [] }} + options={ + Platform.OS === "android" + ? { headerShown: false } + : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] } + } /> navigation.navigate("NewTaskSheet", { screen: "NewTask" })} > <> - {/* Restore the compact title after the split branch blanks the detail - header. The 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). */} + {/* Restore the header after leaving split view; screen options are + shallow-merged. The brand slot also doubles as the connection + status surface while an environment reconnects. */} - navigation.navigate("SettingsSheet", { - screen: "SettingsContent", - params: { screen: "SettingsEnvironments" }, - }), - })} + options={{ + ...getConnectionAwareBrandHeaderOptions({ + onOpenEnvironments: () => + navigation.navigate("SettingsSheet", { + screen: "SettingsContent", + params: { screen: "SettingsEnvironments" }, + }), + }), + headerShown: true, + }} /> { + navigation.navigate("NewTaskSheet", { screen: "NewTask" }); + }, [navigation]); + // Minted here (root stack navigation) so the sidebar pane stays free of // navigation hooks — on iOS it renders inside an independent nav tree. const handleOpenEnvironmentSettings = useCallback(() => { @@ -528,18 +533,22 @@ function AdaptiveWorkspaceLayoutContent( pointerEvents={panes.primarySidebarVisible ? "auto" : "none"} style={sidebarAnimatedStyle} > - + + + + + ) : null} diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 856e5ac23435740d827db7a15c235a0ed164624f..6feca0013527b4398989e30fab3177bffeba430b 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1,4 +1,3 @@ -import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; import type { EnvironmentProject, EnvironmentThreadShell, @@ -14,16 +13,16 @@ import { AsyncResult } from "effect/unstable/reactivity"; import type { EnvironmentId } from "@t3tools/contracts"; import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort"; import type { ChangeRequestSettleSource } from "@t3tools/client-runtime/state/thread-settled"; -import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; -import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { LayoutChangeEvent } from "react-native"; import { Platform, Pressable, StyleSheet, TextInput, View } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { SearchBarCommands } from "react-native-screens"; -import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; +import { CompactBrandTitle } from "../../components/CompactBrandTitle"; import { ControlPillMenu } from "../../components/ControlPill"; import { SymbolView } from "../../components/AppSymbol"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; @@ -40,7 +39,6 @@ import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; import { useWorkspaceState } from "../../state/workspace"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; -import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { hasCustomHomeListOptions, PROJECT_SORT_OPTIONS, @@ -97,48 +95,7 @@ type SidebarListItem = | ThreadListV2ListItem | { readonly type: "v2-show-more"; readonly key: string; readonly hiddenCount: number }; -/** - * Shared capsule behind the sidebar header buttons — a native liquid-glass - * surface on iOS 26+, a tinted pill everywhere else. - */ -function SidebarHeaderButtonGroup(props: { - readonly children: ReactNode; - readonly colorScheme: "light" | "dark"; -}) { - const fallbackBackground = useThemeColor("--color-glass-surface"); - const fallbackBorder = useThemeColor("--color-header-border"); - if (isLiquidGlassSupported) { - return ( - - {props.children} - - ); - } - - return ( - - {props.children} - - ); -} - const SIDEBAR_STICKY_HEADER_HEIGHT = 106; -const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 44; -const SIDEBAR_HEADER_WASH_OPACITY = { - dark: [0.22, 0.14, 0.04], - light: [0.46, 0.3, 0.08], -} as const; interface ThreadNavigationSidebarProps { readonly width: number; @@ -195,16 +152,13 @@ function ThreadNavigationSidebarPane( props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean }, ) { const insets = useSafeAreaInsets(); - const { themeAppearance: colorScheme } = useAppearancePreferences(); const projects = useProjects(); const threads = useThreadShells(); const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); - const [headerIsOverContent, setHeaderIsOverContent] = useState(false); const searchInputRef = useRef(null); const searchBarRef = useRef(null); const openSwipeableRef = useRef(null); - const headerIsOverContentRef = useRef(false); const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); const { archiveThread, @@ -780,8 +734,6 @@ function ThreadNavigationSidebarPane( const borderColor = useThemeColor("--color-border"); const mutedColor = useThemeColor("--color-foreground-muted"); const placeholderColor = useThemeColor("--color-placeholder"); - const headerFadeColor = String(backgroundColor); - const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(null); // The sticky header (title row, search field, optional connection status) // is measured so the list inset always matches its real height — no @@ -810,19 +762,10 @@ function ThreadNavigationSidebarPane( }, [props.onSelectThread], ); - const handleScroll = useCallback((event: NativeSyntheticEvent) => { - const next = event.nativeEvent.contentOffset.y > 6; - if (headerIsOverContentRef.current === next) { - return; - } - headerIsOverContentRef.current = next; - setHeaderIsOverContent(next); - }, []); const handleScrollBeginDrag = useCallback(() => { openSwipeableRef.current?.close(); }, []); const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ - onScroll: handleScroll, onScrollBeginDrag: handleScrollBeginDrag, }); // Project shells load after the first rows draw, so the maps they feed have @@ -1321,7 +1264,10 @@ function ThreadNavigationSidebarPane( contentContainerStyle={[ styles.threadListContent, { - paddingBottom: 16 + insets.bottom, + paddingBottom: + Platform.OS === "android" + ? Math.max(insets.bottom, 16) + 88 - insets.bottom + : 16 + insets.bottom, paddingTop: topListInset, }, ]} @@ -1340,65 +1286,34 @@ function ThreadNavigationSidebarPane( - - - - - - - - - - - - - {/* Title slot doubles as the connection status surface: while an - environment reconnects, "Threads" fades to a status label in + environment reconnects, the brand fades to a status label in place (no layout shift in the list below). */} - Threads - + + + } /> - + - + - - + + @@ -1423,12 +1338,6 @@ function ThreadNavigationSidebarPane( } const styles = StyleSheet.create({ - headerButtonGroup: { - alignItems: "center", - borderRadius: 22, - flexDirection: "row", - overflow: "hidden", - }, threadList: { flex: 1, }, diff --git a/apps/mobile/src/features/threads/sidebar-filter-button.tsx b/apps/mobile/src/features/threads/sidebar-filter-button.tsx index 0c33da436a572f4eefdc38beb8a8b9a163a0b5d1..1895ef0d45ca398258e601854e14367097d8df5a 100644 --- a/apps/mobile/src/features/threads/sidebar-filter-button.tsx +++ b/apps/mobile/src/features/threads/sidebar-filter-button.tsx @@ -1,5 +1,5 @@ import { SymbolView } from "../../components/AppSymbol"; -import { Pressable, StyleSheet } from "react-native"; +import { Pressable } from "react-native"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -10,31 +10,17 @@ export type SidebarFilterButtonIcon = export function SidebarFilterButton(props: { readonly accessibilityLabel: string; readonly icon: SidebarFilterButtonIcon; - /** Rendered inside a shared capsule group — no own background/border. */ - readonly grouped?: boolean; }) { const iconColor = useThemeColor("--color-foreground"); - const pressedBackgroundColor = useThemeColor("--color-subtle"); - const idleBackgroundColor = useThemeColor("--color-glass-surface"); - const borderColor = useThemeColor("--color-header-border"); return ( [ - props.grouped - ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } - : { - backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, - borderColor, - borderWidth: StyleSheet.hairlineWidth, - }, - ]} > - + ); } diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.tsx index b0f5f1131f68805ec1954779654922a671cee05e..9ce77f8991bdbd91b7ddf9a6b4f3f257412ebbee 100644 --- a/apps/mobile/src/features/threads/sidebar-header-actions.tsx +++ b/apps/mobile/src/features/threads/sidebar-header-actions.tsx @@ -1,43 +1,28 @@ import { SymbolView } from "../../components/AppSymbol"; -import { Pressable, StyleSheet, View } from "react-native"; +import { Pressable, View } from "react-native"; import { useThemeColor } from "../../lib/useThemeColor"; export interface SidebarHeaderActionsProps { readonly onOpenSettings: () => void; - /** Rendered inside a shared capsule group — buttons drop their own chrome. */ - readonly grouped?: boolean; } function FallbackHeaderButton(props: { readonly accessibilityLabel: string; readonly icon: "gearshape" | "square.and.pencil"; - readonly grouped?: boolean; readonly onPress: () => void; }) { const iconColor = useThemeColor("--color-foreground"); - const pressedBackgroundColor = useThemeColor("--color-subtle"); - const idleBackgroundColor = useThemeColor("--color-glass-surface"); - const borderColor = useThemeColor("--color-header-border"); return ( [ - props.grouped - ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } - : { - backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, - borderColor, - borderWidth: StyleSheet.hairlineWidth, - }, - ]} > - + ); } @@ -47,7 +32,6 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { -- 2.54.0