Skip to content

Commit c4e4434

Browse files
kfaracikclaude
andcommitted
Merge branch 'main' into chore/229-migrate-swmansion-bottom-sheet
Bring in the scroll-down-button overlap fix (#251). Resolve the ChatScreen import conflict: keep AppBottomSheetRef (swmansion bottom-sheet migration) and adopt scroll-down's useReanimatedKeyboardAnimation, dropping the now-unused KeyboardStickyView and @gorhom BottomSheetModal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 parents a73d472 + 7a1c8fa commit c4e4434

4 files changed

Lines changed: 108 additions & 26 deletions

File tree

__mocks__/react-native-reanimated.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ const Animated = {
3232
createAnimatedComponent,
3333
};
3434

35+
type AnimationBuilder = Record<
36+
string,
37+
(...args: unknown[]) => AnimationBuilder
38+
>;
39+
40+
const makeAnimationBuilder = (): AnimationBuilder => {
41+
const builder = new Proxy(
42+
{},
43+
{ get: () => () => builder }
44+
) as AnimationBuilder;
45+
return builder;
46+
};
47+
3548
module.exports = {
3649
__esModule: true,
3750
default: Animated,
@@ -73,4 +86,9 @@ module.exports = {
7386
interpolateColor: (val: any, _r: any, outputRange: any) => outputRange[0],
7487
runOnJS: (fn: any) => fn,
7588
runOnUI: (fn: any) => fn,
89+
configureReanimatedLogger: () => {},
90+
LinearTransition: makeAnimationBuilder(),
91+
FadeIn: makeAnimationBuilder(),
92+
FadeInDown: makeAnimationBuilder(),
93+
FadeOut: makeAnimationBuilder(),
7694
};

components/chat-screen/ChatBar.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import {
1515
Keyboard,
1616
Platform,
1717
} from 'react-native';
18-
import type { SharedValue } from 'react-native-reanimated';
18+
import Animated, {
19+
Easing,
20+
LinearTransition,
21+
withTiming,
22+
type SharedValue,
23+
} from 'react-native-reanimated';
1924
import { type PasteEventPayload, TextInputWrapper } from 'expo-paste-input';
2025
import AttachmentSheet from '../bottomSheets/AttachmentSheet';
2126
import { useAttachment, Attachment } from '../../hooks/useAttachment';
@@ -33,6 +38,11 @@ import AttachmentThumbnail from './AttachmentThumbnail';
3338
import { AudioManager } from 'react-native-audio-api';
3439
import Toast from 'react-native-toast-message';
3540

41+
const BAR_GROW_DURATION = 200;
42+
const BAR_GROW_EASING = Easing.out(Easing.ease);
43+
const BAR_GROW_LAYOUT =
44+
LinearTransition.duration(BAR_GROW_DURATION).easing(BAR_GROW_EASING);
45+
3646
interface Props {
3747
chatId: number | null;
3848
onSend: (
@@ -94,6 +104,7 @@ const ChatBar = ({
94104
} = useAttachment();
95105

96106
const defaultBarHeight = useRef(0);
107+
const prevBarHeight = useRef(0);
97108
const textInputRef = useRef<RNTextInput>(null);
98109
// iOS-only: bump the TextInput key to force a remount when a prompt
99110
// suggestion is set programmatically. iOS doesn't re-fire onLayout
@@ -136,9 +147,16 @@ const ChatBar = ({
136147
}
137148
const baseline = defaultBarHeight.current || height;
138149
const delta = height - baseline;
139-
extraContentPadding.set(Math.max(0, delta));
150+
extraContentPadding.set(
151+
withTiming(Math.max(0, delta), {
152+
duration: BAR_GROW_DURATION,
153+
easing: BAR_GROW_EASING,
154+
})
155+
);
140156
onHeightChange?.(height);
141-
if (delta > 0) {
157+
const grew = height > prevBarHeight.current;
158+
prevBarHeight.current = height;
159+
if (delta > 0 && grew) {
142160
onBarGrow?.();
143161
}
144162
},
@@ -266,7 +284,11 @@ const ChatBar = ({
266284
}
267285

268286
return (
269-
<View style={containerStyle} onLayout={handleBarLayoutForPadding}>
287+
<Animated.View
288+
style={containerStyle}
289+
onLayout={handleBarLayoutForPadding}
290+
layout={BAR_GROW_LAYOUT}
291+
>
270292
{model?.isDownloaded && (
271293
<>
272294
{!hasMessages && (
@@ -330,7 +352,7 @@ const ChatBar = ({
330352
/>
331353
</>
332354
)}
333-
</View>
355+
</Animated.View>
334356
);
335357
};
336358

@@ -382,7 +404,9 @@ const createStyles = (theme: Theme) =>
382404
fontSize: fontSizes.md,
383405
// lineHeight on Android causes typed text to be taller than the
384406
// placeholder, making the ChatBar jump on first keystroke.
385-
...(Platform.OS === 'ios' && { lineHeight: lineHeights.md }),
407+
...(Platform.OS === 'ios'
408+
? { lineHeight: lineHeights.md }
409+
: { includeFontPadding: false }),
386410
fontFamily: fontFamily.regular,
387411
textAlignVertical: 'center',
388412
color: theme.text.onChatBar,

components/chat-screen/ChatScreen.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import React, {
88
import { Keyboard, StyleSheet, useWindowDimensions, View } from 'react-native';
99
import { LinearGradient } from 'expo-linear-gradient';
1010
import type { AppBottomSheetRef } from '../bottomSheets/AppBottomSheet';
11-
import { KeyboardStickyView } from 'react-native-keyboard-controller';
11+
import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller';
1212
import Animated, {
1313
useAnimatedStyle,
1414
useSharedValue,
@@ -109,12 +109,20 @@ export default function ChatScreen({
109109
const { theme } = useTheme();
110110
const styles = useMemo(() => createStyles(theme), [theme]);
111111

112-
const stickyOffset = useMemo(
113-
() => ({ closed: 0, opened: theme.insets.bottom }),
114-
[theme.insets.bottom]
115-
);
116112
const scrollBottomOffset = theme.insets.bottom;
117113

114+
const { height: keyboardHeight, progress: keyboardProgress } =
115+
useReanimatedKeyboardAnimation();
116+
const insetsBottom = theme.insets.bottom;
117+
const chatBarStickyStyle = useAnimatedStyle(() => ({
118+
transform: [
119+
{
120+
translateY:
121+
keyboardHeight.value + keyboardProgress.value * insetsBottom,
122+
},
123+
],
124+
}));
125+
118126
const { settings: chatSettings, setSetting } = useChatSettings(chatId);
119127

120128
const enabledSources =
@@ -349,7 +357,7 @@ export default function ChatScreen({
349357
chatBarSpacerHeight > 0 && { height: chatBarSpacerHeight },
350358
]}
351359
>
352-
<KeyboardStickyView offset={stickyOffset} style={styles.chatBarSticky}>
360+
<Animated.View style={[styles.chatBarSticky, chatBarStickyStyle]}>
353361
<ChatBar
354362
chatId={chatId}
355363
onSend={handleSendMessage}
@@ -371,11 +379,11 @@ export default function ChatScreen({
371379
}}
372380
onBarGrow={() => {
373381
setTimeout(() => {
374-
messagesRef.current?.scrollToEnd();
382+
messagesRef.current?.scrollToEndIfAtBottom();
375383
}, 100);
376384
}}
377385
/>
378-
</KeyboardStickyView>
386+
</Animated.View>
379387
</View>
380388

381389
<ModelSelectSheet

components/chat-screen/Messages.tsx

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
TouchableOpacity,
1818
View,
1919
} from 'react-native';
20-
import { KeyboardChatScrollView } from 'react-native-keyboard-controller';
20+
import {
21+
KeyboardChatScrollView,
22+
useReanimatedKeyboardAnimation,
23+
} from 'react-native-keyboard-controller';
2124
import Reanimated, {
2225
useSharedValue,
2326
useAnimatedStyle,
@@ -33,6 +36,7 @@ import ChevronDown from '../../assets/icons/chevron-down.svg';
3336
export interface MessagesHandle {
3437
onMessageSent: () => void;
3538
scrollToEnd: () => void;
39+
scrollToEndIfAtBottom: () => void;
3640
}
3741

3842
interface Props {
@@ -83,6 +87,20 @@ const Messages = ({
8387
opacity: opacity.get(),
8488
}));
8589

90+
const { height: keyboardHeight, progress: keyboardProgress } =
91+
useReanimatedKeyboardAnimation();
92+
const insetsBottom = theme.insets.bottom;
93+
const scrollButtonAnimatedStyle = useAnimatedStyle(() => ({
94+
transform: [
95+
{
96+
translateY:
97+
-extraContentPadding.value +
98+
keyboardHeight.value +
99+
keyboardProgress.value * insetsBottom,
100+
},
101+
],
102+
}));
103+
86104
// Re-arm the initial scroll when the chat history is cleared (e.g.
87105
// navigating away via useFocusEffect in the chat route sets
88106
// messageHistory to [] while reloading). This ensures that returning
@@ -166,6 +184,11 @@ const Messages = ({
166184
scrollToEnd: () => {
167185
scrollRef.current?.scrollToEnd({ animated: true });
168186
},
187+
scrollToEndIfAtBottom: () => {
188+
if (isAtBottomRef.current) {
189+
scrollRef.current?.scrollToEnd({ animated: true });
190+
}
191+
},
169192
onMessageSent: () => {
170193
// Ensure the view is visible (covers new-chat case where the
171194
// initial-scroll effect hasn't fired because there were no
@@ -378,17 +401,24 @@ const Messages = ({
378401
</KeyboardChatScrollView>
379402

380403
{showScrollButton && (
381-
<TouchableOpacity
382-
style={styles.scrollToBottomButton}
383-
onPress={scrollToBottom}
384-
activeOpacity={0.8}
404+
<Reanimated.View
405+
style={[
406+
styles.scrollToBottomButtonContainer,
407+
scrollButtonAnimatedStyle,
408+
]}
385409
>
386-
<ChevronDown
387-
width={20}
388-
height={20}
389-
style={{ color: theme.text.primary }}
390-
/>
391-
</TouchableOpacity>
410+
<TouchableOpacity
411+
style={styles.scrollToBottomButton}
412+
onPress={scrollToBottom}
413+
activeOpacity={0.8}
414+
>
415+
<ChevronDown
416+
width={20}
417+
height={20}
418+
style={{ color: theme.text.primary }}
419+
/>
420+
</TouchableOpacity>
421+
</Reanimated.View>
392422
)}
393423
</Reanimated.View>
394424
);
@@ -407,10 +437,12 @@ const createStyles = (theme: Theme) =>
407437
paddingTop: 16,
408438
paddingBottom: 8,
409439
},
410-
scrollToBottomButton: {
440+
scrollToBottomButtonContainer: {
411441
position: 'absolute',
412442
bottom: 16,
413443
right: 16,
444+
},
445+
scrollToBottomButton: {
414446
width: 36,
415447
height: 36,
416448
borderRadius: 18,

0 commit comments

Comments
 (0)