A React Native app for the ultimate thinking partner.
at sdk-v1-upgrade 108 lines 3.3 kB view raw
1import { useRef, useEffect, useCallback, useState } from 'react'; 2import { FlatList, NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; 3 4interface UseScrollToBottomOptions { 5 /** 6 * Whether to scroll to bottom on initial mount/load 7 * @default true 8 */ 9 scrollOnMount?: boolean; 10 11 /** 12 * Delay before scrolling (ms) - useful for rendering completion 13 * @default 100 14 */ 15 delay?: number; 16 17 /** 18 * Distance from bottom (px) to consider "at bottom" 19 * @default 100 20 */ 21 threshold?: number; 22} 23 24/** 25 * Hook for managing scroll-to-bottom behavior in chat interfaces 26 * 27 * ULTRATHINK SIMPLE SOLUTION: 28 * - Tracks if user is at bottom 29 * - Only auto-scrolls if user is at bottom 30 * - Prevents scroll jumps on content replace 31 * - Allows manual scroll up without interference 32 * 33 * @example 34 * const { scrollViewRef, scrollToBottom, onContentSizeChange, onScroll } = useScrollToBottom(); 35 * 36 * <FlatList 37 * ref={scrollViewRef} 38 * onContentSizeChange={onContentSizeChange} 39 * onScroll={onScroll} 40 * /> 41 */ 42export function useScrollToBottom(options: UseScrollToBottomOptions = {}) { 43 const { 44 scrollOnMount = true, 45 delay = 100, 46 threshold = 100, 47 } = options; 48 49 const scrollViewRef = useRef<FlatList<any>>(null); 50 const hasMountedRef = useRef(false); 51 const isNearBottomRef = useRef(true); // Assume at bottom initially 52 const contentSizeRef = useRef({ width: 0, height: 0 }); 53 const layoutHeightRef = useRef(0); 54 55 // Track scroll position to determine if user is at bottom 56 const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => { 57 const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; 58 59 // Calculate distance from bottom 60 const distanceFromBottom = contentSize.height - (contentOffset.y + layoutMeasurement.height); 61 62 // Update near-bottom state 63 isNearBottomRef.current = distanceFromBottom <= threshold; 64 65 // Store layout height for later use 66 layoutHeightRef.current = layoutMeasurement.height; 67 }, [threshold]); 68 69 // Scroll to bottom - but only if user is already near bottom 70 const scrollToBottom = useCallback((force: boolean = false) => { 71 if (force || isNearBottomRef.current) { 72 setTimeout(() => { 73 scrollViewRef.current?.scrollToEnd({ animated: false }); 74 }, delay); 75 } 76 }, [delay]); 77 78 // Handle content size change - scroll if user is at bottom 79 const onContentSizeChange = useCallback((width: number, height: number) => { 80 const prevHeight = contentSizeRef.current.height; 81 contentSizeRef.current = { width, height }; 82 83 // Initial mount - always scroll to bottom 84 if (!hasMountedRef.current && height > 0) { 85 hasMountedRef.current = true; 86 if (scrollOnMount) { 87 setTimeout(() => { 88 scrollViewRef.current?.scrollToEnd({ animated: false }); 89 }, delay); 90 } 91 return; 92 } 93 94 // Content grew (new messages) - only scroll if user was at bottom 95 if (height > prevHeight && isNearBottomRef.current) { 96 setTimeout(() => { 97 scrollViewRef.current?.scrollToEnd({ animated: false }); 98 }, delay); 99 } 100 }, [scrollOnMount, delay]); 101 102 return { 103 scrollViewRef, 104 scrollToBottom, 105 onContentSizeChange, 106 onScroll, 107 }; 108}