@@ -2,11 +2,12 @@ import type { PostData } from '@/src/common/lib/polycentric-hooks';
22import { isWeb } from '@/src/common/util/platform' ;
33import {
44 FlashList ,
5+ FlashListRef ,
56 type FlashListProps ,
67 type ListRenderItem ,
78 type ListRenderItemInfo ,
89} from '@shopify/flash-list' ;
9- import {
10+ import React , {
1011 cloneElement ,
1112 isValidElement ,
1213 useEffect ,
@@ -19,6 +20,7 @@ import Animated, {
1920 useAnimatedStyle ,
2021 useSharedValue ,
2122} from 'react-native-reanimated' ;
23+ import { Atoms } from '../../theme' ;
2224
2325// A reanimated-compatible FlashList.
2426const AnimatedFlashList = Animated . createAnimatedComponent ( FlashList ) ;
@@ -32,7 +34,14 @@ const HEADER_HIDE_THRESHOLD = 50;
3234
3335export type { FlashListProps , ListRenderItem , ListRenderItemInfo } ;
3436
35- export type ListProps < T > = FlashListProps < T > ;
37+ export type ListProps < T > = FlashListProps < T > & {
38+ HeaderComponent ?:
39+ | React . ComponentType < any >
40+ | React . ReactElement < unknown , string | React . JSXElementConstructor < any > >
41+ | React . ExoticComponent < any >
42+ | null
43+ | undefined ;
44+ } ;
3645
3746export function List < T extends PostData = PostData > ( props : ListProps < T > ) {
3847 if ( isWeb ) {
@@ -43,36 +52,55 @@ export function List<T extends PostData = PostData>(props: ListProps<T>) {
4352}
4453
4554function NativeList < T > ( {
46- ListHeaderComponent ,
55+ HeaderComponent ,
4756 contentContainerStyle,
4857 refreshControl,
4958 onScroll : _ignoredOnScroll ,
5059 ...rest
51- } : FlashListProps < T > ) {
60+ } : ListProps < T > ) {
61+ const ref = useRef < FlashListRef < T > > ( null ) ;
5262 const lastScrollY = useSharedValue ( 0 ) ;
5363 const headerTranslate = useSharedValue ( 0 ) ;
5464 const headerHeightShared = useSharedValue ( 0 ) ;
65+ const isDragging = useSharedValue ( false ) ;
66+ const isMomentum = useSharedValue ( false ) ;
5567 const [ headerHeight , setHeaderHeight ] = useState ( 0 ) ;
5668
57- const onScroll = useAnimatedScrollHandler ( ( event ) => {
58- const currentY = event . contentOffset . y ;
59- const h = headerHeightShared . value ;
60-
61- if ( currentY <= HEADER_HIDE_THRESHOLD ) {
62- headerTranslate . value = 0 ;
63- } else {
64- // Compute delta relative to the threshold so movement past it
65- // starts the hide from translate 0 instead of snapping.
66- const lastEffective = Math . max (
67- 0 ,
68- lastScrollY . value - HEADER_HIDE_THRESHOLD ,
69- ) ;
70- const currentEffective = currentY - HEADER_HIDE_THRESHOLD ;
71- const delta = currentEffective - lastEffective ;
72- const next = headerTranslate . value - delta ;
73- headerTranslate . value = Math . min ( 0 , Math . max ( - h , next ) ) ;
74- }
75- lastScrollY . value = currentY ;
69+ const onScroll = useAnimatedScrollHandler ( {
70+ onBeginDrag : ( ) => {
71+ isDragging . value = true ;
72+ } ,
73+ onEndDrag : ( ) => {
74+ isDragging . value = false ;
75+ } ,
76+ onMomentumBegin : ( ) => {
77+ isMomentum . value = true ;
78+ } ,
79+ onMomentumEnd : ( ) => {
80+ isMomentum . value = false ;
81+ } ,
82+ onScroll : ( event ) => {
83+ const currentY = event . contentOffset . y ;
84+ const h = headerHeightShared . value ;
85+
86+ if ( currentY <= HEADER_HIDE_THRESHOLD ) {
87+ headerTranslate . value = 0 ;
88+ } else if ( isDragging . value || isMomentum . value ) {
89+ // Compute delta relative to the threshold so movement past it
90+ // starts the hide from translate 0 instead of snapping.
91+ const lastEffective = Math . max (
92+ 0 ,
93+ lastScrollY . value - HEADER_HIDE_THRESHOLD ,
94+ ) ;
95+ const currentEffective = currentY - HEADER_HIDE_THRESHOLD ;
96+ const delta = currentEffective - lastEffective ;
97+ const next = headerTranslate . value - delta ;
98+ headerTranslate . value = Math . min ( 0 , Math . max ( - h , next ) ) ;
99+ }
100+ // Always update so the next user-driven event computes the
101+ // correct delta — even if we skipped animating this tick.
102+ lastScrollY . value = currentY ;
103+ } ,
76104 } ) ;
77105
78106 const headerAnimatedStyle = useAnimatedStyle ( ( ) => ( {
@@ -85,7 +113,7 @@ function NativeList<T>({
85113 if ( next !== headerHeight ) setHeaderHeight ( next ) ;
86114 } ;
87115
88- const renderedHeader = renderNode ( ListHeaderComponent ) ;
116+ const renderedHeader = renderNode ( HeaderComponent ) ;
89117
90118 // Show below the sticky header
91119 const adjustedRefreshControl = (
@@ -100,15 +128,16 @@ function NativeList<T>({
100128 ) as FlashListProps < T > [ 'refreshControl' ] ;
101129
102130 return (
103- < View style = { styles . container } >
131+ < View style = { [ Atoms . flex_1 ] } >
104132 { renderedHeader ? (
105133 < Animated . View
106134 onLayout = { onHeaderLayout }
107- style = { [ styles . stickyHeader , headerAnimatedStyle ] }
135+ style = { [ Atoms . absolute , styles . stickyHeader , headerAnimatedStyle ] }
108136 >
109137 { renderedHeader }
110138 </ Animated . View >
111139 ) : null }
140+
112141 < AnimatedFlashList
113142 { ...( rest as FlashListProps < unknown > ) }
114143 refreshControl = { adjustedRefreshControl }
@@ -213,11 +242,7 @@ function renderNode(node: ReactNodeOrComponent) {
213242}
214243
215244const styles = StyleSheet . create ( {
216- container : {
217- flex : 1 ,
218- } ,
219245 stickyHeader : {
220- position : 'absolute' ,
221246 top : 0 ,
222247 left : 0 ,
223248 right : 0 ,
0 commit comments