From b90c9c874f692be07055fe25ff9d34604a231aae Mon Sep 17 00:00:00 2001 From: Kyle Kincer Date: Mon, 3 Aug 2026 12:11:19 -0400 Subject: [PATCH] fix(mobile): keep pending input card below the navigation header The pending approval / user-input cards render inside the bottom-anchored composer overlay with no height cap, so a card with several questions grows upward underneath the transparent navigation header. Cap the card stack to the space between the header and the composer and make it scrollable. Co-Authored-By: Claude Fable 5 --- .../features/threads/ThreadDetailScreen.tsx | 80 ++++++++++++++----- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 5cb04290f66..ce962ff5cf3 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -2,6 +2,7 @@ import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connect import type { EnvironmentThreadStatus } from "@t3tools/client-runtime/state/threads"; import { useKeyboardChatComposerInset, useKeyboardScrollToEnd } from "@legendapp/list/keyboard"; import type { LegendListRef } from "@legendapp/list/react-native"; +import { HeaderHeightContext } from "@react-navigation/elements"; import type { ApprovalRequestId, EnvironmentId, @@ -15,8 +16,23 @@ import type { ThreadId, } from "@t3tools/contracts"; import * as Haptics from "expo-haptics"; -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; -import { Platform, View, type GestureResponderEvent } from "react-native"; +import { + memo, + useCallback, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Platform, + ScrollView, + useWindowDimensions, + View, + type GestureResponderEvent, +} from "react-native"; import { KeyboardController, KeyboardStickyView } from "react-native-keyboard-controller"; import Animated, { FadeInDown, FadeOut } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -202,6 +218,21 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const selectedThreadFeed = props.selectedThreadFeed; const composerChrome = composerExpanded ? COMPOSER_EXPANDED_CHROME : COMPOSER_COLLAPSED_CHROME; const composerOverlapHeight = composerChrome + composerBottomInset; + const windowDimensions = useWindowDimensions(); + // The pending approval/user-input cards live in the bottom-anchored composer + // overlay, so a card with many questions grows upward past the navigation + // header. Cap the card stack to the space between the header and the + // composer and let it scroll instead. Read the context directly + // (useHeaderHeight throws outside a header-providing screen) and fall back + // to the standard iOS bar height. + const navigationHeaderHeight = useContext(HeaderHeightContext); + const pendingCardsMaxHeight = Math.max( + 120, + windowDimensions.height - + (navigationHeaderHeight || insets.top + 44) - + composerOverlapHeight - + 12, + ); const estimatedOverlayHeight = composerOverlapHeight; // The overlay's measured height includes the home-indicator inset (the // composer pads it), but contentInsetAdjustmentBehavior="automatic" makes @@ -390,28 +421,35 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread {props.activePendingApproval || props.activePendingUserInput ? ( - {props.activePendingApproval ? ( - - ) : null} - {props.activePendingUserInput ? ( - - ) : null} + + {props.activePendingApproval ? ( + + ) : null} + {props.activePendingUserInput ? ( + + ) : null} + ) : null}