diff --git a/apps/mobile/src/features/threads/PendingApprovalCard.tsx b/apps/mobile/src/features/threads/PendingApprovalCard.tsx index f9ec44d5d4ee5..3ea89ad46d5d5 100644 --- a/apps/mobile/src/features/threads/PendingApprovalCard.tsx +++ b/apps/mobile/src/features/threads/PendingApprovalCard.tsx @@ -10,6 +10,11 @@ import { } from "@t3tools/client-runtime/state/thread-requests"; import { useState } from "react"; import { Pressable, View } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + type DerivedValue, +} from "react-native-reanimated"; import { AppText as Text } from "../../components/ui/AppText"; import type { PendingApproval } from "../../lib/threadActivity"; @@ -18,6 +23,7 @@ export interface PendingApprovalCardProps { readonly canOperateThread: boolean; readonly approval: PendingApproval; readonly respondingApprovalId: RuntimeRequestId | null; + readonly maxHeight?: DerivedValue; readonly onRespond: ( requestId: RuntimeRequestId, decision: ProviderApprovalDecision, @@ -30,6 +36,8 @@ const DEFAULT_APPROVAL_OPTIONS: ReadonlyArray = [ { decision: "decline", label: "Decline" }, ]; +const BODY_MIN_HEIGHT = 64; + export function PendingApprovalCard(props: PendingApprovalCardProps) { const options: ReadonlyArray = props.approval.options ?? DEFAULT_APPROVAL_OPTIONS; @@ -37,40 +45,71 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) { const canRespond = props.approval.responseCapability === "live"; const disabled = !canRespond || props.respondingApprovalId === props.approval.requestId; const [explanationOpen, setExplanationOpen] = useState(false); + const { maxHeight } = props; + const cardHeight = useSharedValue(0); + const bodyHeight = useSharedValue(0); + // The body takes what the pinned rows leave. Below BODY_MIN_HEIGHT the caller's capped + // ScrollView scrolls the whole card instead, so the buttons stay reachable. + const bodyHeightStyle = useAnimatedStyle(() => + maxHeight === undefined + ? {} + : { + maxHeight: Math.max( + BODY_MIN_HEIGHT, + maxHeight.value - (cardHeight.value - bodyHeight.value), + ), + }, + ); return ( - - - Approval needed - - - {props.approval.appName ?? props.approval.requestKind} - - {props.approval.detail ? ( - - {props.approval.detail} + { + cardHeight.set(event.nativeEvent.layout.height); + }} + > + { + bodyHeight.set(event.nativeEvent.layout.height); + }} + style={bodyHeightStyle} + > + + Approval needed - ) : null} - {props.approval.reason ? ( - - {props.approval.reason} + + {props.approval.appName ?? props.approval.requestKind} - ) : null} - {props.approval.fullAccessOverride ? ( - setExplanationOpen((open) => !open)} - > - - {FULL_ACCESS_OVERRIDE_QUESTION} + {props.approval.detail ? ( + + {props.approval.detail} + + ) : null} + {props.approval.reason ? ( + + {props.approval.reason} - {explanationOpen ? ( - - {fullAccessOverrideExplanation(props.approval.fullAccessOverride)} + ) : null} + {props.approval.fullAccessOverride ? ( + setExplanationOpen((open) => !open)} + > + + {FULL_ACCESS_OVERRIDE_QUESTION} - ) : null} - - ) : null} + {explanationOpen ? ( + + {fullAccessOverrideExplanation(props.approval.fullAccessOverride)} + + ) : null} + + ) : null} + {!canRespond ? ( The provider process for this request is no longer available. Interrupt or restart the run diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index d9bb48682f894..dd4814edc8984 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -1063,13 +1063,16 @@ function ThreadDetailBody( flexGrow: 0, })); // Questions overlay the composer on iOS; an approval sits above it, so it gives up its height. - const approvalHeightStyle = useAnimatedStyle(() => ({ - maxHeight: derivePendingUserInputMaxHeight({ + const pendingApprovalMaxHeight = useDerivedValue(() => + derivePendingUserInputMaxHeight({ windowHeight: screenAreaHeight, keyboardHeight: keyboardStickyEnabled ? -keyboardTranslation.value : 0, navigationHeaderHeight, composerOverlapHeight, }), + ); + const approvalHeightStyle = useAnimatedStyle(() => ({ + maxHeight: pendingApprovalMaxHeight.value, flexGrow: 0, })); // The usage panel stacks with the composer and any approval card, so it gets @@ -1757,6 +1760,7 @@ function ThreadDetailBody(