Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make scroll handling contextual (#2200)

* Add an intermediate List component

* Fix type

* Add onScrolledDownChange

* Port pager to use onScrolledDownChange

* Fix on mobile

* Don't pass down onScroll (replacement TBD)

* Remove resetMainScroll

* Replace onMainScroll with MainScrollProvider

* Hook ScrollProvider to pager

* Fix the remaining special case

* Optimize a bit

* Enforce that onScroll cannot be passed

* Keep value updated even if no handler

* Also memo it

authored by

dan and committed by
GitHub
7fd79702 fa3ccafa

+280 -354
+35
src/lib/ScrollContext.tsx
··· 1 + import React, {createContext, useContext, useMemo} from 'react' 2 + import {ScrollHandlers} from 'react-native-reanimated' 3 + 4 + const ScrollContext = createContext<ScrollHandlers<any>>({ 5 + onBeginDrag: undefined, 6 + onEndDrag: undefined, 7 + onScroll: undefined, 8 + }) 9 + 10 + export function useScrollHandlers(): ScrollHandlers<any> { 11 + return useContext(ScrollContext) 12 + } 13 + 14 + type ProviderProps = {children: React.ReactNode} & ScrollHandlers<any> 15 + 16 + // Note: this completely *overrides* the parent handlers. 17 + // It's up to you to compose them with the parent ones via useScrollHandlers() if needed. 18 + export function ScrollProvider({ 19 + children, 20 + onBeginDrag, 21 + onEndDrag, 22 + onScroll, 23 + }: ProviderProps) { 24 + const handlers = useMemo( 25 + () => ({ 26 + onBeginDrag, 27 + onEndDrag, 28 + onScroll, 29 + }), 30 + [onBeginDrag, onEndDrag, onScroll], 31 + ) 32 + return ( 33 + <ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider> 34 + ) 35 + }
+13 -41
src/lib/hooks/useOnMainScroll.ts src/view/com/util/MainScrollProvider.tsx
··· 1 - import {useState, useCallback, useMemo} from 'react' 2 - import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' 1 + import React, {useCallback} from 'react' 2 + import {ScrollProvider} from '#/lib/ScrollContext' 3 + import {NativeScrollEvent} from 'react-native' 3 4 import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' 4 5 import {useShellLayout} from '#/state/shell/shell-layout' 5 - import {s} from 'lib/styles' 6 6 import {isWeb} from 'platform/detection' 7 - import { 8 - useSharedValue, 9 - interpolate, 10 - runOnJS, 11 - ScrollHandlers, 12 - } from 'react-native-reanimated' 7 + import {useSharedValue, interpolate} from 'react-native-reanimated' 13 8 14 9 function clamp(num: number, min: number, max: number) { 15 10 'worklet' 16 11 return Math.min(Math.max(num, min), max) 17 12 } 18 13 19 - export type OnScrollCb = ( 20 - event: NativeSyntheticEvent<NativeScrollEvent>, 21 - ) => void 22 - export type OnScrollHandler = ScrollHandlers<any> 23 - export type ResetCb = () => void 24 - 25 - export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] { 14 + export function MainScrollProvider({children}: {children: React.ReactNode}) { 26 15 const {headerHeight} = useShellLayout() 27 - const [isScrolledDown, setIsScrolledDown] = useState(false) 28 16 const mode = useMinimalShellMode() 29 17 const setMode = useSetMinimalShellMode() 30 18 const startDragOffset = useSharedValue<number | null>(null) ··· 58 46 const onScroll = useCallback( 59 47 (e: NativeScrollEvent) => { 60 48 'worklet' 61 - // Keep track of whether we want to show "scroll to top". 62 - if (!isScrolledDown && e.contentOffset.y > s.window.height) { 63 - runOnJS(setIsScrolledDown)(true) 64 - } else if (isScrolledDown && e.contentOffset.y < s.window.height) { 65 - runOnJS(setIsScrolledDown)(false) 66 - } 67 - 68 49 if (startDragOffset.value === null || startMode.value === null) { 69 50 if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) { 70 51 // If we're close enough to the top, always show the shell. ··· 102 83 startMode.value = mode.value 103 84 } 104 85 }, 105 - [headerHeight, mode, setMode, isScrolledDown, startDragOffset, startMode], 86 + [headerHeight, mode, setMode, startDragOffset, startMode], 106 87 ) 107 88 108 - const scrollHandler: ScrollHandlers<any> = useMemo( 109 - () => ({ 110 - onBeginDrag, 111 - onEndDrag, 112 - onScroll, 113 - }), 114 - [onBeginDrag, onEndDrag, onScroll], 89 + return ( 90 + <ScrollProvider 91 + onBeginDrag={onBeginDrag} 92 + onEndDrag={onEndDrag} 93 + onScroll={onScroll}> 94 + {children} 95 + </ScrollProvider> 115 96 ) 116 - 117 - return [ 118 - scrollHandler, 119 - isScrolledDown, 120 - useCallback(() => { 121 - setIsScrolledDown(false) 122 - setMode(false) 123 - }, [setMode]), 124 - ] 125 97 }
+25 -21
src/view/com/feeds/FeedPage.tsx
··· 7 7 import {useAnalytics} from 'lib/analytics/analytics' 8 8 import {useQueryClient} from '@tanstack/react-query' 9 9 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 10 - import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 10 + import {MainScrollProvider} from '../util/MainScrollProvider' 11 11 import {usePalette} from 'lib/hooks/usePalette' 12 12 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 13 + import {useSetMinimalShellMode} from '#/state/shell' 13 14 import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' 14 15 import {ComposeIcon2} from 'lib/icons' 15 16 import {colors, s} from 'lib/styles' 16 - import {FlatList, View, useWindowDimensions} from 'react-native' 17 + import {View, useWindowDimensions} from 'react-native' 18 + import {ListMethods} from '../util/List' 17 19 import {Feed} from '../posts/Feed' 18 20 import {TextLink} from '../util/Link' 19 21 import {FAB} from '../util/fab/FAB' ··· 51 53 const {isDesktop} = useWebMediaQueries() 52 54 const queryClient = useQueryClient() 53 55 const {openComposer} = useComposerControls() 54 - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() 56 + const [isScrolledDown, setIsScrolledDown] = React.useState(false) 57 + const setMinimalShellMode = useSetMinimalShellMode() 55 58 const {screen, track} = useAnalytics() 56 59 const headerOffset = useHeaderOffset() 57 - const scrollElRef = React.useRef<FlatList>(null) 60 + const scrollElRef = React.useRef<ListMethods>(null) 58 61 const [hasNew, setHasNew] = React.useState(false) 59 62 60 63 const scrollToTop = React.useCallback(() => { ··· 62 65 animated: isNative, 63 66 offset: -headerOffset, 64 67 }) 65 - resetMainScroll() 66 - }, [headerOffset, resetMainScroll]) 68 + setMinimalShellMode(false) 69 + }, [headerOffset, setMinimalShellMode]) 67 70 68 71 const onSoftReset = React.useCallback(() => { 69 72 const isScreenFocused = ··· 164 167 165 168 return ( 166 169 <View testID={testID} style={s.h100pct}> 167 - <Feed 168 - testID={testID ? `${testID}-feed` : undefined} 169 - enabled={isPageFocused} 170 - feed={feed} 171 - feedParams={feedParams} 172 - pollInterval={POLL_FREQ} 173 - scrollElRef={scrollElRef} 174 - onScroll={onMainScroll} 175 - onHasNew={setHasNew} 176 - scrollEventThrottle={1} 177 - renderEmptyState={renderEmptyState} 178 - renderEndOfFeed={renderEndOfFeed} 179 - ListHeaderComponent={ListHeaderComponent} 180 - headerOffset={headerOffset} 181 - /> 170 + <MainScrollProvider> 171 + <Feed 172 + testID={testID ? `${testID}-feed` : undefined} 173 + enabled={isPageFocused} 174 + feed={feed} 175 + feedParams={feedParams} 176 + pollInterval={POLL_FREQ} 177 + scrollElRef={scrollElRef} 178 + onScrolledDownChange={setIsScrolledDown} 179 + onHasNew={setHasNew} 180 + renderEmptyState={renderEmptyState} 181 + renderEndOfFeed={renderEndOfFeed} 182 + ListHeaderComponent={ListHeaderComponent} 183 + headerOffset={headerOffset} 184 + /> 185 + </MainScrollProvider> 182 186 {(isScrolledDown || hasNew) && ( 183 187 <LoadLatestBtn 184 188 onPress={onPressLoadLatest}
+5 -21
src/view/com/feeds/ProfileFeedgens.tsx
··· 1 - import React, {MutableRefObject} from 'react' 1 + import React from 'react' 2 2 import { 3 3 Dimensions, 4 4 RefreshControl, ··· 8 8 ViewStyle, 9 9 } from 'react-native' 10 10 import {useQueryClient} from '@tanstack/react-query' 11 - import {FlatList} from '../util/Views' 11 + import {List, ListRef} from '../util/List' 12 12 import {FeedSourceCardLoaded} from './FeedSourceCard' 13 13 import {ErrorMessage} from '../util/error/ErrorMessage' 14 14 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 15 15 import {Text} from '../util/text/Text' 16 16 import {usePalette} from 'lib/hooks/usePalette' 17 17 import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens' 18 - import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll' 19 18 import {logger} from '#/logger' 20 19 import {Trans} from '@lingui/macro' 21 20 import {cleanError} from '#/lib/strings/errors' 22 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 23 21 import {useTheme} from '#/lib/ThemeContext' 24 22 import {usePreferencesQuery} from '#/state/queries/preferences' 25 23 import {hydrateFeedGenerator} from '#/state/queries/feed' ··· 37 35 38 36 interface ProfileFeedgensProps { 39 37 did: string 40 - scrollElRef: MutableRefObject<FlatList<any> | null> 41 - onScroll?: OnScrollHandler 42 - scrollEventThrottle?: number 38 + scrollElRef: ListRef 43 39 headerOffset: number 44 40 enabled?: boolean 45 41 style?: StyleProp<ViewStyle> ··· 50 46 SectionRef, 51 47 ProfileFeedgensProps 52 48 >(function ProfileFeedgensImpl( 53 - { 54 - did, 55 - scrollElRef, 56 - onScroll, 57 - scrollEventThrottle, 58 - headerOffset, 59 - enabled, 60 - style, 61 - testID, 62 - }, 49 + {did, scrollElRef, headerOffset, enabled, style, testID}, 63 50 ref, 64 51 ) { 65 52 const pal = usePalette('default') ··· 185 172 [error, refetch, onPressRetryLoadMore, pal, preferences], 186 173 ) 187 174 188 - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) 189 175 return ( 190 176 <View testID={testID} style={style}> 191 - <FlatList 177 + <List 192 178 testID={testID ? `${testID}-flatlist` : undefined} 193 179 ref={scrollElRef} 194 180 data={items} ··· 207 193 minHeight: Dimensions.get('window').height * 1.5, 208 194 }} 209 195 style={{paddingTop: headerOffset}} 210 - onScroll={onScroll != null ? scrollHandler : undefined} 211 - scrollEventThrottle={scrollEventThrottle} 212 196 indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 213 197 removeClippedSubviews={true} 214 198 contentOffset={{x: 0, y: headerOffset * -1}}
+7 -13
src/view/com/lists/ListMembers.tsx
··· 1 - import React, {MutableRefObject} from 'react' 1 + import React from 'react' 2 2 import { 3 3 ActivityIndicator, 4 4 Dimensions, ··· 8 8 ViewStyle, 9 9 } from 'react-native' 10 10 import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' 11 - import {FlatList} from '../util/Views' 11 + import {List, ListRef} from '../util/List' 12 12 import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 13 13 import {ErrorMessage} from '../util/error/ErrorMessage' 14 14 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' ··· 18 18 import {usePalette} from 'lib/hooks/usePalette' 19 19 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 20 20 import {useListMembersQuery} from '#/state/queries/list-members' 21 - import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' 22 21 import {logger} from '#/logger' 23 22 import {useModalControls} from '#/state/modals' 24 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 25 23 import {useSession} from '#/state/session' 26 24 import {cleanError} from '#/lib/strings/errors' 27 25 ··· 34 32 list, 35 33 style, 36 34 scrollElRef, 37 - onScroll, 35 + onScrolledDownChange, 38 36 onPressTryAgain, 39 37 renderHeader, 40 38 renderEmptyState, 41 39 testID, 42 - scrollEventThrottle, 43 40 headerOffset = 0, 44 41 desktopFixedHeightOffset, 45 42 }: { 46 43 list: string 47 44 style?: StyleProp<ViewStyle> 48 - scrollElRef?: MutableRefObject<FlatList<any> | null> 49 - onScroll: OnScrollHandler 45 + scrollElRef?: ListRef 46 + onScrolledDownChange: (isScrolledDown: boolean) => void 50 47 onPressTryAgain?: () => void 51 48 renderHeader: () => JSX.Element 52 49 renderEmptyState: () => JSX.Element 53 50 testID?: string 54 - scrollEventThrottle?: number 55 51 headerOffset?: number 56 52 desktopFixedHeightOffset?: number 57 53 }) { ··· 209 205 [isFetching], 210 206 ) 211 207 212 - const scrollHandler = useAnimatedScrollHandler(onScroll) 213 208 return ( 214 209 <View testID={testID} style={style}> 215 - <FlatList 210 + <List 216 211 testID={testID ? `${testID}-flatlist` : undefined} 217 212 ref={scrollElRef} 218 213 data={items} ··· 233 228 minHeight: Dimensions.get('window').height * 1.5, 234 229 }} 235 230 style={{paddingTop: headerOffset}} 236 - onScroll={scrollHandler} 231 + onScrolledDownChange={onScrolledDownChange} 237 232 onEndReached={onEndReached} 238 233 onEndReachedThreshold={0.6} 239 - scrollEventThrottle={scrollEventThrottle} 240 234 removeClippedSubviews={true} 241 235 contentOffset={{x: 0, y: headerOffset * -1}} 242 236 // @ts-ignore our .web version only -prf
+2 -2
src/view/com/lists/MyLists.tsx
··· 15 15 import {Text} from '../util/text/Text' 16 16 import {useAnalytics} from 'lib/analytics/analytics' 17 17 import {usePalette} from 'lib/hooks/usePalette' 18 - import {FlatList} from '../util/Views' 18 + import {List} from '../util/List' 19 19 import {s} from 'lib/styles' 20 20 import {logger} from '#/logger' 21 21 import {Trans} from '@lingui/macro' ··· 119 119 [error, onRefresh, renderItem, pal], 120 120 ) 121 121 122 - const FlatListCom = inline ? RNFlatList : FlatList 122 + const FlatListCom = inline ? RNFlatList : List 123 123 return ( 124 124 <View testID={testID} style={style}> 125 125 {items.length > 0 && (
+5 -21
src/view/com/lists/ProfileLists.tsx
··· 1 - import React, {MutableRefObject} from 'react' 1 + import React from 'react' 2 2 import { 3 3 Dimensions, 4 4 RefreshControl, ··· 8 8 ViewStyle, 9 9 } from 'react-native' 10 10 import {useQueryClient} from '@tanstack/react-query' 11 - import {FlatList} from '../util/Views' 11 + import {List, ListRef} from '../util/List' 12 12 import {ListCard} from './ListCard' 13 13 import {ErrorMessage} from '../util/error/ErrorMessage' 14 14 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' ··· 16 16 import {useAnalytics} from 'lib/analytics/analytics' 17 17 import {usePalette} from 'lib/hooks/usePalette' 18 18 import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists' 19 - import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll' 20 19 import {logger} from '#/logger' 21 20 import {Trans} from '@lingui/macro' 22 21 import {cleanError} from '#/lib/strings/errors' 23 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 24 22 import {useTheme} from '#/lib/ThemeContext' 25 23 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 26 24 import {isNative} from '#/platform/detection' ··· 36 34 37 35 interface ProfileListsProps { 38 36 did: string 39 - scrollElRef: MutableRefObject<FlatList<any> | null> 40 - onScroll?: OnScrollHandler 41 - scrollEventThrottle?: number 37 + scrollElRef: ListRef 42 38 headerOffset: number 43 39 enabled?: boolean 44 40 style?: StyleProp<ViewStyle> ··· 47 43 48 44 export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( 49 45 function ProfileListsImpl( 50 - { 51 - did, 52 - scrollElRef, 53 - onScroll, 54 - scrollEventThrottle, 55 - headerOffset, 56 - enabled, 57 - style, 58 - testID, 59 - }, 46 + {did, scrollElRef, headerOffset, enabled, style, testID}, 60 47 ref, 61 48 ) { 62 49 const pal = usePalette('default') ··· 187 174 [error, refetch, onPressRetryLoadMore, pal], 188 175 ) 189 176 190 - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) 191 177 return ( 192 178 <View testID={testID} style={style}> 193 - <FlatList 179 + <List 194 180 testID={testID ? `${testID}-flatlist` : undefined} 195 181 ref={scrollElRef} 196 182 data={items} ··· 209 195 minHeight: Dimensions.get('window').height * 1.5, 210 196 }} 211 197 style={{paddingTop: headerOffset}} 212 - onScroll={onScroll != null ? scrollHandler : undefined} 213 - scrollEventThrottle={scrollEventThrottle} 214 198 indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 215 199 removeClippedSubviews={true} 216 200 contentOffset={{x: 0, y: headerOffset * -1}}
+8 -11
src/view/com/notifications/Feed.tsx
··· 1 - import React, {MutableRefObject} from 'react' 2 - import {CenteredView, FlatList} from '../util/Views' 1 + import React from 'react' 2 + import {CenteredView} from '../util/Views' 3 3 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' 4 4 import {FeedItem} from './FeedItem' 5 5 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 6 6 import {ErrorMessage} from '../util/error/ErrorMessage' 7 7 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 8 8 import {EmptyState} from '../util/EmptyState' 9 - import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' 10 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 11 9 import {s} from 'lib/styles' 12 10 import {usePalette} from 'lib/hooks/usePalette' 13 11 import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' ··· 15 13 import {logger} from '#/logger' 16 14 import {cleanError} from '#/lib/strings/errors' 17 15 import {useModerationOpts} from '#/state/queries/preferences' 16 + import {List, ListRef} from '../util/List' 18 17 19 18 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} 20 19 const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} ··· 23 22 export function Feed({ 24 23 scrollElRef, 25 24 onPressTryAgain, 26 - onScroll, 25 + onScrolledDownChange, 27 26 ListHeaderComponent, 28 27 }: { 29 - scrollElRef?: MutableRefObject<FlatList<any> | null> 28 + scrollElRef?: ListRef 30 29 onPressTryAgain?: () => void 31 - onScroll?: OnScrollHandler 30 + onScrolledDownChange: (isScrolledDown: boolean) => void 32 31 ListHeaderComponent?: () => JSX.Element 33 32 }) { 34 33 const pal = usePalette('default') ··· 135 134 [isFetchingNextPage], 136 135 ) 137 136 138 - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) 139 137 return ( 140 138 <View style={s.hContentRegion}> 141 139 {error && ( ··· 146 144 /> 147 145 </CenteredView> 148 146 )} 149 - <FlatList 147 + <List 150 148 testID="notifsFeed" 151 149 ref={scrollElRef} 152 150 data={items} ··· 164 162 } 165 163 onEndReached={onEndReached} 166 164 onEndReachedThreshold={0.6} 167 - onScroll={scrollHandler} 168 - scrollEventThrottle={1} 165 + onScrolledDownChange={onScrolledDownChange} 169 166 contentContainerStyle={s.contentContainer} 170 167 // @ts-ignore our .web version only -prf 171 168 desktopFixedHeight
+14 -33
src/view/com/pager/PagerWithHeader.tsx
··· 1 1 import * as React from 'react' 2 2 import { 3 3 LayoutChangeEvent, 4 - FlatList, 5 4 ScrollView, 6 5 StyleSheet, 7 6 View, ··· 20 19 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' 21 20 import {TabBar} from './TabBar' 22 21 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 23 - import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' 24 22 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 25 - 26 - const SCROLLED_DOWN_LIMIT = 200 23 + import {ListMethods} from '../util/List' 24 + import {ScrollProvider} from '#/lib/ScrollContext' 27 25 28 26 export interface PagerWithHeaderChildParams { 29 27 headerHeight: number 30 28 isFocused: boolean 31 - onScroll: OnScrollHandler 32 - isScrolledDown: boolean 33 - scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null> 29 + scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null> 34 30 } 35 31 36 32 export interface PagerWithHeaderProps { ··· 62 58 const [currentPage, setCurrentPage] = React.useState(0) 63 59 const [tabBarHeight, setTabBarHeight] = React.useState(0) 64 60 const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) 65 - const [isScrolledDown, setIsScrolledDown] = React.useState(false) 66 61 const scrollY = useSharedValue(0) 67 62 const headerHeight = headerOnlyHeight + tabBarHeight 68 63 ··· 155 150 if (!throttleTimeout.current) { 156 151 throttleTimeout.current = setTimeout(() => { 157 152 throttleTimeout.current = null 158 - 159 153 runOnUI(adjustScrollForOtherPages)() 160 - 161 - const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT 162 - if (isScrolledDown !== nextIsScrolledDown) { 163 - React.startTransition(() => { 164 - setIsScrolledDown(nextIsScrolledDown) 165 - }) 166 - } 167 154 }, 80 /* Sync often enough you're unlikely to catch it unsynced */) 168 155 } 169 156 }) ··· 211 198 index={i} 212 199 isReady={isReady} 213 200 isFocused={i === currentPage} 214 - isScrolledDown={isScrolledDown} 215 201 onScrollWorklet={i === currentPage ? onScrollWorklet : noop} 216 202 registerRef={registerRef} 217 203 renderTab={child} ··· 293 279 index, 294 280 isReady, 295 281 isFocused, 296 - isScrolledDown, 297 282 onScrollWorklet, 298 283 renderTab, 299 284 registerRef, ··· 302 287 index: number 303 288 isFocused: boolean 304 289 isReady: boolean 305 - isScrolledDown: boolean 306 290 registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void 307 291 onScrollWorklet: (e: NativeScrollEvent) => void 308 292 renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null ··· 316 300 } 317 301 }, [scrollElRef, registerRef, index]) 318 302 319 - const scrollHandler = React.useMemo( 320 - () => ({onScroll: onScrollWorklet}), 321 - [onScrollWorklet], 322 - ) 323 - 324 303 if (!isReady || renderTab == null) { 325 304 return null 326 305 } 327 306 328 - return renderTab({ 329 - headerHeight, 330 - isFocused, 331 - isScrolledDown, 332 - onScroll: scrollHandler, 333 - scrollElRef: scrollElRef as React.MutableRefObject< 334 - FlatList<any> | ScrollView | null 335 - >, 336 - }) 307 + return ( 308 + <ScrollProvider onScroll={onScrollWorklet}> 309 + {renderTab({ 310 + headerHeight, 311 + isFocused, 312 + scrollElRef: scrollElRef as React.MutableRefObject< 313 + ListMethods | ScrollView | null 314 + >, 315 + })} 316 + </ScrollProvider> 317 + ) 337 318 } 338 319 339 320 const styles = StyleSheet.create({
+3 -2
src/view/com/post-thread/PostLikedBy.tsx
··· 1 1 import React, {useCallback, useMemo, useState} from 'react' 2 2 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' 3 3 import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' 4 - import {CenteredView, FlatList} from '../util/Views' 4 + import {CenteredView} from '../util/Views' 5 + import {List} from '../util/List' 5 6 import {ErrorMessage} from '../util/error/ErrorMessage' 6 7 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' 7 8 import {usePalette} from 'lib/hooks/usePalette' ··· 84 85 // loaded 85 86 // = 86 87 return ( 87 - <FlatList 88 + <List 88 89 data={likes} 89 90 keyExtractor={item => item.actor.did} 90 91 refreshControl={
+3 -2
src/view/com/post-thread/PostRepostedBy.tsx
··· 1 1 import React, {useMemo, useCallback, useState} from 'react' 2 2 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' 3 3 import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 4 - import {CenteredView, FlatList} from '../util/Views' 4 + import {CenteredView} from '../util/Views' 5 + import {List} from '../util/List' 5 6 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' 6 7 import {ErrorMessage} from '../util/error/ErrorMessage' 7 8 import {usePalette} from 'lib/hooks/usePalette' ··· 85 86 // loaded 86 87 // = 87 88 return ( 88 - <FlatList 89 + <List 89 90 data={repostedBy} 90 91 keyExtractor={item => item.did} 91 92 refreshControl={
+4 -3
src/view/com/post-thread/PostThread.tsx
··· 8 8 View, 9 9 } from 'react-native' 10 10 import {AppBskyFeedDefs} from '@atproto/api' 11 - import {CenteredView, FlatList} from '../util/Views' 11 + import {CenteredView} from '../util/Views' 12 + import {List, ListMethods} from '../util/List' 12 13 import { 13 14 FontAwesomeIcon, 14 15 FontAwesomeIconStyle, ··· 140 141 const {_} = useLingui() 141 142 const pal = usePalette('default') 142 143 const {isTablet, isDesktop} = useWebMediaQueries() 143 - const ref = useRef<FlatList>(null) 144 + const ref = useRef<ListMethods>(null) 144 145 const highlightedPostRef = useRef<View | null>(null) 145 146 const needsScrollAdjustment = useRef<boolean>( 146 147 !isNative || // web always uses scroll adjustment ··· 335 336 ) 336 337 337 338 return ( 338 - <FlatList 339 + <List 339 340 ref={ref} 340 341 data={posts} 341 342 initialNumToRender={!isNative ? posts.length : undefined}
+7 -13
src/view/com/posts/Feed.tsx
··· 1 - import React, {memo, MutableRefObject} from 'react' 1 + import React, {memo} from 'react' 2 2 import { 3 3 ActivityIndicator, 4 4 AppState, ··· 10 10 ViewStyle, 11 11 } from 'react-native' 12 12 import {useQueryClient} from '@tanstack/react-query' 13 - import {FlatList} from '../util/Views' 13 + import {List, ListRef} from '../util/List' 14 14 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 15 15 import {FeedErrorMessage} from './FeedErrorMessage' 16 16 import {FeedSlice} from './FeedSlice' 17 17 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 18 - import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' 19 18 import {useAnalytics} from 'lib/analytics/analytics' 20 19 import {usePalette} from 'lib/hooks/usePalette' 21 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 22 20 import {useTheme} from 'lib/ThemeContext' 23 21 import {logger} from '#/logger' 24 22 import { ··· 45 43 enabled, 46 44 pollInterval, 47 45 scrollElRef, 48 - onScroll, 46 + onScrolledDownChange, 49 47 onHasNew, 50 - scrollEventThrottle, 51 48 renderEmptyState, 52 49 renderEndOfFeed, 53 50 testID, ··· 62 59 style?: StyleProp<ViewStyle> 63 60 enabled?: boolean 64 61 pollInterval?: number 65 - scrollElRef?: MutableRefObject<FlatList<any> | null> 62 + scrollElRef?: ListRef 66 63 onHasNew?: (v: boolean) => void 67 - onScroll?: OnScrollHandler 68 - scrollEventThrottle?: number 64 + onScrolledDownChange?: (isScrolledDown: boolean) => void 69 65 renderEmptyState: () => JSX.Element 70 66 renderEndOfFeed?: () => JSX.Element 71 67 testID?: string ··· 270 266 ) 271 267 }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) 272 268 273 - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) 274 269 return ( 275 270 <View testID={testID} style={style}> 276 - <FlatList 271 + <List 277 272 testID={testID ? `${testID}-flatlist` : undefined} 278 273 ref={scrollElRef} 279 274 data={feedItems} ··· 294 289 minHeight: Dimensions.get('window').height * 1.5, 295 290 }} 296 291 style={{paddingTop: headerOffset}} 297 - onScroll={onScroll != null ? scrollHandler : undefined} 298 - scrollEventThrottle={scrollEventThrottle} 292 + onScrolledDownChange={onScrolledDownChange} 299 293 indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 300 294 onEndReached={onEndReached} 301 295 onEndReachedThreshold={2} // number of posts left to trigger load more
+3 -2
src/view/com/profile/ProfileFollowers.tsx
··· 1 1 import React from 'react' 2 2 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' 3 3 import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 4 - import {CenteredView, FlatList} from '../util/Views' 4 + import {CenteredView} from '../util/Views' 5 + import {List} from '../util/List' 5 6 import {ErrorMessage} from '../util/error/ErrorMessage' 6 7 import {ProfileCardWithFollowBtn} from './ProfileCard' 7 8 import {usePalette} from 'lib/hooks/usePalette' ··· 86 87 // loaded 87 88 // = 88 89 return ( 89 - <FlatList 90 + <List 90 91 data={followers} 91 92 keyExtractor={item => item.did} 92 93 refreshControl={
+3 -2
src/view/com/profile/ProfileFollows.tsx
··· 1 1 import React from 'react' 2 2 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' 3 3 import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 4 - import {CenteredView, FlatList} from '../util/Views' 4 + import {CenteredView} from '../util/Views' 5 + import {List} from '../util/List' 5 6 import {ErrorMessage} from '../util/error/ErrorMessage' 6 7 import {ProfileCardWithFollowBtn} from './ProfileCard' 7 8 import {usePalette} from 'lib/hooks/usePalette' ··· 86 87 // loaded 87 88 // = 88 89 return ( 89 - <FlatList 90 + <List 90 91 data={follows} 91 92 keyExtractor={item => item.did} 92 93 refreshControl={
+64
src/view/com/util/List.tsx
··· 1 + import React, {memo, startTransition} from 'react' 2 + import {FlatListProps} from 'react-native' 3 + import {FlatList_INTERNAL} from './Views' 4 + import {useScrollHandlers} from '#/lib/ScrollContext' 5 + import {runOnJS, useSharedValue} from 'react-native-reanimated' 6 + import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 7 + 8 + export type ListMethods = FlatList_INTERNAL 9 + export type ListProps<ItemT> = Omit< 10 + FlatListProps<ItemT>, 11 + 'onScroll' // Use ScrollContext instead. 12 + > & { 13 + onScrolledDownChange?: (isScrolledDown: boolean) => void 14 + } 15 + export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> 16 + 17 + const SCROLLED_DOWN_LIMIT = 200 18 + 19 + function ListImpl<ItemT>( 20 + {onScrolledDownChange, ...props}: ListProps<ItemT>, 21 + ref: React.Ref<ListMethods>, 22 + ) { 23 + const isScrolledDown = useSharedValue(false) 24 + const contextScrollHandlers = useScrollHandlers() 25 + 26 + function handleScrolledDownChange(didScrollDown: boolean) { 27 + startTransition(() => { 28 + onScrolledDownChange?.(didScrollDown) 29 + }) 30 + } 31 + 32 + const scrollHandler = useAnimatedScrollHandler({ 33 + onBeginDrag(e, ctx) { 34 + contextScrollHandlers.onBeginDrag?.(e, ctx) 35 + }, 36 + onEndDrag(e, ctx) { 37 + contextScrollHandlers.onEndDrag?.(e, ctx) 38 + }, 39 + onScroll(e, ctx) { 40 + contextScrollHandlers.onScroll?.(e, ctx) 41 + 42 + const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT 43 + if (isScrolledDown.value !== didScrollDown) { 44 + isScrolledDown.value = didScrollDown 45 + if (onScrolledDownChange != null) { 46 + runOnJS(handleScrolledDownChange)(didScrollDown) 47 + } 48 + } 49 + }, 50 + }) 51 + 52 + return ( 53 + <FlatList_INTERNAL 54 + {...props} 55 + onScroll={scrollHandler} 56 + scrollEventThrottle={1} 57 + ref={ref} 58 + /> 59 + ) 60 + } 61 + 62 + export const List = memo(React.forwardRef(ListImpl)) as <ItemT>( 63 + props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>}, 64 + ) => React.ReactElement
+6 -5
src/view/com/util/ViewSelector.tsx
··· 1 1 import React, {useEffect, useState} from 'react' 2 2 import { 3 + NativeSyntheticEvent, 4 + NativeScrollEvent, 3 5 Pressable, 4 6 RefreshControl, 5 7 StyleSheet, 6 8 View, 7 9 ScrollView, 8 10 } from 'react-native' 9 - import {FlatList} from './Views' 10 - import {OnScrollCb} from 'lib/hooks/useOnMainScroll' 11 + import {FlatList_INTERNAL} from './Views' 11 12 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 12 13 import {Text} from './text/Text' 13 14 import {usePalette} from 'lib/hooks/usePalette' ··· 38 39 | null 39 40 | undefined 40 41 onSelectView?: (viewIndex: number) => void 41 - onScroll?: OnScrollCb 42 + onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void 42 43 onRefresh?: () => void 43 44 onEndReached?: (info: {distanceFromEnd: number}) => void 44 45 } ··· 59 60 ) { 60 61 const pal = usePalette('default') 61 62 const [selectedIndex, setSelectedIndex] = useState<number>(0) 62 - const flatListRef = React.useRef<FlatList>(null) 63 + const flatListRef = React.useRef<FlatList_INTERNAL>(null) 63 64 64 65 // events 65 66 // = ··· 110 111 [items], 111 112 ) 112 113 return ( 113 - <FlatList 114 + <FlatList_INTERNAL 114 115 ref={flatListRef} 115 116 data={data} 116 117 keyExtractor={keyExtractor}
+1 -1
src/view/com/util/Views.d.ts
··· 1 1 import React from 'react' 2 2 import {ViewProps} from 'react-native' 3 - export {FlatList, ScrollView} from 'react-native' 3 + export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native' 4 4 export function CenteredView({ 5 5 style, 6 6 sideBorders,
+1 -1
src/view/com/util/Views.jsx
··· 2 2 import {View} from 'react-native' 3 3 import Animated from 'react-native-reanimated' 4 4 5 - export const FlatList = Animated.FlatList 5 + export const FlatList_INTERNAL = Animated.FlatList 6 6 export const ScrollView = Animated.ScrollView 7 7 export function CenteredView(props) { 8 8 return <View {...props} />
+1 -1
src/view/com/util/Views.web.tsx
··· 49 49 return <View style={style} {...props} /> 50 50 } 51 51 52 - export const FlatList = React.forwardRef(function FlatListImpl<ItemT>( 52 + export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>( 53 53 { 54 54 contentContainerStyle, 55 55 style,
+2 -2
src/view/screens/Feeds.tsx
··· 19 19 import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 20 20 import debounce from 'lodash.debounce' 21 21 import {Text} from 'view/com/util/text/Text' 22 - import {FlatList} from 'view/com/util/Views' 22 + import {List} from 'view/com/util/List' 23 23 import {useFocusEffect} from '@react-navigation/native' 24 24 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 25 25 import {Trans, msg} from '@lingui/macro' ··· 481 481 482 482 {preferences ? <View /> : <ActivityIndicator />} 483 483 484 - <FlatList 484 + <List 485 485 style={[!isTabletOrDesktop && s.flex1, styles.list]} 486 486 data={items} 487 487 keyExtractor={item => item.key}
+14 -11
src/view/screens/Notifications.tsx
··· 1 1 import React from 'react' 2 - import {FlatList, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 import {useFocusEffect} from '@react-navigation/native' 4 4 import {useQueryClient} from '@tanstack/react-query' 5 5 import { ··· 9 9 import {ViewHeader} from '../com/util/ViewHeader' 10 10 import {Feed} from '../com/notifications/Feed' 11 11 import {TextLink} from 'view/com/util/Link' 12 + import {ListMethods} from 'view/com/util/List' 12 13 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 13 - import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 14 + import {MainScrollProvider} from '../com/util/MainScrollProvider' 14 15 import {usePalette} from 'lib/hooks/usePalette' 15 16 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 16 17 import {s, colors} from 'lib/styles' ··· 35 36 export function NotificationsScreen({}: Props) { 36 37 const {_} = useLingui() 37 38 const setMinimalShellMode = useSetMinimalShellMode() 38 - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() 39 - const scrollElRef = React.useRef<FlatList>(null) 39 + const [isScrolledDown, setIsScrolledDown] = React.useState(false) 40 + const scrollElRef = React.useRef<ListMethods>(null) 40 41 const checkLatestRef = React.useRef<() => void | null>() 41 42 const {screen} = useAnalytics() 42 43 const pal = usePalette('default') ··· 50 51 // = 51 52 const scrollToTop = React.useCallback(() => { 52 53 scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0}) 53 - resetMainScroll() 54 - }, [scrollElRef, resetMainScroll]) 54 + setMinimalShellMode(false) 55 + }, [scrollElRef, setMinimalShellMode]) 55 56 56 57 const onPressLoadLatest = React.useCallback(() => { 57 58 scrollToTop() ··· 130 131 return ( 131 132 <View testID="notificationsScreen" style={s.hContentRegion}> 132 133 <ViewHeader title={_(msg`Notifications`)} canGoBack={false} /> 133 - <Feed 134 - onScroll={onMainScroll} 135 - scrollElRef={scrollElRef} 136 - ListHeaderComponent={ListHeaderComponent} 137 - /> 134 + <MainScrollProvider> 135 + <Feed 136 + onScrolledDownChange={setIsScrolledDown} 137 + scrollElRef={scrollElRef} 138 + ListHeaderComponent={ListHeaderComponent} 139 + /> 140 + </MainScrollProvider> 138 141 {(isScrolledDown || hasNew) && ( 139 142 <LoadLatestBtn 140 143 onPress={onPressLoadLatest}
+18 -64
src/view/screens/Profile.tsx
··· 5 5 import {msg, Trans} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 8 - import {CenteredView, FlatList} from '../com/util/Views' 8 + import {CenteredView} from '../com/util/Views' 9 + import {ListRef} from '../com/util/List' 9 10 import {ScreenHider} from 'view/com/util/moderation/ScreenHider' 10 11 import {Feed} from 'view/com/posts/Feed' 11 12 import {ProfileLists} from '../com/lists/ProfileLists' ··· 20 21 import {ComposeIcon2} from 'lib/icons' 21 22 import {useSetTitle} from 'lib/hooks/useSetTitle' 22 23 import {combinedDisplayName} from 'lib/strings/display-names' 23 - import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll' 24 24 import {FeedDescriptor} from '#/state/queries/post-feed' 25 25 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 26 26 import {useProfileQuery} from '#/state/queries/profile' ··· 277 277 onPageSelected={onPageSelected} 278 278 onCurrentPageSelected={onCurrentPageSelected} 279 279 renderHeader={renderHeader}> 280 - {({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => ( 280 + {({headerHeight, isFocused, scrollElRef}) => ( 281 281 <FeedSection 282 282 ref={postsSectionRef} 283 283 feed={`author|${profile.did}|posts_and_author_threads`} 284 - onScroll={onScroll} 285 284 headerHeight={headerHeight} 286 285 isFocused={isFocused} 287 - isScrolledDown={isScrolledDown} 288 - scrollElRef={ 289 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 290 - } 286 + scrollElRef={scrollElRef as ListRef} 291 287 ignoreFilterFor={profile.did} 292 288 /> 293 289 )} 294 290 {showRepliesTab 295 - ? ({ 296 - onScroll, 297 - headerHeight, 298 - isFocused, 299 - isScrolledDown, 300 - scrollElRef, 301 - }) => ( 291 + ? ({headerHeight, isFocused, scrollElRef}) => ( 302 292 <FeedSection 303 293 ref={repliesSectionRef} 304 294 feed={`author|${profile.did}|posts_with_replies`} 305 - onScroll={onScroll} 306 295 headerHeight={headerHeight} 307 296 isFocused={isFocused} 308 - isScrolledDown={isScrolledDown} 309 - scrollElRef={ 310 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 311 - } 297 + scrollElRef={scrollElRef as ListRef} 312 298 ignoreFilterFor={profile.did} 313 299 /> 314 300 ) 315 301 : null} 316 - {({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => ( 302 + {({headerHeight, isFocused, scrollElRef}) => ( 317 303 <FeedSection 318 304 ref={mediaSectionRef} 319 305 feed={`author|${profile.did}|posts_with_media`} 320 - onScroll={onScroll} 321 306 headerHeight={headerHeight} 322 307 isFocused={isFocused} 323 - isScrolledDown={isScrolledDown} 324 - scrollElRef={ 325 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 326 - } 308 + scrollElRef={scrollElRef as ListRef} 327 309 ignoreFilterFor={profile.did} 328 310 /> 329 311 )} 330 312 {showLikesTab 331 - ? ({ 332 - onScroll, 333 - headerHeight, 334 - isFocused, 335 - isScrolledDown, 336 - scrollElRef, 337 - }) => ( 313 + ? ({headerHeight, isFocused, scrollElRef}) => ( 338 314 <FeedSection 339 315 ref={likesSectionRef} 340 316 feed={`likes|${profile.did}`} 341 - onScroll={onScroll} 342 317 headerHeight={headerHeight} 343 318 isFocused={isFocused} 344 - isScrolledDown={isScrolledDown} 345 - scrollElRef={ 346 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 347 - } 319 + scrollElRef={scrollElRef as ListRef} 348 320 ignoreFilterFor={profile.did} 349 321 /> 350 322 ) 351 323 : null} 352 324 {showFeedsTab 353 - ? ({onScroll, headerHeight, isFocused, scrollElRef}) => ( 325 + ? ({headerHeight, isFocused, scrollElRef}) => ( 354 326 <ProfileFeedgens 355 327 ref={feedsSectionRef} 356 328 did={profile.did} 357 - scrollElRef={ 358 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 359 - } 360 - onScroll={onScroll} 361 - scrollEventThrottle={1} 329 + scrollElRef={scrollElRef as ListRef} 362 330 headerOffset={headerHeight} 363 331 enabled={isFocused} 364 332 /> 365 333 ) 366 334 : null} 367 335 {showListsTab 368 - ? ({onScroll, headerHeight, isFocused, scrollElRef}) => ( 336 + ? ({headerHeight, isFocused, scrollElRef}) => ( 369 337 <ProfileLists 370 338 ref={listsSectionRef} 371 339 did={profile.did} 372 - scrollElRef={ 373 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 374 - } 375 - onScroll={onScroll} 376 - scrollEventThrottle={1} 340 + scrollElRef={scrollElRef as ListRef} 377 341 headerOffset={headerHeight} 378 342 enabled={isFocused} 379 343 /> ··· 396 360 397 361 interface FeedSectionProps { 398 362 feed: FeedDescriptor 399 - onScroll: OnScrollHandler 400 363 headerHeight: number 401 364 isFocused: boolean 402 - isScrolledDown: boolean 403 - scrollElRef: React.MutableRefObject<FlatList<any> | null> 365 + scrollElRef: ListRef 404 366 ignoreFilterFor?: string 405 367 } 406 368 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( 407 369 function FeedSectionImpl( 408 - { 409 - feed, 410 - onScroll, 411 - headerHeight, 412 - isFocused, 413 - isScrolledDown, 414 - scrollElRef, 415 - ignoreFilterFor, 416 - }, 370 + {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor}, 417 371 ref, 418 372 ) { 419 373 const queryClient = useQueryClient() 420 374 const [hasNew, setHasNew] = React.useState(false) 375 + const [isScrolledDown, setIsScrolledDown] = React.useState(false) 421 376 422 377 const onScrollToTop = React.useCallback(() => { 423 378 scrollElRef.current?.scrollToOffset({ ··· 443 398 feed={feed} 444 399 scrollElRef={scrollElRef} 445 400 onHasNew={setHasNew} 446 - onScroll={onScroll} 447 - scrollEventThrottle={1} 401 + onScrolledDownChange={setIsScrolledDown} 448 402 renderEmptyState={renderPostsEmpty} 449 403 headerOffset={headerHeight} 450 404 renderEndOfFeed={ProfileEndOfFeed}
+15 -31
src/view/screens/ProfileFeed.tsx
··· 1 1 import React, {useMemo, useCallback} from 'react' 2 - import { 3 - Dimensions, 4 - StyleSheet, 5 - View, 6 - ActivityIndicator, 7 - FlatList, 8 - } from 'react-native' 2 + import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native' 9 3 import {NativeStackScreenProps} from '@react-navigation/native-stack' 10 4 import {useNavigation} from '@react-navigation/native' 11 5 import {useQueryClient} from '@tanstack/react-query' ··· 20 14 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' 21 15 import {Feed} from 'view/com/posts/Feed' 22 16 import {TextLink} from 'view/com/util/Link' 17 + import {ListRef} from 'view/com/util/List' 23 18 import {Button} from 'view/com/util/forms/Button' 24 19 import {Text} from 'view/com/util/text/Text' 25 20 import {RichText} from 'view/com/util/text/RichText' ··· 29 24 import * as Toast from 'view/com/util/Toast' 30 25 import {useSetTitle} from 'lib/hooks/useSetTitle' 31 26 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 32 - import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' 33 27 import {shareUrl} from 'lib/sharing' 34 28 import {toShareUrl} from 'lib/strings/url-helpers' 35 29 import {Haptics} from 'lib/haptics' 36 30 import {useAnalytics} from 'lib/analytics/analytics' 37 31 import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' 32 + import {useScrollHandlers} from '#/lib/ScrollContext' 33 + import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 38 34 import {makeCustomFeedLink} from 'lib/routes/links' 39 35 import {pluralize} from 'lib/strings/helpers' 40 36 import {CenteredView, ScrollView} from 'view/com/util/Views' ··· 46 42 import {Trans, msg} from '@lingui/macro' 47 43 import {useLingui} from '@lingui/react' 48 44 import {useModalControls} from '#/state/modals' 49 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 50 45 import { 51 46 useFeedSourceInfoQuery, 52 47 FeedSourceFeedInfo, ··· 403 398 isHeaderReady={true} 404 399 renderHeader={renderHeader} 405 400 onCurrentPageSelected={onCurrentPageSelected}> 406 - {({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) => 401 + {({headerHeight, scrollElRef, isFocused}) => 407 402 isPublicResponse?.isPublic ? ( 408 403 <FeedSection 409 404 ref={feedSectionRef} 410 405 feed={`feedgen|${feedInfo.uri}`} 411 - onScroll={onScroll} 412 406 headerHeight={headerHeight} 413 - isScrolledDown={isScrolledDown} 414 - scrollElRef={ 415 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 416 - } 407 + scrollElRef={scrollElRef as ListRef} 417 408 isFocused={isFocused} 418 409 /> 419 410 ) : ( ··· 422 413 </CenteredView> 423 414 ) 424 415 } 425 - {({onScroll, headerHeight, scrollElRef}) => ( 416 + {({headerHeight, scrollElRef}) => ( 426 417 <AboutSection 427 418 feedOwnerDid={feedInfo.creatorDid} 428 419 feedRkey={feedInfo.route.params.rkey} 429 420 feedInfo={feedInfo} 430 421 headerHeight={headerHeight} 431 - onScroll={onScroll} 432 422 scrollElRef={ 433 423 scrollElRef as React.MutableRefObject<ScrollView | null> 434 424 } ··· 497 487 498 488 interface FeedSectionProps { 499 489 feed: FeedDescriptor 500 - onScroll: OnScrollHandler 501 490 headerHeight: number 502 - isScrolledDown: boolean 503 - scrollElRef: React.MutableRefObject<FlatList<any> | null> 491 + scrollElRef: ListRef 504 492 isFocused: boolean 505 493 } 506 494 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( 507 - function FeedSectionImpl( 508 - {feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}, 509 - ref, 510 - ) { 495 + function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) { 511 496 const [hasNew, setHasNew] = React.useState(false) 497 + const [isScrolledDown, setIsScrolledDown] = React.useState(false) 512 498 const queryClient = useQueryClient() 513 499 514 500 const onScrollToTop = useCallback(() => { ··· 536 522 pollInterval={30e3} 537 523 scrollElRef={scrollElRef} 538 524 onHasNew={setHasNew} 539 - onScroll={onScroll} 540 - scrollEventThrottle={5} 525 + onScrolledDownChange={setIsScrolledDown} 541 526 renderEmptyState={renderPostsEmpty} 542 527 headerOffset={headerHeight} 543 528 /> ··· 558 543 feedRkey, 559 544 feedInfo, 560 545 headerHeight, 561 - onScroll, 562 546 scrollElRef, 563 547 isOwner, 564 548 }: { ··· 566 550 feedRkey: string 567 551 feedInfo: FeedSourceFeedInfo 568 552 headerHeight: number 569 - onScroll: OnScrollHandler 570 553 scrollElRef: React.MutableRefObject<ScrollView | null> 571 554 isOwner: boolean 572 555 }) { 573 556 const pal = usePalette('default') 574 557 const {_} = useLingui() 575 - const scrollHandler = useAnimatedScrollHandler(onScroll) 558 + const scrollHandlers = useScrollHandlers() 559 + const onScroll = useAnimatedScrollHandler(scrollHandlers) 576 560 const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) 577 561 const {hasSession} = useSession() 578 562 const {track} = useAnalytics() ··· 608 592 return ( 609 593 <ScrollView 610 594 ref={scrollElRef} 595 + onScroll={onScroll} 611 596 scrollEventThrottle={1} 612 597 contentContainerStyle={{ 613 598 paddingTop: headerHeight, 614 599 minHeight: Dimensions.get('window').height * 1.5, 615 - }} 616 - onScroll={scrollHandler}> 600 + }}> 617 601 <View 618 602 style={[ 619 603 {
+16 -47
src/view/screens/ProfileList.tsx
··· 1 1 import React, {useCallback, useMemo} from 'react' 2 - import { 3 - ActivityIndicator, 4 - FlatList, 5 - Pressable, 6 - StyleSheet, 7 - View, 8 - } from 'react-native' 2 + import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' 9 3 import {useFocusEffect} from '@react-navigation/native' 10 4 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 11 5 import {useNavigation} from '@react-navigation/native' ··· 22 16 import {RichText} from 'view/com/util/text/RichText' 23 17 import {Button} from 'view/com/util/forms/Button' 24 18 import {TextLink} from 'view/com/util/Link' 19 + import {ListRef} from 'view/com/util/List' 25 20 import * as Toast from 'view/com/util/Toast' 26 21 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 27 22 import {FAB} from 'view/com/util/fab/FAB' ··· 31 26 import {useSetTitle} from 'lib/hooks/useSetTitle' 32 27 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 33 28 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 34 - import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' 35 29 import {NavigationProp} from 'lib/routes/types' 36 30 import {toShareUrl} from 'lib/strings/url-helpers' 37 31 import {shareUrl} from 'lib/sharing' ··· 165 159 isHeaderReady={true} 166 160 renderHeader={renderHeader} 167 161 onCurrentPageSelected={onCurrentPageSelected}> 168 - {({ 169 - onScroll, 170 - headerHeight, 171 - isScrolledDown, 172 - scrollElRef, 173 - isFocused, 174 - }) => ( 162 + {({headerHeight, scrollElRef, isFocused}) => ( 175 163 <FeedSection 176 164 ref={feedSectionRef} 177 165 feed={`list|${uri}`} 178 - scrollElRef={ 179 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 180 - } 181 - onScroll={onScroll} 166 + scrollElRef={scrollElRef as ListRef} 182 167 headerHeight={headerHeight} 183 - isScrolledDown={isScrolledDown} 184 168 isFocused={isFocused} 185 169 /> 186 170 )} 187 - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( 171 + {({headerHeight, scrollElRef}) => ( 188 172 <AboutSection 189 173 ref={aboutSectionRef} 190 - scrollElRef={ 191 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 192 - } 174 + scrollElRef={scrollElRef as ListRef} 193 175 list={list} 194 176 onPressAddUser={onPressAddUser} 195 - onScroll={onScroll} 196 177 headerHeight={headerHeight} 197 - isScrolledDown={isScrolledDown} 198 178 /> 199 179 )} 200 180 </PagerWithHeader> ··· 221 201 items={SECTION_TITLES_MOD} 222 202 isHeaderReady={true} 223 203 renderHeader={renderHeader}> 224 - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( 204 + {({headerHeight, scrollElRef}) => ( 225 205 <AboutSection 226 206 list={list} 227 - scrollElRef={ 228 - scrollElRef as React.MutableRefObject<FlatList<any> | null> 229 - } 207 + scrollElRef={scrollElRef as ListRef} 230 208 onPressAddUser={onPressAddUser} 231 - onScroll={onScroll} 232 209 headerHeight={headerHeight} 233 - isScrolledDown={isScrolledDown} 234 210 /> 235 211 )} 236 212 </PagerWithHeader> ··· 615 591 616 592 interface FeedSectionProps { 617 593 feed: FeedDescriptor 618 - onScroll: OnScrollHandler 619 594 headerHeight: number 620 - isScrolledDown: boolean 621 - scrollElRef: React.MutableRefObject<FlatList<any> | null> 595 + scrollElRef: ListRef 622 596 isFocused: boolean 623 597 } 624 598 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( 625 - function FeedSectionImpl( 626 - {feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused}, 627 - ref, 628 - ) { 599 + function FeedSectionImpl({feed, scrollElRef, headerHeight, isFocused}, ref) { 629 600 const queryClient = useQueryClient() 630 601 const [hasNew, setHasNew] = React.useState(false) 602 + const [isScrolledDown, setIsScrolledDown] = React.useState(false) 631 603 632 604 const onScrollToTop = useCallback(() => { 633 605 scrollElRef.current?.scrollToOffset({ ··· 654 626 pollInterval={30e3} 655 627 scrollElRef={scrollElRef} 656 628 onHasNew={setHasNew} 657 - onScroll={onScroll} 658 - scrollEventThrottle={1} 629 + onScrolledDownChange={setIsScrolledDown} 659 630 renderEmptyState={renderPostsEmpty} 660 631 headerOffset={headerHeight} 661 632 /> ··· 674 645 interface AboutSectionProps { 675 646 list: AppBskyGraphDefs.ListView 676 647 onPressAddUser: () => void 677 - onScroll: OnScrollHandler 678 648 headerHeight: number 679 - isScrolledDown: boolean 680 - scrollElRef: React.MutableRefObject<FlatList<any> | null> 649 + scrollElRef: ListRef 681 650 } 682 651 const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( 683 652 function AboutSectionImpl( 684 - {list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef}, 653 + {list, onPressAddUser, headerHeight, scrollElRef}, 685 654 ref, 686 655 ) { 687 656 const pal = usePalette('default') 688 657 const {_} = useLingui() 689 658 const {isMobile} = useWebMediaQueries() 690 659 const {currentAccount} = useSession() 660 + const [isScrolledDown, setIsScrolledDown] = React.useState(false) 691 661 const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' 692 662 const isOwner = list.creator.did === currentAccount?.did 693 663 ··· 817 787 renderHeader={renderHeader} 818 788 renderEmptyState={renderEmptyState} 819 789 headerOffset={headerHeight} 820 - onScroll={onScroll} 821 - scrollEventThrottle={1} 790 + onScrolledDownChange={setIsScrolledDown} 822 791 /> 823 792 {isScrolledDown && ( 824 793 <LoadLatestBtn
+5 -4
src/view/screens/Search/Search.tsx
··· 8 8 Pressable, 9 9 Platform, 10 10 } from 'react-native' 11 - import {FlatList, ScrollView, CenteredView} from '#/view/com/util/Views' 11 + import {ScrollView, CenteredView} from '#/view/com/util/Views' 12 + import {List} from '#/view/com/util/List' 12 13 import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' 13 14 import {msg, Trans} from '@lingui/macro' 14 15 import {useLingui} from '@lingui/react' ··· 155 156 }, [currentAccount, setSuggestions, getSuggestedFollowsByActor]) 156 157 157 158 return suggestions.length ? ( 158 - <FlatList 159 + <List 159 160 data={suggestions} 160 161 renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />} 161 162 keyExtractor={item => item.did} ··· 243 244 {isFetched ? ( 244 245 <> 245 246 {posts.length ? ( 246 - <FlatList 247 + <List 247 248 data={items} 248 249 renderItem={({item}) => { 249 250 if (item.type === 'post') { ··· 284 285 return isFetched && results ? ( 285 286 <> 286 287 {results.length ? ( 287 - <FlatList 288 + <List 288 289 data={results} 289 290 renderItem={({item}) => ( 290 291 <ProfileCardWithFollowBtn profile={item} noBg />