abstract into a nice hook

This commit is contained in:
Samuel Newman
2026-02-18 17:16:45 +02:00
parent 58629959e8
commit 866118c49f
7 changed files with 299 additions and 390 deletions
@@ -7,11 +7,6 @@ import {
View,
type ViewStyle,
} from 'react-native'
import {
type ScrollHandler,
useAnimatedProps,
useSharedValue,
} from 'react-native-reanimated'
import {type AppBskyGraphDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -21,20 +16,19 @@ import {useNavigation} from '@react-navigation/native'
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type NavigationProp} from '#/lib/routes/types'
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
import {ScrollProvider} from '#/lib/ScrollContext'
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
import {logger} from '#/logger'
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
import {useShellLayout} from '#/state/shell/shell-layout'
import {
EmptyState,
type EmptyStateButtonProps,
} from '#/view/com/util/EmptyState'
import {List, type ListRef} from '#/view/com/util/List'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {atoms as a, ios, useTheme} from '#/alf'
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {useProfileScrollbarAdjustment} from '#/screens/Profile/useProfileScrollbarAdjustment'
import {atoms as a, ios, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
@@ -88,39 +82,8 @@ export function ProfileStarterPacks({
const bottomBarOffset = useBottomBarOffset(100)
const {height} = useWindowDimensions()
const [isPTRing, setIsPTRing] = useState(false)
const {gtMobile} = useBreakpoints()
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: Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight),
right: 1,
left: 0,
bottom: footerHeight.get(),
},
}
}
return {}
})
const {
data,
refetch,
@@ -129,7 +92,6 @@ export function ProfileStarterPacks({
isFetchingNextPage,
fetchNextPage,
} = useActorStarterPacksQuery({did, enabled})
const {isTabletOrDesktop} = useWebMediaQueries()
const items = data?.pages.flatMap(page => page.starterPacks)
const {_} = useLingui()
@@ -191,58 +153,54 @@ export function ProfileStarterPacks({
<View
style={[
a.p_lg,
(isTabletOrDesktop || index !== 0) && a.border_t,
(gtMobile || index !== 0) && a.border_t,
t.atoms.border_contrast_low,
]}>
<StarterPackCard starterPack={item} />
</View>
)
},
[isTabletOrDesktop, t.atoms.border_contrast_low],
[gtMobile, t.atoms.border_contrast_low],
)
const list = (
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
headerOffset={headerOffset}
progressViewOffset={ios(0)}
contentContainerStyle={{
minHeight: height + headerOffset,
paddingBottom: bottomBarOffset,
}}
removeClippedSubviews={true}
desktopFixedHeight
onEndReached={onEndReached}
onRefresh={onRefresh}
ListEmptyComponent={
data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder
}
ListFooterComponent={
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
}
animatedProps={IS_IOS ? animatedProps : undefined}
/>
)
const {scrollHandlers, animatedProps} = useProfileScrollbarAdjustment({
headerOffset,
collapsedHeaderHeight,
})
return (
<View testID={testID} style={style}>
{IS_IOS ? (
<ScrollProvider
onScroll={onScrollWorklet}
onBeginDrag={onBeginDragFromContext}
onEndDrag={onEndDragFromContext}
onMomentumBegin={onMomentumEndFromContext}
onMomentumEnd={onMomentumEndFromContext}>
{list}
</ScrollProvider>
) : (
list
)}
<ScrollProvider {...scrollHandlers}>
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
headerOffset={headerOffset}
progressViewOffset={ios(0)}
contentContainerStyle={{
minHeight: height + headerOffset,
paddingBottom: bottomBarOffset,
}}
removeClippedSubviews={true}
desktopFixedHeight
onEndReached={onEndReached}
onRefresh={onRefresh}
ListEmptyComponent={
data
? isMe
? EmptyComponent
: undefined
: FeedFeedLoadingPlaceholder
}
ListFooterComponent={
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
}
animatedProps={animatedProps}
/>
</ScrollProvider>
</View>
)
}
+33 -76
View File
@@ -1,10 +1,5 @@
import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react'
import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native'
import {
type ScrollHandler,
useAnimatedProps,
useSharedValue,
} from 'react-native-reanimated'
import {
type AppBskyLabelerDefs,
type InterpretedLabelValueDefinition,
@@ -16,9 +11,9 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
import {useShellLayout} from '#/state/shell/shell-layout'
import {ScrollProvider} from '#/lib/ScrollContext'
import {List, type ListRef} from '#/view/com/util/List'
import {useProfileScrollbarAdjustment} from '#/screens/Profile/useProfileScrollbarAdjustment'
import {atoms as a, ios, tokens, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
@@ -57,39 +52,6 @@ export function ProfileLabelsSection({
}: LabelsSectionProps) {
const t = useTheme()
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: Math.max(headerHeight - scrollY.get(), collapsedHeaderHeight),
right: 1,
left: 0,
bottom: footerHeight.get(),
},
}
}
return {}
})
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
animated: IS_NATIVE,
@@ -159,45 +121,40 @@ export function ProfileLabelsSection({
[labelerInfo, isSubscribed, numItems, t],
)
const list = (
<List
ref={scrollElRef}
data={labelValues}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={a.px_xl}
headerOffset={headerHeight}
progressViewOffset={ios(0)}
ListHeaderComponent={
<LabelerListHeader
isLabelerLoading={isLabelerLoading}
labelerInfo={labelerInfo}
labelerError={labelerError}
hasValues={labelValues.length !== 0}
isSubscribed={isSubscribed}
/>
}
ListFooterComponent={
<ListFooter height={headerHeight + 180} style={a.border_transparent} />
}
animatedProps={IS_IOS ? animatedProps : undefined}
/>
)
const {scrollHandlers, animatedProps} = useProfileScrollbarAdjustment({
headerOffset: headerHeight,
collapsedHeaderHeight,
})
return (
<View>
{IS_IOS ? (
<ScrollProvider
onScroll={onScrollWorklet}
onBeginDrag={onBeginDragFromContext}
onEndDrag={onEndDragFromContext}
onMomentumBegin={onMomentumEndFromContext}
onMomentumEnd={onMomentumEndFromContext}>
{list}
</ScrollProvider>
) : (
list
)}
<ScrollProvider {...scrollHandlers}>
<List
ref={scrollElRef}
data={labelValues}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={a.px_xl}
headerOffset={headerHeight}
progressViewOffset={ios(0)}
ListHeaderComponent={
<LabelerListHeader
isLabelerLoading={isLabelerLoading}
labelerInfo={labelerInfo}
labelerError={labelerError}
hasValues={labelValues.length !== 0}
isSubscribed={isSubscribed}
/>
}
ListFooterComponent={
<ListFooter
height={headerHeight + 180}
style={a.border_transparent}
/>
}
animatedProps={animatedProps}
/>
</ScrollProvider>
</View>
)
}
@@ -0,0 +1,81 @@
import {useCallback, useMemo} from 'react'
import {
type ScrollHandler,
useAnimatedProps,
useSharedValue,
} from 'react-native-reanimated'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {useShellLayout} from '#/state/shell/shell-layout'
/**
* Adjusts the scroll indicator insets on iOS to account for the pager header.
* Adds another scroll offset listener, so use sparingly.
*
* HOW TO USE:
*
* ```tsx
* const { scrollHandlers, animatedProps } = useProfileScrollbarAdjustment({
* headerHeight: 600, // full size of the header
* collapsedHeaderHeight: 200, // height of the header when collapsed
* })
*
* return (
* <ScrollContext {...scrollHandlers}>
* <List animatedProps={animatedProps} />
* </ScrollContext>
* )
* ````
*/
export function useProfileScrollbarAdjustment({
enabled = true,
headerOffset,
collapsedHeaderHeight,
}: {
enabled?: boolean
headerOffset: number
collapsedHeaderHeight: number
}) {
const {onScroll: onScrollFromContext, ...otherScrollHandlers} =
useScrollHandlers()
const scrollY = useSharedValue(0)
const onScroll = useCallback<ScrollHandler<any>>(
(e, ctx) => {
'worklet'
onScrollFromContext?.(e, ctx)
if (enabled) {
scrollY.set(e.contentOffset.y)
}
},
[onScrollFromContext, scrollY, enabled],
)
const {footerHeight} = useShellLayout()
const animatedProps = useAnimatedProps(() => {
return {
scrollIndicatorInsets: {
top: enabled
? Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight)
: headerOffset,
right: 1,
left: 0,
bottom: footerHeight.get(),
},
}
})
const scrollHandlers = useMemo(
() => ({
onScroll,
...otherScrollHandlers,
}),
[onScroll, otherScrollHandlers],
)
return {
scrollHandlers,
animatedProps,
}
}
@@ -0,0 +1,50 @@
import {useScrollHandlers} from '#/lib/ScrollContext'
/**
* Adjusts the scroll indicator insets on iOS to account for the pager header. Adds another scroll offset
*
* HOW TO USE:
*
* ```tsx
* const { scrollHandlers, animatedProps } = useScrollbarAdjustment({
* headerHeight: 600, // full size of the header
* collapsedHeaderHeight: 200, // height of the header when collapsed
* })
*
* return (
* <ScrollContext {...scrollHandlers}>
* <List animatedProps={animatedProps} />
* </ScrollContext>
* )
* ```
*
* @platform ios
*/
export function useProfileScrollbarAdjustment({}: {
enabled?: boolean
headerOffset: number
collapsedHeaderHeight: number
}): {
scrollHandlers: ReturnType<typeof useScrollHandlers>
animatedProps:
| undefined
| Partial<{
scrollIndicatorInsets: {
top: number
right: number
left: number
bottom: number
}
}>
} {
// this is a no-op version for android/web
const scrollHandlers = useScrollHandlers()
const animatedProps = undefined
return {
scrollHandlers,
animatedProps,
}
}
+27 -72
View File
@@ -13,28 +13,23 @@ import {
View,
type ViewStyle,
} from 'react-native'
import {
type ScrollHandler,
useAnimatedProps,
useSharedValue,
} from 'react-native-reanimated'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
import {ScrollProvider} from '#/lib/ScrollContext'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {List, type ListRef} from '#/view/com/util/List'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {useProfileScrollbarAdjustment} from '#/screens/Profile/useProfileScrollbarAdjustment'
import {atoms as a, ios, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
@@ -77,38 +72,6 @@ export function ProfileFeedgens({
const t = useTheme()
const [isPTRing, setIsPTRing] = useState(false)
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: Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight),
right: 1,
left: 0,
bottom: footerHeight.get(),
},
}
}
return {}
})
const {height} = useWindowDimensions()
const opts = useMemo(() => ({enabled}), [enabled])
const {
@@ -230,7 +193,7 @@ export function ProfileFeedgens({
/>
)
} else if (item === LOADING) {
return <FeedLoadingPlaceholder />
return <FeedFeedLoadingPlaceholder />
}
if (preferences) {
return (
@@ -286,40 +249,32 @@ export function ProfileFeedgens({
isEmpty,
])
const list = (
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
keyExtractor={keyExtractor}
renderItem={renderItem}
ListFooterComponent={ProfileFeedgensFooter}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
progressViewOffset={ios(0)}
removeClippedSubviews={true}
desktopFixedHeight
onEndReached={onEndReached}
contentContainerStyle={{minHeight: height + headerOffset}}
animatedProps={IS_IOS ? animatedProps : undefined}
/>
)
const {scrollHandlers, animatedProps} = useProfileScrollbarAdjustment({
headerOffset,
collapsedHeaderHeight,
})
return (
<View testID={testID} style={style}>
{IS_IOS ? (
<ScrollProvider
onScroll={onScrollWorklet}
onBeginDrag={onBeginDragFromContext}
onEndDrag={onEndDragFromContext}
onMomentumBegin={onMomentumEndFromContext}
onMomentumEnd={onMomentumEndFromContext}>
{list}
</ScrollProvider>
) : (
list
)}
<ScrollProvider {...scrollHandlers}>
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
keyExtractor={keyExtractor}
renderItem={renderItem}
ListFooterComponent={ProfileFeedgensFooter}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
progressViewOffset={ios(0)}
removeClippedSubviews={true}
desktopFixedHeight
onEndReached={onEndReached}
contentContainerStyle={{minHeight: height + headerOffset}}
animatedProps={animatedProps}
/>
</ScrollProvider>
</View>
)
}
+27 -72
View File
@@ -13,28 +13,23 @@ import {
View,
type ViewStyle,
} from 'react-native'
import {
type ScrollHandler,
useAnimatedProps,
useSharedValue,
} from 'react-native-reanimated'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {ScrollProvider, useScrollHandlers} from '#/lib/ScrollContext'
import {ScrollProvider} from '#/lib/ScrollContext'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {List, type ListRef} from '#/view/com/util/List'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {useProfileScrollbarAdjustment} from '#/screens/Profile/useProfileScrollbarAdjustment'
import {atoms as a, ios, useTheme} from '#/alf'
import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
import * as ListCard from '#/components/ListCard'
@@ -77,38 +72,6 @@ export function ProfileLists({
const t = useTheme()
const [isPTRing, setIsPTRing] = useState(false)
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: Math.max(headerOffset - scrollY.get(), collapsedHeaderHeight),
right: 1,
left: 0,
bottom: footerHeight.get(),
},
}
}
return {}
})
const {height} = useWindowDimensions()
const opts = useMemo(() => ({enabled}), [enabled])
const {
@@ -229,7 +192,7 @@ export function ProfileLists({
/>
)
} else if (item === LOADING) {
return <FeedLoadingPlaceholder />
return <FeedFeedLoadingPlaceholder />
}
if (preferences) {
return (
@@ -285,40 +248,32 @@ export function ProfileLists({
isEmpty,
])
const list = (
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
keyExtractor={keyExtractor}
renderItem={renderItem}
ListFooterComponent={ProfileListsFooter}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
progressViewOffset={ios(0)}
removeClippedSubviews={true}
desktopFixedHeight
onEndReached={onEndReached}
contentContainerStyle={{minHeight: height + headerOffset}}
animatedProps={IS_IOS ? animatedProps : undefined}
/>
)
const {scrollHandlers, animatedProps} = useProfileScrollbarAdjustment({
headerOffset,
collapsedHeaderHeight,
})
return (
<View testID={testID} style={style}>
{IS_IOS ? (
<ScrollProvider
onScroll={onScrollWorklet}
onBeginDrag={onBeginDragFromContext}
onEndDrag={onEndDragFromContext}
onMomentumBegin={onMomentumEndFromContext}
onMomentumEnd={onMomentumEndFromContext}>
{list}
</ScrollProvider>
) : (
list
)}
<ScrollProvider {...scrollHandlers}>
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
keyExtractor={keyExtractor}
renderItem={renderItem}
ListFooterComponent={ProfileListsFooter}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
progressViewOffset={ios(0)}
removeClippedSubviews={true}
desktopFixedHeight
onEndReached={onEndReached}
contentContainerStyle={{minHeight: height + headerOffset}}
animatedProps={animatedProps}
/>
</ScrollProvider>
</View>
)
}
+39 -86
View File
@@ -10,11 +10,6 @@ import {
View,
type ViewStyle,
} from 'react-native'
import {
type ScrollHandler,
useAnimatedProps,
useSharedValue,
} from 'react-native-reanimated'
import {
type AppBskyActorDefs,
AppBskyEmbedVideo,
@@ -26,7 +21,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 {ScrollProvider} from '#/lib/ScrollContext'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
@@ -48,10 +43,10 @@ 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'
import {useProfileScrollbarAdjustment} from '#/screens/Profile/useProfileScrollbarAdjustment'
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
import {
@@ -211,7 +206,7 @@ let PostFeed = ({
savedFeedConfig,
initialNumToRender: initialNumToRenderOverride,
isVideoFeed = false,
adjustScrollIndicators,
adjustScrollIndicators = false,
collapsedHeaderHeight = 0,
}: {
feed: FeedDescriptor
@@ -1010,88 +1005,46 @@ 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 {}
const {scrollHandlers, animatedProps} = useProfileScrollbarAdjustment({
enabled: adjustScrollIndicators,
headerOffset,
collapsedHeaderHeight,
})
const list = (
<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={IS_IOS ? animatedProps : undefined}
/>
)
return (
<View testID={testID} style={style}>
{IS_IOS ? (
<ScrollProvider
onScroll={onScrollWorklet}
onBeginDrag={onBeginDragFromContext}
onEndDrag={onEndDragFromContext}
onMomentumBegin={onMomentumEndFromContext}
onMomentumEnd={onMomentumEndFromContext}>
{list}
</ScrollProvider>
) : (
list
)}
<ScrollProvider {...scrollHandlers}>
<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>
)
}