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 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ interface FeedSectionProps {
|
||||
ref?: React.Ref<SectionRef>
|
||||
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) && (
|
||||
<LoadLatestBtn
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
type ScrollHandler,
|
||||
useAnimatedProps,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
AppBskyEmbedVideo,
|
||||
@@ -21,6 +26,7 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
|
||||
@@ -42,6 +48,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {useSelectedFeed} from '#/state/shell/selected-feed'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {List, type ListRef} from '#/view/com/util/List'
|
||||
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
@@ -204,6 +211,8 @@ let PostFeed = ({
|
||||
savedFeedConfig,
|
||||
initialNumToRender: initialNumToRenderOverride,
|
||||
isVideoFeed = false,
|
||||
adjustScrollIndicators,
|
||||
collapsedHeaderHeight = 0,
|
||||
}: {
|
||||
feed: FeedDescriptor
|
||||
feedParams?: FeedParams
|
||||
@@ -227,6 +236,8 @@ let PostFeed = ({
|
||||
initialNumToRender?: number
|
||||
isVideoFeed?: boolean
|
||||
lastFetchDate?: () => 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<ScrollHandler<any>>(
|
||||
(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 (
|
||||
<View testID={testID} style={style}>
|
||||
<List
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
data={feedItems}
|
||||
keyExtractor={(item: FeedRow) => 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}
|
||||
/>
|
||||
<ScrollProvider
|
||||
onScroll={onScrollWorklet}
|
||||
onBeginDrag={onBeginDragFromContext}
|
||||
onEndDrag={onEndDragFromContext}
|
||||
onMomentumBegin={onMomentumEndFromContext}
|
||||
onMomentumEnd={onMomentumEndFromContext}>
|
||||
<List
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
data={feedItems}
|
||||
keyExtractor={(item: FeedRow) => 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}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -420,11 +420,12 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
: null}
|
||||
{showPostsTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={postsSectionRef}
|
||||
feed={`author|${profile.did}|posts_and_author_threads`}
|
||||
headerHeight={headerHeight}
|
||||
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
@@ -446,11 +447,12 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
: null}
|
||||
{showRepliesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={repliesSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_replies`}
|
||||
headerHeight={headerHeight}
|
||||
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
@@ -461,11 +463,12 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
: null}
|
||||
{showMediaTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={mediaSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_media`}
|
||||
headerHeight={headerHeight}
|
||||
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
@@ -488,11 +491,12 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
: null}
|
||||
{showVideosTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={videosSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_video`}
|
||||
headerHeight={headerHeight}
|
||||
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
@@ -515,11 +519,12 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
: null}
|
||||
{showLikesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
? ({headerHeight, collapsedHeaderHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={likesSectionRef}
|
||||
feed={`likes|${profile.did}`}
|
||||
headerHeight={headerHeight}
|
||||
collapsedHeaderHeight={collapsedHeaderHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
|
||||
Reference in New Issue
Block a user