|
| 1 | +diff --git a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx |
| 2 | +index 5447a5c..6898649 100644 |
| 3 | +--- a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx |
| 4 | ++++ b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx |
| 5 | +@@ -97,6 +97,7 @@ const RecyclerViewComponent = <T,>( |
| 6 | + const firstChildViewRef = useRef<CompatView>(null); |
| 7 | + const containerViewSizeRef = useRef<RVDimension | undefined>(undefined); |
| 8 | + const pendingChildIds = useRef<Set<string>>(new Set()).current; |
| 9 | ++ const isInitialLayoutHandledRef = useRef(false); |
| 10 | + |
| 11 | + // Track scroll position |
| 12 | + const scrollY = useRef(new Animated.Value(0)).current; |
| 13 | +@@ -130,6 +131,20 @@ const RecyclerViewComponent = <T,>( |
| 14 | + scrollAnchorRef |
| 15 | + ); |
| 16 | + |
| 17 | ++ // Track if we've attempted initial scroll to bottom |
| 18 | ++ const hasAttemptedInitialScrollToBottom = useRef(false); |
| 19 | ++ const lastDataLengthRef = useRef(recyclerViewManager.getDataLength()); |
| 20 | ++ |
| 21 | ++ // Reset scroll attempt when data length changes significantly (new messages loaded) |
| 22 | ++ useMemo(() => { |
| 23 | ++ const currentDataLength = recyclerViewManager.getDataLength(); |
| 24 | ++ if (Math.abs(currentDataLength - lastDataLengthRef.current) > 5) { |
| 25 | ++ hasAttemptedInitialScrollToBottom.current = false; |
| 26 | ++ isInitialLayoutHandledRef.current = false; |
| 27 | ++ } |
| 28 | ++ lastDataLengthRef.current = currentDataLength; |
| 29 | ++ }, [recyclerViewManager.getDataLength()]); |
| 30 | ++ |
| 31 | + // Initialize view holder collection ref |
| 32 | + const viewHolderCollectionRef = useRef<ViewHolderCollectionRef>(null); |
| 33 | + |
| 34 | +@@ -436,10 +451,15 @@ const RecyclerViewComponent = <T,>( |
| 35 | + |
| 36 | + const maintainVisibleContentPositionInternal = useMemo(() => { |
| 37 | + if (shouldMaintainVisibleContentPosition) { |
| 38 | +- return { |
| 39 | ++ const config: typeof maintainVisibleContentPosition = { |
| 40 | + ...maintainVisibleContentPosition, |
| 41 | +- minIndexForVisible: 0, |
| 42 | + }; |
| 43 | ++ // For inverted lists starting from bottom, don't set minIndexForVisible |
| 44 | ++ // as it interferes with maintaining position of items at the bottom |
| 45 | ++ if (!maintainVisibleContentPosition?.startRenderingFromBottom) { |
| 46 | ++ config.minIndexForVisible = 0; |
| 47 | ++ } |
| 48 | ++ return config; |
| 49 | + } |
| 50 | + return undefined; |
| 51 | + }, [maintainVisibleContentPosition, shouldMaintainVisibleContentPosition]); |
| 52 | +@@ -448,6 +468,69 @@ const RecyclerViewComponent = <T,>( |
| 53 | + recyclerViewManager.getDataLength() > 0 && |
| 54 | + (maintainVisibleContentPosition?.startRenderingFromBottom ?? false); |
| 55 | + |
| 56 | ++ // Handle onContentSizeChange to ensure we scroll to bottom when content size changes |
| 57 | ++ // This is more reliable than onLayout for ensuring we're at the bottom |
| 58 | ++ const handleContentSizeChange = useCallback( |
| 59 | ++ (contentWidth: number, contentHeight: number) => { |
| 60 | ++ if ( |
| 61 | ++ shouldRenderFromBottom && |
| 62 | ++ recyclerViewManager.getDataLength() > 0 && |
| 63 | ++ contentHeight > 0 |
| 64 | ++ ) { |
| 65 | ++ // Try scrolling to bottom, but allow multiple attempts if needed |
| 66 | ++ const attemptScroll = () => { |
| 67 | ++ if (scrollViewRef.current && handlerMethods) { |
| 68 | ++ // Try using scrollToEnd via handlerMethods |
| 69 | ++ handlerMethods.scrollToEnd({ animated: false }); |
| 70 | ++ |
| 71 | ++ // Also try direct scrollToEnd as fallback |
| 72 | ++ if (scrollViewRef.current.scrollToEnd) { |
| 73 | ++ scrollViewRef.current.scrollToEnd({ animated: false }); |
| 74 | ++ } |
| 75 | ++ } |
| 76 | ++ }; |
| 77 | ++ |
| 78 | ++ // Use multiple requestAnimationFrame calls to ensure layout is complete |
| 79 | ++ requestAnimationFrame(() => { |
| 80 | ++ requestAnimationFrame(() => { |
| 81 | ++ attemptScroll(); |
| 82 | ++ // Try one more time after a short delay to handle async layout |
| 83 | ++ setTimeout(() => { |
| 84 | ++ attemptScroll(); |
| 85 | ++ }, 50); |
| 86 | ++ }); |
| 87 | ++ }); |
| 88 | ++ |
| 89 | ++ hasAttemptedInitialScrollToBottom.current = true; |
| 90 | ++ } |
| 91 | ++ }, |
| 92 | ++ [shouldRenderFromBottom, recyclerViewManager, handlerMethods, scrollViewRef] |
| 93 | ++ ); |
| 94 | ++ |
| 95 | ++ // Also handle onLayout as a fallback |
| 96 | ++ const handleScrollViewLayout = useCallback(() => { |
| 97 | ++ if ( |
| 98 | ++ shouldRenderFromBottom && |
| 99 | ++ recyclerViewManager.getIsFirstLayoutComplete() && |
| 100 | ++ recyclerViewManager.getDataLength() > 0 |
| 101 | ++ ) { |
| 102 | ++ const attemptScroll = () => { |
| 103 | ++ if (handlerMethods) { |
| 104 | ++ handlerMethods.scrollToEnd({ animated: false }); |
| 105 | ++ } |
| 106 | ++ }; |
| 107 | ++ |
| 108 | ++ // Use requestAnimationFrame to ensure layout is complete |
| 109 | ++ requestAnimationFrame(() => { |
| 110 | ++ requestAnimationFrame(() => { |
| 111 | ++ attemptScroll(); |
| 112 | ++ }); |
| 113 | ++ }); |
| 114 | ++ |
| 115 | ++ isInitialLayoutHandledRef.current = true; |
| 116 | ++ } |
| 117 | ++ }, [shouldRenderFromBottom, recyclerViewManager, handlerMethods]); |
| 118 | ++ |
| 119 | + // Create view for measuring bounded size |
| 120 | + const viewToMeasureBoundedSize = useMemo(() => { |
| 121 | + return ( |
| 122 | +@@ -512,6 +595,8 @@ const RecyclerViewComponent = <T,>( |
| 123 | + horizontal={horizontal} |
| 124 | + ref={scrollViewRef} |
| 125 | + onScroll={animatedEvent} |
| 126 | ++ onLayout={handleScrollViewLayout} |
| 127 | ++ onContentSizeChange={handleContentSizeChange} |
| 128 | + maintainVisibleContentPosition={ |
| 129 | + maintainVisibleContentPositionInternal |
| 130 | + } |
| 131 | +diff --git a/node_modules/@shopify/flash-list/src/recyclerview/hooks/useRecyclerViewController.tsx b/node_modules/@shopify/flash-list/src/recyclerview/hooks/useRecyclerViewController.tsx |
| 132 | +index 4d99406..5516f1d 100644 |
| 133 | +--- a/node_modules/@shopify/flash-list/src/recyclerview/hooks/useRecyclerViewController.tsx |
| 134 | ++++ b/node_modules/@shopify/flash-list/src/recyclerview/hooks/useRecyclerViewController.tsx |
| 135 | +@@ -52,6 +52,7 @@ export function useRecyclerViewController<T>( |
| 136 | + const [_, setRenderId] = useState(0); |
| 137 | + const pauseOffsetCorrection = useRef(false); |
| 138 | + const initialScrollCompletedRef = useRef(false); |
| 139 | ++ const initialScrollToBottomAttemptedRef = useRef(false); |
| 140 | + const lastDataLengthRef = useRef(recyclerViewManager.getDataLength()); |
| 141 | + const { setTimeout } = useUnmountAwareTimeout(); |
| 142 | + |
| 143 | +@@ -569,6 +570,9 @@ export function useRecyclerViewController<T>( |
| 144 | + const initialScrollIndex = |
| 145 | + recyclerViewManager.getInitialScrollIndex() ?? -1; |
| 146 | + const dataLength = data?.length ?? 0; |
| 147 | ++ const shouldRenderFromBottom = |
| 148 | ++ recyclerViewManager.props.maintainVisibleContentPosition?.startRenderingFromBottom ?? false; |
| 149 | ++ |
| 150 | + if ( |
| 151 | + initialScrollIndex >= 0 && |
| 152 | + initialScrollIndex < dataLength && |
| 153 | +@@ -598,6 +602,19 @@ export function useRecyclerViewController<T>( |
| 154 | + animated: false, |
| 155 | + skipFirstItemOffset: false, |
| 156 | + }); |
| 157 | ++ |
| 158 | ++ // For startRenderingFromBottom, ensure we scroll to actual bottom after layout |
| 159 | ++ // This handles variable height items where initial scroll index position might not be accurate |
| 160 | ++ if (shouldRenderFromBottom && dataLength > 0 && !initialScrollToBottomAttemptedRef.current) { |
| 161 | ++ initialScrollToBottomAttemptedRef.current = true; |
| 162 | ++ // Use multiple requestAnimationFrame calls to ensure layout measurements are complete |
| 163 | ++ requestAnimationFrame(() => { |
| 164 | ++ requestAnimationFrame(() => { |
| 165 | ++ // Scroll to end to ensure we're at the actual bottom |
| 166 | ++ handlerMethods.scrollToEnd({ animated: false }); |
| 167 | ++ }); |
| 168 | ++ }); |
| 169 | ++ } |
| 170 | + }, 0); |
| 171 | + } |
| 172 | + }, [handlerMethods, recyclerViewManager, setTimeout]); |
0 commit comments