fix(keyboard): use transform instead of padding for keyboard animation

This commit is contained in:
Mohamed Boudra
2025-10-26 16:19:30 +01:00
parent 985e9db6db
commit cf7e14e36b
3 changed files with 40 additions and 45 deletions
+33 -23
View File
@@ -3,7 +3,7 @@ import { View, Text, ActivityIndicator } from "react-native";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import ReanimatedAnimated, { useAnimatedStyle } from "react-native-reanimated"; import ReanimatedAnimated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header"; import { BackHeader } from "@/components/headers/back-header";
import { AgentStreamView } from "@/components/agent-stream-view"; import { AgentStreamView } from "@/components/agent-stream-view";
@@ -20,13 +20,18 @@ export default function AgentScreen() {
// Keyboard animation // Keyboard animation
const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
const bottomInset = insets.bottom; const bottomInset = useSharedValue(insets.bottom);
useEffect(() => {
bottomInset.value = insets.bottom;
}, [insets.bottom, bottomInset]);
const animatedKeyboardStyle = useAnimatedStyle(() => { const animatedKeyboardStyle = useAnimatedStyle(() => {
"worklet"; "worklet";
const absoluteHeight = Math.abs(keyboardHeight.value); const absoluteHeight = Math.abs(keyboardHeight.value);
const padding = Math.max(0, absoluteHeight - bottomInset); const shift = Math.max(0, absoluteHeight - bottomInset.value);
return { return {
paddingBottom: padding, transform: [{ translateY: -shift }],
}; };
}); });
@@ -81,25 +86,26 @@ export default function AgentScreen() {
<BackHeader title={agent.title || "Agent"} /> <BackHeader title={agent.title || "Agent"} />
{/* Content Area with Keyboard Animation */} {/* Content Area with Keyboard Animation */}
<ReanimatedAnimated.View style={[styles.content, animatedKeyboardStyle]}> <View style={styles.contentContainer}>
{isInitializing ? ( <ReanimatedAnimated.View style={[styles.content, animatedKeyboardStyle]}>
<View style={styles.loadingContainer}> {isInitializing ? (
<ActivityIndicator size="large" color={theme.colors.primary} /> <View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Loading agent...</Text> <ActivityIndicator size="large" color={theme.colors.primary} />
</View> <Text style={styles.loadingText}>Loading agent...</Text>
) : ( </View>
<AgentStreamView ) : (
agentId={id!} <AgentStreamView
agent={agent} agentId={id!}
streamItems={streamItems} agent={agent}
pendingPermissions={agentPermissions} streamItems={streamItems}
onPermissionResponse={(requestId, optionId) => pendingPermissions={agentPermissions}
respondToPermission(requestId, id!, agent.sessionId || "", [optionId]) onPermissionResponse={(requestId, optionId) =>
} respondToPermission(requestId, id!, agent.sessionId || "", [optionId])
keyboardHeight={keyboardHeight} }
/> />
)} )}
</ReanimatedAnimated.View> </ReanimatedAnimated.View>
</View>
</View> </View>
); );
} }
@@ -109,6 +115,10 @@ const styles = StyleSheet.create((theme) => ({
flex: 1, flex: 1,
backgroundColor: theme.colors.background, backgroundColor: theme.colors.background,
}, },
contentContainer: {
flex: 1,
overflow: "hidden",
},
content: { content: {
flex: 1, flex: 1,
}, },
@@ -3,7 +3,7 @@ import { View, Text, ScrollView, Pressable } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { BottomSheetModal } from "@gorhom/bottom-sheet"; import { BottomSheetModal } from "@gorhom/bottom-sheet";
import Animated, { FadeIn, FadeOut, useAnimatedStyle, SharedValue } from "react-native-reanimated"; import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { ChevronDown } from "lucide-react-native"; import { ChevronDown } from "lucide-react-native";
import { import {
AssistantMessage, AssistantMessage,
@@ -25,7 +25,6 @@ export interface AgentStreamViewProps {
streamItems: StreamItem[]; streamItems: StreamItem[];
pendingPermissions: Map<string, PendingPermission>; pendingPermissions: Map<string, PendingPermission>;
onPermissionResponse: (requestId: string, optionId: string) => void; onPermissionResponse: (requestId: string, optionId: string) => void;
keyboardHeight: SharedValue<number>;
} }
export function AgentStreamView({ export function AgentStreamView({
@@ -34,7 +33,6 @@ export function AgentStreamView({
streamItems, streamItems,
pendingPermissions, pendingPermissions,
onPermissionResponse, onPermissionResponse,
keyboardHeight,
}: AgentStreamViewProps) { }: AgentStreamViewProps) {
const scrollViewRef = useRef<ScrollView>(null); const scrollViewRef = useRef<ScrollView>(null);
const bottomSheetRef = useRef<BottomSheetModal | null>(null); const bottomSheetRef = useRef<BottomSheetModal | null>(null);
@@ -44,17 +42,6 @@ export function AgentStreamView({
const [isNearBottom, setIsNearBottom] = useState(true); const [isNearBottom, setIsNearBottom] = useState(true);
const hasScrolledInitially = useRef(false); const hasScrolledInitially = useRef(false);
// Animated content padding that responds to keyboard
const contentPaddingStyle = useAnimatedStyle(() => {
"worklet";
const absoluteHeight = Math.abs(keyboardHeight.value);
const keyboardPadding = Math.max(0, absoluteHeight - insets.bottom);
const basePadding = Math.max(insets.bottom, 32);
return {
paddingBottom: basePadding + keyboardPadding,
};
}, [insets.bottom]);
// Scroll to bottom immediately on initial load, then animate for new messages // Scroll to bottom immediately on initial load, then animate for new messages
useEffect(() => { useEffect(() => {
if (isNearBottom) { if (isNearBottom) {
@@ -95,12 +82,10 @@ export function AgentStreamView({
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
style={stylesheet.scrollView} style={stylesheet.scrollView}
contentContainerStyle={[ contentContainerStyle={{
{ paddingTop: 24,
paddingTop: 24, paddingBottom: Math.max(insets.bottom, 32),
}, }}
contentPaddingStyle,
]}
onScroll={handleScroll} onScroll={handleScroll}
scrollEventThrottle={16} scrollEventThrottle={16}
> >
+2 -2
View File
@@ -374,13 +374,13 @@
{ {
"id": "97423777-33d3-414a-ae79-ea303230010a", "id": "97423777-33d3-414a-ae79-ea303230010a",
"title": "Commit Local Changes", "title": "Commit Local Changes",
"sessionId": "019a20d4-5fd4-76db-8021-697b8204597e", "sessionId": "addbd42f-dbb1-4536-bc2b-e70a64587de6",
"options": { "options": {
"type": "claude", "type": "claude",
"sessionId": "addbd42f-dbb1-4536-bc2b-e70a64587de6" "sessionId": "addbd42f-dbb1-4536-bc2b-e70a64587de6"
}, },
"createdAt": "2025-10-26T14:02:59.844Z", "createdAt": "2025-10-26T14:02:59.844Z",
"lastActivityAt": "2025-10-26T14:03:22.696Z", "lastActivityAt": "2025-10-26T14:08:58.462Z",
"cwd": "/Users/moboudra/dev/voice-dev" "cwd": "/Users/moboudra/dev/voice-dev"
} }
] ]