diff options
Diffstat (limited to 'src/view')
25 files changed, 329 insertions, 313 deletions
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 31ebc75a1..9c92a0dd5 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -7,13 +7,15 @@ import {useNavigation} from '@react-navigation/native' import {useAnalytics} from 'lib/analytics/analytics' import {useQueryClient} from '@tanstack/react-query' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' -import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {MainScrollProvider} from '../util/MainScrollProvider' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useSetMinimalShellMode} from '#/state/shell' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {ComposeIcon2} from 'lib/icons' import {colors, s} from 'lib/styles' -import {FlatList, View, useWindowDimensions} from 'react-native' +import {View, useWindowDimensions} from 'react-native' +import {ListMethods} from '../util/List' import {Feed} from '../posts/Feed' import {TextLink} from '../util/Link' import {FAB} from '../util/fab/FAB' @@ -51,10 +53,11 @@ export function FeedPage({ const {isDesktop} = useWebMediaQueries() const queryClient = useQueryClient() const {openComposer} = useComposerControls() - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() + const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics() const headerOffset = useHeaderOffset() - const scrollElRef = React.useRef<FlatList>(null) + const scrollElRef = React.useRef<ListMethods>(null) const [hasNew, setHasNew] = React.useState(false) const scrollToTop = React.useCallback(() => { @@ -62,8 +65,8 @@ export function FeedPage({ animated: isNative, offset: -headerOffset, }) - resetMainScroll() - }, [headerOffset, resetMainScroll]) + setMinimalShellMode(false) + }, [headerOffset, setMinimalShellMode]) const onSoftReset = React.useCallback(() => { const isScreenFocused = @@ -164,21 +167,22 @@ export function FeedPage({ return ( <View testID={testID} style={s.h100pct}> - <Feed - testID={testID ? `${testID}-feed` : undefined} - enabled={isPageFocused} - feed={feed} - feedParams={feedParams} - pollInterval={POLL_FREQ} - scrollElRef={scrollElRef} - onScroll={onMainScroll} - onHasNew={setHasNew} - scrollEventThrottle={1} - renderEmptyState={renderEmptyState} - renderEndOfFeed={renderEndOfFeed} - ListHeaderComponent={ListHeaderComponent} - headerOffset={headerOffset} - /> + <MainScrollProvider> + <Feed + testID={testID ? `${testID}-feed` : undefined} + enabled={isPageFocused} + feed={feed} + feedParams={feedParams} + pollInterval={POLL_FREQ} + scrollElRef={scrollElRef} + onScrolledDownChange={setIsScrolledDown} + onHasNew={setHasNew} + renderEmptyState={renderEmptyState} + renderEndOfFeed={renderEndOfFeed} + ListHeaderComponent={ListHeaderComponent} + headerOffset={headerOffset} + /> + </MainScrollProvider> {(isScrolledDown || hasNew) && ( <LoadLatestBtn onPress={onPressLoadLatest} diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 43b170ced..ff6505501 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -1,4 +1,4 @@ -import React, {MutableRefObject} from 'react' +import React from 'react' import { Dimensions, RefreshControl, @@ -8,18 +8,16 @@ import { ViewStyle, } from 'react-native' import {useQueryClient} from '@tanstack/react-query' -import {FlatList} from '../util/Views' +import {List, ListRef} from '../util/List' import {FeedSourceCardLoaded} from './FeedSourceCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens' -import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll' import {logger} from '#/logger' import {Trans} from '@lingui/macro' import {cleanError} from '#/lib/strings/errors' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useTheme} from '#/lib/ThemeContext' import {usePreferencesQuery} from '#/state/queries/preferences' import {hydrateFeedGenerator} from '#/state/queries/feed' @@ -37,9 +35,7 @@ interface SectionRef { interface ProfileFeedgensProps { did: string - scrollElRef: MutableRefObject<FlatList<any> | null> - onScroll?: OnScrollHandler - scrollEventThrottle?: number + scrollElRef: ListRef headerOffset: number enabled?: boolean style?: StyleProp<ViewStyle> @@ -50,16 +46,7 @@ export const ProfileFeedgens = React.forwardRef< SectionRef, ProfileFeedgensProps >(function ProfileFeedgensImpl( - { - did, - scrollElRef, - onScroll, - scrollEventThrottle, - headerOffset, - enabled, - style, - testID, - }, + {did, scrollElRef, headerOffset, enabled, style, testID}, ref, ) { const pal = usePalette('default') @@ -185,10 +172,9 @@ export const ProfileFeedgens = React.forwardRef< [error, refetch, onPressRetryLoadMore, pal, preferences], ) - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) return ( <View testID={testID} style={style}> - <FlatList + <List testID={testID ? `${testID}-flatlist` : undefined} ref={scrollElRef} data={items} @@ -207,8 +193,6 @@ export const ProfileFeedgens = React.forwardRef< minHeight: Dimensions.get('window').height * 1.5, }} style={{paddingTop: headerOffset}} - onScroll={onScroll != null ? scrollHandler : undefined} - scrollEventThrottle={scrollEventThrottle} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} removeClippedSubviews={true} contentOffset={{x: 0, y: headerOffset * -1}} diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx index 160b4b3e5..a31ca4793 100644 --- a/src/view/com/lists/ListMembers.tsx +++ b/src/view/com/lists/ListMembers.tsx @@ -1,4 +1,4 @@ -import React, {MutableRefObject} from 'react' +import React from 'react' import { ActivityIndicator, Dimensions, @@ -8,7 +8,7 @@ import { ViewStyle, } from 'react-native' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' -import {FlatList} from '../util/Views' +import {List, ListRef} from '../util/List' import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ErrorMessage} from '../util/error/ErrorMessage' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' @@ -18,10 +18,8 @@ import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useListMembersQuery} from '#/state/queries/list-members' -import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {logger} from '#/logger' import {useModalControls} from '#/state/modals' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useSession} from '#/state/session' import {cleanError} from '#/lib/strings/errors' @@ -34,24 +32,22 @@ export function ListMembers({ list, style, scrollElRef, - onScroll, + onScrolledDownChange, onPressTryAgain, renderHeader, renderEmptyState, testID, - scrollEventThrottle, headerOffset = 0, desktopFixedHeightOffset, }: { list: string style?: StyleProp<ViewStyle> - scrollElRef?: MutableRefObject<FlatList<any> | null> - onScroll: OnScrollHandler + scrollElRef?: ListRef + onScrolledDownChange: (isScrolledDown: boolean) => void onPressTryAgain?: () => void renderHeader: () => JSX.Element renderEmptyState: () => JSX.Element testID?: string - scrollEventThrottle?: number headerOffset?: number desktopFixedHeightOffset?: number }) { @@ -209,10 +205,9 @@ export function ListMembers({ [isFetching], ) - const scrollHandler = useAnimatedScrollHandler(onScroll) return ( <View testID={testID} style={style}> - <FlatList + <List testID={testID ? `${testID}-flatlist` : undefined} ref={scrollElRef} data={items} @@ -233,10 +228,9 @@ export function ListMembers({ minHeight: Dimensions.get('window').height * 1.5, }} style={{paddingTop: headerOffset}} - onScroll={scrollHandler} + onScrolledDownChange={onScrolledDownChange} onEndReached={onEndReached} onEndReachedThreshold={0.6} - scrollEventThrottle={scrollEventThrottle} removeClippedSubviews={true} contentOffset={{x: 0, y: headerOffset * -1}} // @ts-ignore our .web version only -prf diff --git a/src/view/com/lists/MyLists.tsx b/src/view/com/lists/MyLists.tsx index b46d34ba5..586ad234e 100644 --- a/src/view/com/lists/MyLists.tsx +++ b/src/view/com/lists/MyLists.tsx @@ -15,7 +15,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {Text} from '../util/text/Text' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' -import {FlatList} from '../util/Views' +import {List} from '../util/List' import {s} from 'lib/styles' import {logger} from '#/logger' import {Trans} from '@lingui/macro' @@ -119,7 +119,7 @@ export function MyLists({ [error, onRefresh, renderItem, pal], ) - const FlatListCom = inline ? RNFlatList : FlatList + const FlatListCom = inline ? RNFlatList : List return ( <View testID={testID} style={style}> {items.length > 0 && ( diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index 1bd9d188f..e3d9bd0b4 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -1,4 +1,4 @@ -import React, {MutableRefObject} from 'react' +import React from 'react' import { Dimensions, RefreshControl, @@ -8,7 +8,7 @@ import { ViewStyle, } from 'react-native' import {useQueryClient} from '@tanstack/react-query' -import {FlatList} from '../util/Views' +import {List, ListRef} from '../util/List' import {ListCard} from './ListCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' @@ -16,11 +16,9 @@ import {Text} from '../util/text/Text' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists' -import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll' import {logger} from '#/logger' import {Trans} from '@lingui/macro' import {cleanError} from '#/lib/strings/errors' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useTheme} from '#/lib/ThemeContext' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {isNative} from '#/platform/detection' @@ -36,9 +34,7 @@ interface SectionRef { interface ProfileListsProps { did: string - scrollElRef: MutableRefObject<FlatList<any> | null> - onScroll?: OnScrollHandler - scrollEventThrottle?: number + scrollElRef: ListRef headerOffset: number enabled?: boolean style?: StyleProp<ViewStyle> @@ -47,16 +43,7 @@ interface ProfileListsProps { export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( function ProfileListsImpl( - { - did, - scrollElRef, - onScroll, - scrollEventThrottle, - headerOffset, - enabled, - style, - testID, - }, + {did, scrollElRef, headerOffset, enabled, style, testID}, ref, ) { const pal = usePalette('default') @@ -187,10 +174,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( [error, refetch, onPressRetryLoadMore, pal], ) - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) return ( <View testID={testID} style={style}> - <FlatList + <List testID={testID ? `${testID}-flatlist` : undefined} ref={scrollElRef} data={items} @@ -209,8 +195,6 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( minHeight: Dimensions.get('window').height * 1.5, }} style={{paddingTop: headerOffset}} - onScroll={onScroll != null ? scrollHandler : undefined} - scrollEventThrottle={scrollEventThrottle} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} removeClippedSubviews={true} contentOffset={{x: 0, y: headerOffset * -1}} diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 260c9bbd5..52d534c4f 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -1,13 +1,11 @@ -import React, {MutableRefObject} from 'react' -import {CenteredView, FlatList} from '../util/Views' +import React from 'react' +import {CenteredView} from '../util/Views' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {FeedItem} from './FeedItem' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ErrorMessage} from '../util/error/ErrorMessage' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {EmptyState} from '../util/EmptyState' -import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' @@ -15,6 +13,7 @@ import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' import {logger} from '#/logger' import {cleanError} from '#/lib/strings/errors' import {useModerationOpts} from '#/state/queries/preferences' +import {List, ListRef} from '../util/List' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} @@ -23,12 +22,12 @@ const LOADING_ITEM = {_reactKey: '__loading__'} export function Feed({ scrollElRef, onPressTryAgain, - onScroll, + onScrolledDownChange, ListHeaderComponent, }: { - scrollElRef?: MutableRefObject<FlatList<any> | null> + scrollElRef?: ListRef onPressTryAgain?: () => void - onScroll?: OnScrollHandler + onScrolledDownChange: (isScrolledDown: boolean) => void ListHeaderComponent?: () => JSX.Element }) { const pal = usePalette('default') @@ -135,7 +134,6 @@ export function Feed({ [isFetchingNextPage], ) - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) return ( <View style={s.hContentRegion}> {error && ( @@ -146,7 +144,7 @@ export function Feed({ /> </CenteredView> )} - <FlatList + <List testID="notifsFeed" ref={scrollElRef} data={items} @@ -164,8 +162,7 @@ export function Feed({ } onEndReached={onEndReached} onEndReachedThreshold={0.6} - onScroll={scrollHandler} - scrollEventThrottle={1} + onScrolledDownChange={onScrolledDownChange} contentContainerStyle={s.contentContainer} // @ts-ignore our .web version only -prf desktopFixedHeight diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index cda2d1306..158940d67 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { LayoutChangeEvent, - FlatList, ScrollView, StyleSheet, View, @@ -20,17 +19,14 @@ import Animated, { import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {TabBar} from './TabBar' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' - -const SCROLLED_DOWN_LIMIT = 200 +import {ListMethods} from '../util/List' +import {ScrollProvider} from '#/lib/ScrollContext' export interface PagerWithHeaderChildParams { headerHeight: number isFocused: boolean - onScroll: OnScrollHandler - isScrolledDown: boolean - scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null> + scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null> } export interface PagerWithHeaderProps { @@ -62,7 +58,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( const [currentPage, setCurrentPage] = React.useState(0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) - const [isScrolledDown, setIsScrolledDown] = React.useState(false) const scrollY = useSharedValue(0) const headerHeight = headerOnlyHeight + tabBarHeight @@ -155,15 +150,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( if (!throttleTimeout.current) { throttleTimeout.current = setTimeout(() => { throttleTimeout.current = null - runOnUI(adjustScrollForOtherPages)() - - const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT - if (isScrolledDown !== nextIsScrolledDown) { - React.startTransition(() => { - setIsScrolledDown(nextIsScrolledDown) - }) - } }, 80 /* Sync often enough you're unlikely to catch it unsynced */) } }) @@ -211,7 +198,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( index={i} isReady={isReady} isFocused={i === currentPage} - isScrolledDown={isScrolledDown} onScrollWorklet={i === currentPage ? onScrollWorklet : noop} registerRef={registerRef} renderTab={child} @@ -293,7 +279,6 @@ function PagerItem({ index, isReady, isFocused, - isScrolledDown, onScrollWorklet, renderTab, registerRef, @@ -302,7 +287,6 @@ function PagerItem({ index: number isFocused: boolean isReady: boolean - isScrolledDown: boolean registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void onScrollWorklet: (e: NativeScrollEvent) => void renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null @@ -316,24 +300,21 @@ function PagerItem({ } }, [scrollElRef, registerRef, index]) - const scrollHandler = React.useMemo( - () => ({onScroll: onScrollWorklet}), - [onScrollWorklet], - ) - if (!isReady || renderTab == null) { return null } - return renderTab({ - headerHeight, - isFocused, - isScrolledDown, - onScroll: scrollHandler, - scrollElRef: scrollElRef as React.MutableRefObject< - FlatList<any> | ScrollView | null - >, - }) + return ( + <ScrollProvider onScroll={onScrollWorklet}> + {renderTab({ + headerHeight, + isFocused, + scrollElRef: scrollElRef as React.MutableRefObject< + ListMethods | ScrollView | null + >, + })} + </ScrollProvider> + ) } const styles = StyleSheet.create({ diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx index 60afe1f9c..245ba59e8 100644 --- a/src/view/com/post-thread/PostLikedBy.tsx +++ b/src/view/com/post-thread/PostLikedBy.tsx @@ -1,7 +1,8 @@ import React, {useCallback, useMemo, useState} from 'react' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' -import {CenteredView, FlatList} from '../util/Views' +import {CenteredView} from '../util/Views' +import {List} from '../util/List' import {ErrorMessage} from '../util/error/ErrorMessage' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {usePalette} from 'lib/hooks/usePalette' @@ -84,7 +85,7 @@ export function PostLikedBy({uri}: {uri: string}) { // loaded // = return ( - <FlatList + <List data={likes} keyExtractor={item => item.actor.did} refreshControl={ diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index 1162fec40..5cc006388 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -1,7 +1,8 @@ import React, {useMemo, useCallback, useState} from 'react' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {AppBskyActorDefs as ActorDefs} from '@atproto/api' -import {CenteredView, FlatList} from '../util/Views' +import {CenteredView} from '../util/Views' +import {List} from '../util/List' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' @@ -85,7 +86,7 @@ export function PostRepostedBy({uri}: {uri: string}) { // loaded // = return ( - <FlatList + <List data={repostedBy} keyExtractor={item => item.did} refreshControl={ diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 051bc7849..f27da331f 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -8,7 +8,8 @@ import { View, } from 'react-native' import {AppBskyFeedDefs} from '@atproto/api' -import {CenteredView, FlatList} from '../util/Views' +import {CenteredView} from '../util/Views' +import {List, ListMethods} from '../util/List' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -140,7 +141,7 @@ function PostThreadLoaded({ const {_} = useLingui() const pal = usePalette('default') const {isTablet, isDesktop} = useWebMediaQueries() - const ref = useRef<FlatList>(null) + const ref = useRef<ListMethods>(null) const highlightedPostRef = useRef<View | null>(null) const needsScrollAdjustment = useRef<boolean>( !isNative || // web always uses scroll adjustment @@ -335,7 +336,7 @@ function PostThreadLoaded({ ) return ( - <FlatList + <List ref={ref} data={posts} initialNumToRender={!isNative ? posts.length : undefined} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 24a7f5b42..9194bb163 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -1,4 +1,4 @@ -import React, {memo, MutableRefObject} from 'react' +import React, {memo} from 'react' import { ActivityIndicator, AppState, @@ -10,15 +10,13 @@ import { ViewStyle, } from 'react-native' import {useQueryClient} from '@tanstack/react-query' -import {FlatList} from '../util/Views' +import {List, ListRef} from '../util/List' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {FeedErrorMessage} from './FeedErrorMessage' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useTheme} from 'lib/ThemeContext' import {logger} from '#/logger' import { @@ -45,9 +43,8 @@ let Feed = ({ enabled, pollInterval, scrollElRef, - onScroll, + onScrolledDownChange, onHasNew, - scrollEventThrottle, renderEmptyState, renderEndOfFeed, testID, @@ -62,10 +59,9 @@ let Feed = ({ style?: StyleProp<ViewStyle> enabled?: boolean pollInterval?: number - scrollElRef?: MutableRefObject<FlatList<any> | null> + scrollElRef?: ListRef onHasNew?: (v: boolean) => void - onScroll?: OnScrollHandler - scrollEventThrottle?: number + onScrolledDownChange?: (isScrolledDown: boolean) => void renderEmptyState: () => JSX.Element renderEndOfFeed?: () => JSX.Element testID?: string @@ -270,10 +266,9 @@ let Feed = ({ ) }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) return ( <View testID={testID} style={style}> - <FlatList + <List testID={testID ? `${testID}-flatlist` : undefined} ref={scrollElRef} data={feedItems} @@ -294,8 +289,7 @@ let Feed = ({ minHeight: Dimensions.get('window').height * 1.5, }} style={{paddingTop: headerOffset}} - onScroll={onScroll != null ? scrollHandler : undefined} - scrollEventThrottle={scrollEventThrottle} + onScrolledDownChange={onScrolledDownChange} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} onEndReachedThreshold={2} // number of posts left to trigger load more diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index d94f5103e..077dabe53 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -1,7 +1,8 @@ import React from 'react' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {AppBskyActorDefs as ActorDefs} from '@atproto/api' -import {CenteredView, FlatList} from '../util/Views' +import {CenteredView} from '../util/Views' +import {List} from '../util/List' import {ErrorMessage} from '../util/error/ErrorMessage' import {ProfileCardWithFollowBtn} from './ProfileCard' import {usePalette} from 'lib/hooks/usePalette' @@ -86,7 +87,7 @@ export function ProfileFollowers({name}: {name: string}) { // loaded // = return ( - <FlatList + <List data={followers} keyExtractor={item => item.did} refreshControl={ diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index 890c13eb2..5265ee07e 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -1,7 +1,8 @@ import React from 'react' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {AppBskyActorDefs as ActorDefs} from '@atproto/api' -import {CenteredView, FlatList} from '../util/Views' +import {CenteredView} from '../util/Views' +import {List} from '../util/List' import {ErrorMessage} from '../util/error/ErrorMessage' import {ProfileCardWithFollowBtn} from './ProfileCard' import {usePalette} from 'lib/hooks/usePalette' @@ -86,7 +87,7 @@ export function ProfileFollows({name}: {name: string}) { // loaded // = return ( - <FlatList + <List data={follows} keyExtractor={item => item.did} refreshControl={ diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx new file mode 100644 index 000000000..5947fe87a --- /dev/null +++ b/src/view/com/util/List.tsx @@ -0,0 +1,64 @@ +import React, {memo, startTransition} from 'react' +import {FlatListProps} from 'react-native' +import {FlatList_INTERNAL} from './Views' +import {useScrollHandlers} from '#/lib/ScrollContext' +import {runOnJS, useSharedValue} from 'react-native-reanimated' +import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' + +export type ListMethods = FlatList_INTERNAL +export type ListProps<ItemT> = Omit< + FlatListProps<ItemT>, + 'onScroll' // Use ScrollContext instead. +> & { + onScrolledDownChange?: (isScrolledDown: boolean) => void +} +export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> + +const SCROLLED_DOWN_LIMIT = 200 + +function ListImpl<ItemT>( + {onScrolledDownChange, ...props}: ListProps<ItemT>, + ref: React.Ref<ListMethods>, +) { + const isScrolledDown = useSharedValue(false) + const contextScrollHandlers = useScrollHandlers() + + function handleScrolledDownChange(didScrollDown: boolean) { + startTransition(() => { + onScrolledDownChange?.(didScrollDown) + }) + } + + const scrollHandler = useAnimatedScrollHandler({ + onBeginDrag(e, ctx) { + contextScrollHandlers.onBeginDrag?.(e, ctx) + }, + onEndDrag(e, ctx) { + contextScrollHandlers.onEndDrag?.(e, ctx) + }, + onScroll(e, ctx) { + contextScrollHandlers.onScroll?.(e, ctx) + + const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT + if (isScrolledDown.value !== didScrollDown) { + isScrolledDown.value = didScrollDown + if (onScrolledDownChange != null) { + runOnJS(handleScrolledDownChange)(didScrollDown) + } + } + }, + }) + + return ( + <FlatList_INTERNAL + {...props} + onScroll={scrollHandler} + scrollEventThrottle={1} + ref={ref} + /> + ) +} + +export const List = memo(React.forwardRef(ListImpl)) as <ItemT>( + props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>}, +) => React.ReactElement diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx new file mode 100644 index 000000000..31a4ef0c8 --- /dev/null +++ b/src/view/com/util/MainScrollProvider.tsx @@ -0,0 +1,97 @@ +import React, {useCallback} from 'react' +import {ScrollProvider} from '#/lib/ScrollContext' +import {NativeScrollEvent} from 'react-native' +import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' +import {useShellLayout} from '#/state/shell/shell-layout' +import {isWeb} from 'platform/detection' +import {useSharedValue, interpolate} from 'react-native-reanimated' + +function clamp(num: number, min: number, max: number) { + 'worklet' + return Math.min(Math.max(num, min), max) +} + +export function MainScrollProvider({children}: {children: React.ReactNode}) { + const {headerHeight} = useShellLayout() + const mode = useMinimalShellMode() + const setMode = useSetMinimalShellMode() + const startDragOffset = useSharedValue<number | null>(null) + const startMode = useSharedValue<number | null>(null) + + const onBeginDrag = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + startDragOffset.value = e.contentOffset.y + startMode.value = mode.value + }, + [mode, startDragOffset, startMode], + ) + + const onEndDrag = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + startDragOffset.value = null + startMode.value = null + if (e.contentOffset.y < headerHeight.value / 2) { + // If we're close to the top, show the shell. + setMode(false) + } else { + // Snap to whichever state is the closest. + setMode(Math.round(mode.value) === 1) + } + }, + [startDragOffset, startMode, setMode, mode, headerHeight], + ) + + const onScroll = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + if (startDragOffset.value === null || startMode.value === null) { + if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) { + // If we're close enough to the top, always show the shell. + // Even if we're not dragging. + setMode(false) + return + } + if (isWeb) { + // On the web, there is no concept of "starting" the drag. + // When we get the first scroll event, we consider that the start. + startDragOffset.value = e.contentOffset.y + startMode.value = mode.value + } + return + } + + // The "mode" value is always between 0 and 1. + // Figure out how much to move it based on the current dragged distance. + const dy = e.contentOffset.y - startDragOffset.value + const dProgress = interpolate( + dy, + [-headerHeight.value, headerHeight.value], + [-1, 1], + ) + const newValue = clamp(startMode.value + dProgress, 0, 1) + if (newValue !== mode.value) { + // Manually adjust the value. This won't be (and shouldn't be) animated. + mode.value = newValue + } + if (isWeb) { + // On the web, there is no concept of "starting" the drag, + // so we don't have any specific anchor point to calculate the distance. + // Instead, update it continuosly along the way and diff with the last event. + startDragOffset.value = e.contentOffset.y + startMode.value = mode.value + } + }, + [headerHeight, mode, setMode, startDragOffset, startMode], + ) + + return ( + <ScrollProvider + onBeginDrag={onBeginDrag} + onEndDrag={onEndDrag} + onScroll={onScroll}> + {children} + </ScrollProvider> + ) +} diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 935d93033..ee993c564 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -1,13 +1,14 @@ import React, {useEffect, useState} from 'react' import { + NativeSyntheticEvent, + NativeScrollEvent, Pressable, RefreshControl, StyleSheet, View, ScrollView, } from 'react-native' -import {FlatList} from './Views' -import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {FlatList_INTERNAL} from './Views' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {Text} from './text/Text' import {usePalette} from 'lib/hooks/usePalette' @@ -38,7 +39,7 @@ export const ViewSelector = React.forwardRef< | null | undefined onSelectView?: (viewIndex: number) => void - onScroll?: OnScrollCb + onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void onRefresh?: () => void onEndReached?: (info: {distanceFromEnd: number}) => void } @@ -59,7 +60,7 @@ export const ViewSelector = React.forwardRef< ) { const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState<number>(0) - const flatListRef = React.useRef<FlatList>(null) + const flatListRef = React.useRef<FlatList_INTERNAL>(null) // events // = @@ -110,7 +111,7 @@ export const ViewSelector = React.forwardRef< [items], ) return ( - <FlatList + <FlatList_INTERNAL ref={flatListRef} data={data} keyExtractor={keyExtractor} diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts index 91df1d6bc..6a90cc229 100644 --- a/src/view/com/util/Views.d.ts +++ b/src/view/com/util/Views.d.ts @@ -1,6 +1,6 @@ import React from 'react' import {ViewProps} from 'react-native' -export {FlatList, ScrollView} from 'react-native' +export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native' export function CenteredView({ style, sideBorders, diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx index 8a93ce511..7d6120583 100644 --- a/src/view/com/util/Views.jsx +++ b/src/view/com/util/Views.jsx @@ -2,7 +2,7 @@ import React from 'react' import {View} from 'react-native' import Animated from 'react-native-reanimated' -export const FlatList = Animated.FlatList +export const FlatList_INTERNAL = Animated.FlatList export const ScrollView = Animated.ScrollView export function CenteredView(props) { return <View {...props} /> diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 5a4f266fd..db3b9de0d 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -49,7 +49,7 @@ export function CenteredView({ return <View style={style} {...props} /> } -export const FlatList = React.forwardRef(function FlatListImpl<ItemT>( +export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>( { contentContainerStyle, style, diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index f319fbc39..d2d0aafb8 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -19,7 +19,7 @@ import { import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import debounce from 'lodash.debounce' import {Text} from 'view/com/util/text/Text' -import {FlatList} from 'view/com/util/Views' +import {List} from 'view/com/util/List' import {useFocusEffect} from '@react-navigation/native' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {Trans, msg} from '@lingui/macro' @@ -481,7 +481,7 @@ export function FeedsScreen(_props: Props) { {preferences ? <View /> : <ActivityIndicator />} - <FlatList + <List style={[!isTabletOrDesktop && s.flex1, styles.list]} data={items} keyExtractor={item => item.key} diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index fceaa60c2..e28a67e37 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {FlatList, View} from 'react-native' +import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import { @@ -9,8 +9,9 @@ import { import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/notifications/Feed' import {TextLink} from 'view/com/util/Link' +import {ListMethods} from 'view/com/util/List' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {MainScrollProvider} from '../com/util/MainScrollProvider' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s, colors} from 'lib/styles' @@ -35,8 +36,8 @@ type Props = NativeStackScreenProps< export function NotificationsScreen({}: Props) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() - const scrollElRef = React.useRef<FlatList>(null) + const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const scrollElRef = React.useRef<ListMethods>(null) const checkLatestRef = React.useRef<() => void | null>() const {screen} = useAnalytics() const pal = usePalette('default') @@ -50,8 +51,8 @@ export function NotificationsScreen({}: Props) { // = const scrollToTop = React.useCallback(() => { scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0}) - resetMainScroll() - }, [scrollElRef, resetMainScroll]) + setMinimalShellMode(false) + }, [scrollElRef, setMinimalShellMode]) const onPressLoadLatest = React.useCallback(() => { scrollToTop() @@ -130,11 +131,13 @@ export function NotificationsScreen({}: Props) { return ( <View testID="notificationsScreen" style={s.hContentRegion}> <ViewHeader title={_(msg`Notifications`)} canGoBack={false} /> - <Feed - onScroll={onMainScroll} - scrollElRef={scrollElRef} - ListHeaderComponent={ListHeaderComponent} - /> + <MainScrollProvider> + <Feed + onScrolledDownChange={setIsScrolledDown} + scrollElRef={scrollElRef} + ListHeaderComponent={ListHeaderComponent} + /> + </MainScrollProvider> {(isScrolledDown || hasNew) && ( <LoadLatestBtn onPress={onPressLoadLatest} diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index b7dac8c6d..3f2dd773e 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -5,7 +5,8 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {CenteredView, FlatList} from '../com/util/Views' +import {CenteredView} from '../com/util/Views' +import {ListRef} from '../com/util/List' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' import {Feed} from 'view/com/posts/Feed' import {ProfileLists} from '../com/lists/ProfileLists' @@ -20,7 +21,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {ComposeIcon2} from 'lib/icons' import {useSetTitle} from 'lib/hooks/useSetTitle' import {combinedDisplayName} from 'lib/strings/display-names' -import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll' import {FeedDescriptor} from '#/state/queries/post-feed' import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useProfileQuery} from '#/state/queries/profile' @@ -277,103 +277,67 @@ function ProfileScreenLoaded({ onPageSelected={onPageSelected} onCurrentPageSelected={onCurrentPageSelected} renderHeader={renderHeader}> - {({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => ( + {({headerHeight, isFocused, scrollElRef}) => ( <FeedSection ref={postsSectionRef} feed={`author|${profile.did}|posts_and_author_threads`} - onScroll={onScroll} headerHeight={headerHeight} isFocused={isFocused} - isScrolledDown={isScrolledDown} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} /> )} {showRepliesTab - ? ({ - onScroll, - headerHeight, - isFocused, - isScrolledDown, - scrollElRef, - }) => ( + ? ({headerHeight, isFocused, scrollElRef}) => ( <FeedSection ref={repliesSectionRef} feed={`author|${profile.did}|posts_with_replies`} - onScroll={onScroll} headerHeight={headerHeight} isFocused={isFocused} - isScrolledDown={isScrolledDown} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} /> ) : null} - {({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => ( + {({headerHeight, isFocused, scrollElRef}) => ( <FeedSection ref={mediaSectionRef} feed={`author|${profile.did}|posts_with_media`} - onScroll={onScroll} headerHeight={headerHeight} isFocused={isFocused} - isScrolledDown={isScrolledDown} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} /> )} {showLikesTab - ? ({ - onScroll, - headerHeight, - isFocused, - isScrolledDown, - scrollElRef, - }) => ( + ? ({headerHeight, isFocused, scrollElRef}) => ( <FeedSection ref={likesSectionRef} feed={`likes|${profile.did}`} - onScroll={onScroll} headerHeight={headerHeight} isFocused={isFocused} - isScrolledDown={isScrolledDown} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} /> ) : null} {showFeedsTab - ? ({onScroll, headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, isFocused, scrollElRef}) => ( <ProfileFeedgens ref={feedsSectionRef} did={profile.did} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } - onScroll={onScroll} - scrollEventThrottle={1} + scrollElRef={scrollElRef as ListRef} headerOffset={headerHeight} enabled={isFocused} /> ) : null} {showListsTab - ? ({onScroll, headerHeight, isFocused, scrollElRef}) => ( + ? ({headerHeight, isFocused, scrollElRef}) => ( <ProfileLists ref={listsSectionRef} did={profile.did} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } - onScroll={onScroll} - scrollEventThrottle={1} + scrollElRef={scrollElRef as ListRef} headerOffset={headerHeight} enabled={isFocused} /> @@ -396,28 +360,19 @@ function ProfileScreenLoaded({ interface FeedSectionProps { feed: FeedDescriptor - onScroll: OnScrollHandler headerHeight: number isFocused: boolean - isScrolledDown: boolean - scrollElRef: React.MutableRefObject<FlatList<any> | null> + scrollElRef: ListRef ignoreFilterFor?: string } const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( function FeedSectionImpl( - { - feed, - onScroll, - headerHeight, - isFocused, - isScrolledDown, - scrollElRef, - ignoreFilterFor, - }, + {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor}, ref, ) { const queryClient = useQueryClient() const [hasNew, setHasNew] = React.useState(false) + const [isScrolledDown, setIsScrolledDown] = React.useState(false) const onScrollToTop = React.useCallback(() => { scrollElRef.current?.scrollToOffset({ @@ -443,8 +398,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( feed={feed} scrollElRef={scrollElRef} onHasNew={setHasNew} - onScroll={onScroll} - scrollEventThrottle={1} + onScrolledDownChange={setIsScrolledDown} renderEmptyState={renderPostsEmpty} headerOffset={headerHeight} renderEndOfFeed={ProfileEndOfFeed} diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 620b1814a..ea92ebab1 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -1,11 +1,5 @@ import React, {useMemo, useCallback} from 'react' -import { - Dimensions, - StyleSheet, - View, - ActivityIndicator, - FlatList, -} from 'react-native' +import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -20,6 +14,7 @@ import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {Feed} from 'view/com/posts/Feed' import {TextLink} from 'view/com/util/Link' +import {ListRef} from 'view/com/util/List' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import {RichText} from 'view/com/util/text/RichText' @@ -29,12 +24,13 @@ import {EmptyState} from 'view/com/util/EmptyState' import * as Toast from 'view/com/util/Toast' import {useSetTitle} from 'lib/hooks/useSetTitle' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' -import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' import {useAnalytics} from 'lib/analytics/analytics' import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' +import {useScrollHandlers} from '#/lib/ScrollContext' +import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {makeCustomFeedLink} from 'lib/routes/links' import {pluralize} from 'lib/strings/helpers' import {CenteredView, ScrollView} from 'view/com/util/Views' @@ -46,7 +42,6 @@ import {logger} from '#/logger' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import { useFeedSourceInfoQuery, FeedSourceFeedInfo, @@ -403,17 +398,13 @@ export function ProfileFeedScreenInner({ isHeaderReady={true} renderHeader={renderHeader} onCurrentPageSelected={onCurrentPageSelected}> - {({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) => + {({headerHeight, scrollElRef, isFocused}) => isPublicResponse?.isPublic ? ( <FeedSection ref={feedSectionRef} feed={`feedgen|${feedInfo.uri}`} - onScroll={onScroll} headerHeight={headerHeight} - isScrolledDown={isScrolledDown} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} isFocused={isFocused} /> ) : ( @@ -422,13 +413,12 @@ export function ProfileFeedScreenInner({ </CenteredView> ) } - {({onScroll, headerHeight, scrollElRef}) => ( + {({headerHeight, scrollElRef}) => ( <AboutSection feedOwnerDid={feedInfo.creatorDid} feedRkey={feedInfo.route.params.rkey} feedInfo={feedInfo} headerHeight={headerHeight} - onScroll={onScroll} scrollElRef={ scrollElRef as React.MutableRefObject<ScrollView | null> } @@ -497,18 +487,14 @@ function NonPublicFeedMessage({rawError}: {rawError?: Error}) { interface FeedSectionProps { feed: FeedDescriptor - onScroll: OnScrollHandler headerHeight: number - isScrolledDown: boolean - scrollElRef: React.MutableRefObject<FlatList<any> | null> + scrollElRef: ListRef isFocused: boolean } const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( - function FeedSectionImpl( - {feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}, - ref, - ) { + function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) { const [hasNew, setHasNew] = React.useState(false) + const [isScrolledDown, setIsScrolledDown] = React.useState(false) const queryClient = useQueryClient() const onScrollToTop = useCallback(() => { @@ -536,8 +522,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( pollInterval={30e3} scrollElRef={scrollElRef} onHasNew={setHasNew} - onScroll={onScroll} - scrollEventThrottle={5} + onScrolledDownChange={setIsScrolledDown} renderEmptyState={renderPostsEmpty} headerOffset={headerHeight} /> @@ -558,7 +543,6 @@ function AboutSection({ feedRkey, feedInfo, headerHeight, - onScroll, scrollElRef, isOwner, }: { @@ -566,13 +550,13 @@ function AboutSection({ feedRkey: string feedInfo: FeedSourceFeedInfo headerHeight: number - onScroll: OnScrollHandler scrollElRef: React.MutableRefObject<ScrollView | null> isOwner: boolean }) { const pal = usePalette('default') const {_} = useLingui() - const scrollHandler = useAnimatedScrollHandler(onScroll) + const scrollHandlers = useScrollHandlers() + const onScroll = useAnimatedScrollHandler(scrollHandlers) const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) const {hasSession} = useSession() const {track} = useAnalytics() @@ -608,12 +592,12 @@ function AboutSection({ return ( <ScrollView ref={scrollElRef} + onScroll={onScroll} scrollEventThrottle={1} contentContainerStyle={{ paddingTop: headerHeight, minHeight: Dimensions.get('window').height * 1.5, - }} - onScroll={scrollHandler}> + }}> <View style={[ { diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 3dcc7121f..7f922e5b4 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -1,11 +1,5 @@ import React, {useCallback, useMemo} from 'react' -import { - ActivityIndicator, - FlatList, - Pressable, - StyleSheet, - View, -} from 'react-native' +import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {useNavigation} from '@react-navigation/native' @@ -22,6 +16,7 @@ import {EmptyState} from 'view/com/util/EmptyState' import {RichText} from 'view/com/util/text/RichText' import {Button} from 'view/com/util/forms/Button' import {TextLink} from 'view/com/util/Link' +import {ListRef} from 'view/com/util/List' import * as Toast from 'view/com/util/Toast' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {FAB} from 'view/com/util/fab/FAB' @@ -31,7 +26,6 @@ import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' -import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {NavigationProp} from 'lib/routes/types' import {toShareUrl} from 'lib/strings/url-helpers' import {shareUrl} from 'lib/sharing' @@ -165,36 +159,22 @@ function ProfileListScreenLoaded({ isHeaderReady={true} renderHeader={renderHeader} onCurrentPageSelected={onCurrentPageSelected}> - {({ - onScroll, - headerHeight, - isScrolledDown, - scrollElRef, - isFocused, - }) => ( + {({headerHeight, scrollElRef, isFocused}) => ( <FeedSection ref={feedSectionRef} feed={`list|${uri}`} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } - onScroll={onScroll} + scrollElRef={scrollElRef as ListRef} headerHeight={headerHeight} - isScrolledDown={isScrolledDown} isFocused={isFocused} /> )} - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + {({headerHeight, scrollElRef}) => ( <AboutSection ref={aboutSectionRef} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} list={list} onPressAddUser={onPressAddUser} - onScroll={onScroll} headerHeight={headerHeight} - isScrolledDown={isScrolledDown} /> )} </PagerWithHeader> @@ -221,16 +201,12 @@ function ProfileListScreenLoaded({ items={SECTION_TITLES_MOD} isHeaderReady={true} renderHeader={renderHeader}> - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + {({headerHeight, scrollElRef}) => ( <AboutSection list={list} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } + scrollElRef={scrollElRef as ListRef} onPressAddUser={onPressAddUser} - onScroll={onScroll} headerHeight={headerHeight} - isScrolledDown={isScrolledDown} /> )} </PagerWithHeader> @@ -615,19 +591,15 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { interface FeedSectionProps { feed: FeedDescriptor - onScroll: OnScrollHandler headerHeight: number - isScrolledDown: boolean - scrollElRef: React.MutableRefObject<FlatList<any> | null> + scrollElRef: ListRef isFocused: boolean } const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( - function FeedSectionImpl( - {feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused}, - ref, - ) { + function FeedSectionImpl({feed, scrollElRef, headerHeight, isFocused}, ref) { const queryClient = useQueryClient() const [hasNew, setHasNew] = React.useState(false) + const [isScrolledDown, setIsScrolledDown] = React.useState(false) const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ @@ -654,8 +626,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( pollInterval={30e3} scrollElRef={scrollElRef} onHasNew={setHasNew} - onScroll={onScroll} - scrollEventThrottle={1} + onScrolledDownChange={setIsScrolledDown} renderEmptyState={renderPostsEmpty} headerOffset={headerHeight} /> @@ -674,20 +645,19 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( interface AboutSectionProps { list: AppBskyGraphDefs.ListView onPressAddUser: () => void - onScroll: OnScrollHandler headerHeight: number - isScrolledDown: boolean - scrollElRef: React.MutableRefObject<FlatList<any> | null> + scrollElRef: ListRef } const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( function AboutSectionImpl( - {list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef}, + {list, onPressAddUser, headerHeight, scrollElRef}, ref, ) { const pal = usePalette('default') const {_} = useLingui() const {isMobile} = useWebMediaQueries() const {currentAccount} = useSession() + const [isScrolledDown, setIsScrolledDown] = React.useState(false) const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' const isOwner = list.creator.did === currentAccount?.did @@ -817,8 +787,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( renderHeader={renderHeader} renderEmptyState={renderEmptyState} headerOffset={headerHeight} - onScroll={onScroll} - scrollEventThrottle={1} + onScrolledDownChange={setIsScrolledDown} /> {isScrolledDown && ( <LoadLatestBtn diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index b4db270b3..7d7b4098f 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -8,7 +8,8 @@ import { Pressable, Platform, } from 'react-native' -import {FlatList, ScrollView, CenteredView} from '#/view/com/util/Views' +import {ScrollView, CenteredView} from '#/view/com/util/Views' +import {List} from '#/view/com/util/List' import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -155,7 +156,7 @@ function SearchScreenSuggestedFollows() { }, [currentAccount, setSuggestions, getSuggestedFollowsByActor]) return suggestions.length ? ( - <FlatList + <List data={suggestions} renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />} keyExtractor={item => item.did} @@ -243,7 +244,7 @@ function SearchScreenPostResults({query}: {query: string}) { {isFetched ? ( <> {posts.length ? ( - <FlatList + <List data={items} renderItem={({item}) => { if (item.type === 'post') { @@ -284,7 +285,7 @@ function SearchScreenUserResults({query}: {query: string}) { return isFetched && results ? ( <> {results.length ? ( - <FlatList + <List data={results} renderItem={({item}) => ( <ProfileCardWithFollowBtn profile={item} noBg /> |