diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 46 | ||||
-rw-r--r-- | src/view/com/feeds/ProfileFeedgens.tsx | 26 | ||||
-rw-r--r-- | src/view/com/lists/ListMembers.tsx | 20 | ||||
-rw-r--r-- | src/view/com/lists/MyLists.tsx | 4 | ||||
-rw-r--r-- | src/view/com/lists/ProfileLists.tsx | 26 | ||||
-rw-r--r-- | src/view/com/notifications/Feed.tsx | 19 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 47 | ||||
-rw-r--r-- | src/view/com/post-thread/PostLikedBy.tsx | 5 | ||||
-rw-r--r-- | src/view/com/post-thread/PostRepostedBy.tsx | 5 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 7 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 20 | ||||
-rw-r--r-- | src/view/com/profile/ProfileFollowers.tsx | 5 | ||||
-rw-r--r-- | src/view/com/profile/ProfileFollows.tsx | 5 | ||||
-rw-r--r-- | src/view/com/util/List.tsx | 64 | ||||
-rw-r--r-- | src/view/com/util/MainScrollProvider.tsx | 97 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 11 | ||||
-rw-r--r-- | src/view/com/util/Views.d.ts | 2 | ||||
-rw-r--r-- | src/view/com/util/Views.jsx | 2 | ||||
-rw-r--r-- | src/view/com/util/Views.web.tsx | 2 |
19 files changed, 259 insertions, 154 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, |