Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions apps/mobile/src/features/threads/PendingUserInputCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
type DerivedValue,
type SharedValue,
} from "react-native-reanimated";

Expand All @@ -33,7 +34,7 @@ import {
export interface PendingUserInputCardProps {
readonly canOperateThread: boolean;
readonly pendingUserInput: PendingUserInput;
readonly maxHeight: number;
readonly maxHeight: DerivedValue<number>;
readonly collapsed: boolean;
readonly onToggleCollapsed: () => void;
readonly onStopThread?: () => void;
Expand Down Expand Up @@ -121,12 +122,16 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
[cardHeight, notifyCoverage],
);
const cardProgress = props.cardProgress;
const maxHeight = props.maxHeight;
const cardAnimatedStyle = useAnimatedStyle(() => {
const progress = cardProgress === undefined ? 1 : cardProgress.value;
return {
maxHeight: maxHeight.value,
transform: [{ translateY: (1 - progress) * cardHeight.value }],
};
});
const cardMaxHeightStyle = useAnimatedStyle(() => ({ maxHeight: maxHeight.value }));
const overlayHeightStyle = useAnimatedStyle(() => ({ height: maxHeight.value }));

// On iOS the card stays MOUNTED while collapsed (hidden via the animated
// style): expanding animates existing views on the UI thread the same
Expand Down Expand Up @@ -194,11 +199,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
}
layout={CARD_LAYOUT_TRANSITION}
className="overflow-hidden gap-2.5 rounded-[20px] border border-border bg-card-alt p-4"
style={
EXPANDED_CARD_IS_OVERLAY
? [{ maxHeight: props.maxHeight }, cardAnimatedStyle]
: { maxHeight: props.maxHeight }
}
style={EXPANDED_CARD_IS_OVERLAY ? cardAnimatedStyle : cardMaxHeightStyle}
>
<Pressable
accessibilityRole="button"
Expand Down Expand Up @@ -330,13 +331,13 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
<View className="relative">
{bar}
{EXPANDED_CARD_IS_OVERLAY ? (
<View
<Animated.View
pointerEvents={props.collapsed ? "none" : "box-none"}
className="absolute inset-x-0 bottom-0 justify-end overflow-hidden"
style={{ height: props.maxHeight }}
style={overlayHeightStyle}
>
{card}
</View>
</Animated.View>
) : (
card
)}
Expand Down
41 changes: 22 additions & 19 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
KeyboardController,
KeyboardStickyView,
useKeyboardState,
useReanimatedKeyboardAnimation,
} from "react-native-keyboard-controller";
import Animated, {
Easing,
Expand Down Expand Up @@ -878,10 +879,8 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai
);
const userInputCollapsed =
activeUserInputRequestId !== null && collapsedUserInputRequestId === activeUserInputRequestId;
// The card's height RESERVES keyboard space at all times instead of tracking the keyboard:
// transforms (the sticky translation) apply same-frame on the UI thread while layout props lag
// a Yoga pass behind, so any height that follows the keyboard flashes the card over the nav
// header on the way up.
// A height React sets from keyboard state lags the sticky translation by a Yoga pass and
// flashes over the nav header on the way up, so the usage panel reserves the keyboard instead.
const [lastKnownKeyboardHeight, setLastKnownKeyboardHeight] = useState(0);
useEffect(() => {
if (liveKeyboardHeight > 0 && liveKeyboardHeight !== lastKnownKeyboardHeight) {
Expand All @@ -890,12 +889,23 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai
}, [lastKnownKeyboardHeight, liveKeyboardHeight]);
const reservedKeyboardHeight =
lastKnownKeyboardHeight > 0 ? lastKnownKeyboardHeight : ESTIMATED_KEYBOARD_HEIGHT;
const pendingUserInputMaxHeight = derivePendingUserInputMaxHeight({
windowHeight,
keyboardHeight: reservedKeyboardHeight,
navigationHeaderHeight,
composerOverlapHeight: composerBottomInset,
});
// Waiting for a visibility render misses the first keyboard frames.
const keyboardStickyEnabled =
Platform.OS === "ios"
? deviceType !== "duo-unfolded" || (composerOwnsKeyboard && !sidebarSearchActive)
: !keyboardStateSuspect;
// The question card reads the keyboard frame on the UI thread, in the same frame the sticky
// view translates, so it gets the full height while the keyboard is closed and never crosses
// the header while the keyboard moves.
const { height: keyboardTranslation } = useReanimatedKeyboardAnimation();
const pendingUserInputMaxHeight = useDerivedValue(() =>
derivePendingUserInputMaxHeight({
windowHeight,
keyboardHeight: keyboardStickyEnabled ? -keyboardTranslation.value : 0,
navigationHeaderHeight,
composerOverlapHeight: composerBottomInset,
}),
);
const estimatedOverlayHeight = composerOverlapHeight;
// The overlay's measured height includes the home-indicator inset (the
// composer pads it), but contentInsetAdjustmentBehavior="automatic" makes
Expand Down Expand Up @@ -946,9 +956,7 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai
usageLimitsLayout.overlay > 0
? usageLimitsLayout.overlay - usageLimitsLayout.panel
: composerOverlapHeight;
// The keyboard is reserved even while closed, like the question card, so the
// panel never grows over the header while the keyboard animates. The floor
// keeps the close row on screen when the stack below leaves no room at all.
// The floor keeps the close row on screen when the stack below leaves no room at all.
const usageLimitsMaxHeight = Math.max(
USAGE_LIMITS_PANEL_MIN_HEIGHT,
Math.min(
Expand Down Expand Up @@ -1456,12 +1464,7 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai

{showContent ? (
<KeyboardStickyView
// Waiting for a visibility render misses the first keyboard frames.
enabled={
Platform.OS === "ios"
? deviceType !== "duo-unfolded" || (composerOwnsKeyboard && !sidebarSearchActive)
: !keyboardStateSuspect
}
enabled={keyboardStickyEnabled}
pointerEvents="box-none"
style={{ position: "absolute", bottom: 0, left: 0, right: 0, top: 0 }}
offset={{ closed: 0, opened: composerKeyboardOffset }}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/pendingUserInputLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function derivePendingUserInputMaxHeight(input: {
readonly navigationHeaderHeight: number;
readonly composerOverlapHeight: number;
}): number {
"worklet";
const availableHeight =
input.windowHeight -
Math.max(0, input.keyboardHeight) -
Expand Down
Loading