diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 5bb721a..821dd44 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -1254,11 +1254,11 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta name="AIChat" component={AIChatScreen} options={{ - animation: Platform.OS === 'android' ? 'none' : 'slide_from_bottom', + animation: Platform.OS === 'android' ? 'none' : 'slide_from_right', animationDuration: Platform.OS === 'android' ? 220 : 300, - presentation: 'modal', + presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'modal', gestureEnabled: true, - gestureDirection: Platform.OS === 'android' ? 'vertical' : 'vertical', + gestureDirection: Platform.OS === 'ios' ? 'horizontal' : 'vertical', headerShown: false, contentStyle: { backgroundColor: currentTheme.colors.darkBackground, diff --git a/src/screens/AIChatScreen.tsx b/src/screens/AIChatScreen.tsx index 953ae82..2c28ccd 100644 --- a/src/screens/AIChatScreen.tsx +++ b/src/screens/AIChatScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { View, Text, @@ -64,7 +64,7 @@ interface ChatBubbleProps { isLast: boolean; } -const ChatBubble: React.FC = ({ message, isLast }) => { +const ChatBubble: React.FC = React.memo(({ message, isLast }) => { const { currentTheme } = useTheme(); const isUser = message.role === 'user'; @@ -265,14 +265,22 @@ const ChatBubble: React.FC = ({ message, isLast }) => { )} ); -}; +}, (prev, next) => { + return ( + prev.isLast === next.isLast && + prev.message.id === next.message.id && + prev.message.role === next.message.role && + prev.message.content === next.message.content && + prev.message.timestamp === next.message.timestamp + ); +}); interface SuggestionChipProps { text: string; onPress: () => void; } -const SuggestionChip: React.FC = ({ text, onPress }) => { +const SuggestionChip: React.FC = React.memo(({ text, onPress }) => { const { currentTheme } = useTheme(); return ( @@ -286,7 +294,7 @@ const SuggestionChip: React.FC = ({ text, onPress }) => { ); -}; +}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress); const AIChatScreen: React.FC = () => { const route = useRoute(); @@ -329,20 +337,35 @@ const AIChatScreen: React.FC = () => { loadContext(); }, []); - // Track keyboard to adjust bottom padding only when hidden on iOS + // Track keyboard and animate input to avoid gaps on iOS useEffect(() => { + const onShow = (e: any) => { + setIsKeyboardVisible(true); + if (Platform.OS === 'ios') { + const kbHeight = e?.endCoordinates?.height ?? 0; + const lift = Math.max(0, kbHeight - insets.bottom); + inputContainerY.value = withTiming(-lift, { duration: 220 }); + } + }; + const onHide = () => { + setIsKeyboardVisible(false); + if (Platform.OS === 'ios') { + inputContainerY.value = withTiming(0, { duration: 220 }); + } + }; + const showSub = Platform.OS === 'ios' - ? Keyboard.addListener('keyboardWillShow', () => setIsKeyboardVisible(true)) - : Keyboard.addListener('keyboardDidShow', () => setIsKeyboardVisible(true)); + ? Keyboard.addListener('keyboardWillShow', onShow) + : Keyboard.addListener('keyboardDidShow', onShow); const hideSub = Platform.OS === 'ios' - ? Keyboard.addListener('keyboardWillHide', () => setIsKeyboardVisible(false)) - : Keyboard.addListener('keyboardDidHide', () => setIsKeyboardVisible(false)); + ? Keyboard.addListener('keyboardWillHide', onHide) + : Keyboard.addListener('keyboardDidHide', onHide); return () => { showSub.remove(); hideSub.remove(); }; - }, []); + }, [insets.bottom, inputContainerY]); // Animate in on Android for full-screen modal feel useEffect(() => { @@ -601,7 +624,7 @@ const AIChatScreen: React.FC = () => { styles.header, { backgroundColor: 'transparent', - paddingTop: Platform.OS === 'ios' ? 8 : insets.top + paddingTop: Platform.OS === 'ios' ? insets.top : insets.top }, headerAnimatedStyle ]}> @@ -639,17 +662,19 @@ const AIChatScreen: React.FC = () => { {/* Chat Messages */} {messages.length === 0 && suggestions.length > 0 && ( @@ -711,7 +736,7 @@ const AIChatScreen: React.FC = () => { styles.inputContainer, { backgroundColor: 'transparent', - paddingBottom: Platform.OS === 'ios' ? (isKeyboardVisible ? 12 : 56) : 12 + paddingBottom: 12 }, inputAnimatedStyle ]}>