4c374eb616
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>
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
import {useCallback, useEffect, useImperativeHandle, useState} from 'react'
|
|
import {findNodeHandle, View} from 'react-native'
|
|
import {msg} from '@lingui/core/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
import {Trans} from '@lingui/react/macro'
|
|
import {useQueryClient} from '@tanstack/react-query'
|
|
|
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
|
import {
|
|
type FeedDescriptor,
|
|
RQKEY as FEED_RQKEY,
|
|
} from '#/state/queries/post-feed'
|
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
|
import {PostFeed} from '#/view/com/posts/PostFeed'
|
|
import {
|
|
EmptyState,
|
|
type EmptyStateButtonProps,
|
|
} from '#/view/com/util/EmptyState'
|
|
import {type ListRef} from '#/view/com/util/List'
|
|
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
|
import {atoms as a, ios, useTheme} from '#/alf'
|
|
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
|
|
import {Text} from '#/components/Typography'
|
|
import {IS_IOS, IS_NATIVE} from '#/env'
|
|
import {type SectionRef} from './types'
|
|
|
|
interface FeedSectionProps {
|
|
ref?: React.Ref<SectionRef>
|
|
feed: FeedDescriptor
|
|
headerHeight: number
|
|
collapsedHeaderHeight: number
|
|
isFocused: boolean
|
|
scrollElRef: ListRef
|
|
ignoreFilterFor?: string
|
|
setScrollViewTag: (tag: number | null) => void
|
|
emptyStateMessage?: string
|
|
emptyStateButton?: EmptyStateButtonProps
|
|
emptyStateIcon?: React.ComponentType<any> | React.ReactElement
|
|
}
|
|
|
|
export function ProfileFeedSection({
|
|
ref,
|
|
feed,
|
|
headerHeight,
|
|
collapsedHeaderHeight,
|
|
isFocused,
|
|
scrollElRef,
|
|
ignoreFilterFor,
|
|
setScrollViewTag,
|
|
emptyStateMessage,
|
|
emptyStateButton,
|
|
emptyStateIcon,
|
|
}: FeedSectionProps) {
|
|
const {_} = useLingui()
|
|
const queryClient = useQueryClient()
|
|
const [hasNew, setHasNew] = useState(false)
|
|
const [isScrolledDown, setIsScrolledDown] = useState(false)
|
|
const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
|
|
const isVideoFeed = IS_NATIVE && feed.endsWith('posts_with_video')
|
|
const adjustedInitialNumToRender = useInitialNumToRender({
|
|
screenHeightOffset: headerHeight,
|
|
})
|
|
const onScrollToTop = useCallback(() => {
|
|
scrollElRef.current?.scrollToOffset({
|
|
animated: IS_NATIVE,
|
|
offset: -headerHeight,
|
|
})
|
|
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
|
setHasNew(false)
|
|
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
scrollToTop: onScrollToTop,
|
|
}))
|
|
|
|
const renderPostsEmpty = useCallback(() => {
|
|
return (
|
|
<View style={[a.flex_1, a.justify_center, a.align_center]}>
|
|
<EmptyState
|
|
style={{width: '100%'}}
|
|
icon={emptyStateIcon || EditIcon}
|
|
iconSize="3xl"
|
|
message={emptyStateMessage || _(msg`No posts yet`)}
|
|
button={emptyStateButton}
|
|
/>
|
|
</View>
|
|
)
|
|
}, [_, emptyStateButton, emptyStateIcon, emptyStateMessage])
|
|
|
|
useEffect(() => {
|
|
if (IS_IOS && isFocused && scrollElRef.current) {
|
|
const nativeTag = findNodeHandle(scrollElRef.current)
|
|
setScrollViewTag(nativeTag)
|
|
}
|
|
}, [isFocused, scrollElRef, setScrollViewTag])
|
|
|
|
return (
|
|
<View>
|
|
<PostFeed
|
|
testID="postsFeed"
|
|
enabled={isFocused}
|
|
feed={feed}
|
|
scrollElRef={scrollElRef}
|
|
onHasNew={setHasNew}
|
|
onScrolledDownChange={setIsScrolledDown}
|
|
renderEmptyState={renderPostsEmpty}
|
|
headerOffset={headerHeight}
|
|
progressViewOffset={ios(0)}
|
|
renderEndOfFeed={isVideoFeed ? undefined : ProfileEndOfFeed}
|
|
ignoreFilterFor={ignoreFilterFor}
|
|
initialNumToRender={
|
|
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
|
}
|
|
isVideoFeed={isVideoFeed}
|
|
adjustScrollIndicators
|
|
collapsedHeaderHeight={collapsedHeaderHeight}
|
|
/>
|
|
{(isScrolledDown || hasNew) && (
|
|
<LoadLatestBtn
|
|
onPress={onScrollToTop}
|
|
label={_(msg`Load new posts`)}
|
|
showIndicator={hasNew}
|
|
/>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function ProfileEndOfFeed() {
|
|
const t = useTheme()
|
|
|
|
return (
|
|
<View
|
|
style={[a.w_full, a.py_5xl, a.border_t, t.atoms.border_contrast_medium]}>
|
|
<Text style={[t.atoms.text_contrast_medium, a.text_center]}>
|
|
<Trans>End of feed</Trans>
|
|
</Text>
|
|
</View>
|
|
)
|
|
}
|