From 4c374eb616525589ac640df494f64629b068ae6a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 17 Feb 2026 11:48:52 +0200 Subject: [PATCH] Add animated scroll indicator insets to PostFeed Track scroll position in PostFeed and use it to dynamically adjust iOS scroll indicator insets so they follow the collapsing header. Gate the scrollY-based adjustment behind an adjustScrollIndicators prop so it only activates on profile pages. Thread collapsedHeaderHeight from PagerWithHeader through ProfileFeedSection to PostFeed to replace the previous hardcoded minimum. Co-Authored-By: Claude Opus 4.6 --- src/screens/Profile/Sections/Feed.tsx | 4 + src/view/com/posts/PostFeed.tsx | 112 +++++++++++++++++++------- src/view/screens/Profile.tsx | 15 ++-- 3 files changed, 97 insertions(+), 34 deletions(-) diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index dc9b46356b..6130351273 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -28,6 +28,7 @@ interface FeedSectionProps { ref?: React.Ref feed: FeedDescriptor headerHeight: number + collapsedHeaderHeight: number isFocused: boolean scrollElRef: ListRef ignoreFilterFor?: string @@ -41,6 +42,7 @@ export function ProfileFeedSection({ ref, feed, headerHeight, + collapsedHeaderHeight, isFocused, scrollElRef, ignoreFilterFor, @@ -110,6 +112,8 @@ export function ProfileFeedSection({ shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined } isVideoFeed={isVideoFeed} + adjustScrollIndicators + collapsedHeaderHeight={collapsedHeaderHeight} /> {(isScrolledDown || hasNew) && ( number + adjustScrollIndicators?: boolean + collapsedHeaderHeight?: number }): React.ReactNode => { const ax = useAnalytics() const {t: l} = useLingui() @@ -999,37 +1010,80 @@ let PostFeed = ({ [feedFeedback, feed, liveNowConfig, getPostPosition, ax], ) + const { + onBeginDrag: onBeginDragFromContext, + onEndDrag: onEndDragFromContext, + onScroll: onScrollFromContext, + onMomentumEnd: onMomentumEndFromContext, + } = useScrollHandlers() + + const scrollY = useSharedValue(0) + const onScrollWorklet = useCallback>( + (e, ctx) => { + 'worklet' + onScrollFromContext?.(e, ctx) + scrollY.set(e.contentOffset.y) + }, + [onScrollFromContext, scrollY], + ) + + const {footerHeight} = useShellLayout() + + const animatedProps = useAnimatedProps(() => { + if (IS_IOS) { + return { + scrollIndicatorInsets: { + top: adjustScrollIndicators + ? Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight) + : headerOffset, + right: 1, + left: 0, + bottom: footerHeight.get(), + }, + } + } + return {} + }) + return ( - item.key} - renderItem={renderItem} - ListFooterComponent={FeedFooter} - ListHeaderComponent={ListHeaderComponent} - refreshing={isPTRing} - onRefresh={() => void onRefresh()} - headerOffset={headerOffset} - progressViewOffset={progressViewOffset} - contentContainerStyle={{ - minHeight: Dimensions.get('window').height * 1.5, - }} - onScrolledDownChange={handleScrolledDownChange} - onEndReached={() => void onEndReached()} - onEndReachedThreshold={2} // number of posts left to trigger load more - removeClippedSubviews={true} - extraData={extraData} - desktopFixedHeight={ - desktopFixedHeightOffset ? desktopFixedHeightOffset : true - } - initialNumToRender={initialNumToRenderOverride ?? initialNumToRender} - windowSize={9} - maxToRenderPerBatch={IS_IOS ? 5 : 1} - updateCellsBatchingPeriod={40} - onItemSeen={onItemSeen} - /> + + item.key} + renderItem={renderItem} + ListFooterComponent={FeedFooter} + ListHeaderComponent={ListHeaderComponent} + refreshing={isPTRing} + onRefresh={() => void onRefresh()} + headerOffset={headerOffset} + progressViewOffset={progressViewOffset} + contentContainerStyle={{ + minHeight: Dimensions.get('window').height * 1.5, + }} + onScrolledDownChange={handleScrolledDownChange} + onEndReached={() => void onEndReached()} + onEndReachedThreshold={2} // number of posts left to trigger load more + removeClippedSubviews={true} + extraData={extraData} + desktopFixedHeight={ + desktopFixedHeightOffset ? desktopFixedHeightOffset : true + } + initialNumToRender={initialNumToRenderOverride ?? initialNumToRender} + windowSize={9} + maxToRenderPerBatch={IS_IOS ? 5 : 1} + updateCellsBatchingPeriod={40} + onItemSeen={onItemSeen} + animatedProps={animatedProps} + /> + ) } diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index d29c9a0420..0e79f2f3fe 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -420,11 +420,12 @@ function ProfileScreenLoaded({ ) : null} {showPostsTab - ? ({headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => ( ( + ? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (