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
zio/stable
dan 2023-12-14 02:48:20 +00:00 committed by GitHub
parent fa3ccafa80
commit 7fd7970237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 280 additions and 354 deletions

View File

@ -0,0 +1,35 @@
import React, {createContext, useContext, useMemo} from 'react'
import {ScrollHandlers} from 'react-native-reanimated'
const ScrollContext = createContext<ScrollHandlers<any>>({
onBeginDrag: undefined,
onEndDrag: undefined,
onScroll: undefined,
})
export function useScrollHandlers(): ScrollHandlers<any> {
return useContext(ScrollContext)
}
type ProviderProps = {children: React.ReactNode} & ScrollHandlers<any>
// Note: this completely *overrides* the parent handlers.
// It's up to you to compose them with the parent ones via useScrollHandlers() if needed.
export function ScrollProvider({
children,
onBeginDrag,
onEndDrag,
onScroll,
}: ProviderProps) {
const handlers = useMemo(
() => ({
onBeginDrag,
onEndDrag,
onScroll,
}),
[onBeginDrag, onEndDrag, onScroll],
)
return (
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>
)
}

View File

@ -7,13 +7,15 @@ import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 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 {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSetMinimalShellMode} from '#/state/shell'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {ComposeIcon2} from 'lib/icons' import {ComposeIcon2} from 'lib/icons'
import {colors, s} from 'lib/styles' 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 {Feed} from '../posts/Feed'
import {TextLink} from '../util/Link' import {TextLink} from '../util/Link'
import {FAB} from '../util/fab/FAB' import {FAB} from '../util/fab/FAB'
@ -51,10 +53,11 @@ export function FeedPage({
const {isDesktop} = useWebMediaQueries() const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const setMinimalShellMode = useSetMinimalShellMode()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const headerOffset = useHeaderOffset() const headerOffset = useHeaderOffset()
const scrollElRef = React.useRef<FlatList>(null) const scrollElRef = React.useRef<ListMethods>(null)
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const scrollToTop = React.useCallback(() => { const scrollToTop = React.useCallback(() => {
@ -62,8 +65,8 @@ export function FeedPage({
animated: isNative, animated: isNative,
offset: -headerOffset, offset: -headerOffset,
}) })
resetMainScroll() setMinimalShellMode(false)
}, [headerOffset, resetMainScroll]) }, [headerOffset, setMinimalShellMode])
const onSoftReset = React.useCallback(() => { const onSoftReset = React.useCallback(() => {
const isScreenFocused = const isScreenFocused =
@ -164,21 +167,22 @@ export function FeedPage({
return ( return (
<View testID={testID} style={s.h100pct}> <View testID={testID} style={s.h100pct}>
<Feed <MainScrollProvider>
testID={testID ? `${testID}-feed` : undefined} <Feed
enabled={isPageFocused} testID={testID ? `${testID}-feed` : undefined}
feed={feed} enabled={isPageFocused}
feedParams={feedParams} feed={feed}
pollInterval={POLL_FREQ} feedParams={feedParams}
scrollElRef={scrollElRef} pollInterval={POLL_FREQ}
onScroll={onMainScroll} scrollElRef={scrollElRef}
onHasNew={setHasNew} onScrolledDownChange={setIsScrolledDown}
scrollEventThrottle={1} onHasNew={setHasNew}
renderEmptyState={renderEmptyState} renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed} renderEndOfFeed={renderEndOfFeed}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
headerOffset={headerOffset} headerOffset={headerOffset}
/> />
</MainScrollProvider>
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
onPress={onPressLoadLatest} onPress={onPressLoadLatest}

View File

@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react' import React from 'react'
import { import {
Dimensions, Dimensions,
RefreshControl, RefreshControl,
@ -8,18 +8,16 @@ import {
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views' import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard' import {FeedSourceCardLoaded} from './FeedSourceCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens' import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {hydrateFeedGenerator} from '#/state/queries/feed' import {hydrateFeedGenerator} from '#/state/queries/feed'
@ -37,9 +35,7 @@ interface SectionRef {
interface ProfileFeedgensProps { interface ProfileFeedgensProps {
did: string did: string
scrollElRef: MutableRefObject<FlatList<any> | null> scrollElRef: ListRef
onScroll?: OnScrollHandler
scrollEventThrottle?: number
headerOffset: number headerOffset: number
enabled?: boolean enabled?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
@ -50,16 +46,7 @@ export const ProfileFeedgens = React.forwardRef<
SectionRef, SectionRef,
ProfileFeedgensProps ProfileFeedgensProps
>(function ProfileFeedgensImpl( >(function ProfileFeedgensImpl(
{ {did, scrollElRef, headerOffset, enabled, style, testID},
did,
scrollElRef,
onScroll,
scrollEventThrottle,
headerOffset,
enabled,
style,
testID,
},
ref, ref,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
@ -185,10 +172,9 @@ export const ProfileFeedgens = React.forwardRef<
[error, refetch, onPressRetryLoadMore, pal, preferences], [error, refetch, onPressRetryLoadMore, pal, preferences],
) )
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
<FlatList <List
testID={testID ? `${testID}-flatlist` : undefined} testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef} ref={scrollElRef}
data={items} data={items}
@ -207,8 +193,6 @@ export const ProfileFeedgens = React.forwardRef<
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}} style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}} contentOffset={{x: 0, y: headerOffset * -1}}

View File

@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
@ -8,7 +8,7 @@ import {
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {FlatList} from '../util/Views' import {List, ListRef} from '../util/List'
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@ -18,10 +18,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useListMembersQuery} from '#/state/queries/list-members' import {useListMembersQuery} from '#/state/queries/list-members'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
@ -34,24 +32,22 @@ export function ListMembers({
list, list,
style, style,
scrollElRef, scrollElRef,
onScroll, onScrolledDownChange,
onPressTryAgain, onPressTryAgain,
renderHeader, renderHeader,
renderEmptyState, renderEmptyState,
testID, testID,
scrollEventThrottle,
headerOffset = 0, headerOffset = 0,
desktopFixedHeightOffset, desktopFixedHeightOffset,
}: { }: {
list: string list: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: ListRef
onScroll: OnScrollHandler onScrolledDownChange: (isScrolledDown: boolean) => void
onPressTryAgain?: () => void onPressTryAgain?: () => void
renderHeader: () => JSX.Element renderHeader: () => JSX.Element
renderEmptyState: () => JSX.Element renderEmptyState: () => JSX.Element
testID?: string testID?: string
scrollEventThrottle?: number
headerOffset?: number headerOffset?: number
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
}) { }) {
@ -209,10 +205,9 @@ export function ListMembers({
[isFetching], [isFetching],
) )
const scrollHandler = useAnimatedScrollHandler(onScroll)
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
<FlatList <List
testID={testID ? `${testID}-flatlist` : undefined} testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef} ref={scrollElRef}
data={items} data={items}
@ -233,10 +228,9 @@ export function ListMembers({
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}} style={{paddingTop: headerOffset}}
onScroll={scrollHandler} onScrolledDownChange={onScrolledDownChange}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
scrollEventThrottle={scrollEventThrottle}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}} contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf

View File

@ -15,7 +15,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {FlatList} from '../util/Views' import {List} from '../util/List'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
@ -119,7 +119,7 @@ export function MyLists({
[error, onRefresh, renderItem, pal], [error, onRefresh, renderItem, pal],
) )
const FlatListCom = inline ? RNFlatList : FlatList const FlatListCom = inline ? RNFlatList : List
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
{items.length > 0 && ( {items.length > 0 && (

View File

@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react' import React from 'react'
import { import {
Dimensions, Dimensions,
RefreshControl, RefreshControl,
@ -8,7 +8,7 @@ import {
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views' import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard' import {ListCard} from './ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@ -16,11 +16,9 @@ import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists' import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
@ -36,9 +34,7 @@ interface SectionRef {
interface ProfileListsProps { interface ProfileListsProps {
did: string did: string
scrollElRef: MutableRefObject<FlatList<any> | null> scrollElRef: ListRef
onScroll?: OnScrollHandler
scrollEventThrottle?: number
headerOffset: number headerOffset: number
enabled?: boolean enabled?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
@ -47,16 +43,7 @@ interface ProfileListsProps {
export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
function ProfileListsImpl( function ProfileListsImpl(
{ {did, scrollElRef, headerOffset, enabled, style, testID},
did,
scrollElRef,
onScroll,
scrollEventThrottle,
headerOffset,
enabled,
style,
testID,
},
ref, ref,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
@ -187,10 +174,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
[error, refetch, onPressRetryLoadMore, pal], [error, refetch, onPressRetryLoadMore, pal],
) )
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
<FlatList <List
testID={testID ? `${testID}-flatlist` : undefined} testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef} ref={scrollElRef}
data={items} data={items}
@ -209,8 +195,6 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}} style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}} contentOffset={{x: 0, y: headerOffset * -1}}

View File

@ -1,13 +1,11 @@
import React, {MutableRefObject} from 'react' import React from 'react'
import {CenteredView, FlatList} from '../util/Views' import {CenteredView} from '../util/Views'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState' import {EmptyState} from '../util/EmptyState'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
@ -15,6 +13,7 @@ import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger' import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {List, ListRef} from '../util/List'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@ -23,12 +22,12 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
export function Feed({ export function Feed({
scrollElRef, scrollElRef,
onPressTryAgain, onPressTryAgain,
onScroll, onScrolledDownChange,
ListHeaderComponent, ListHeaderComponent,
}: { }: {
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: ListRef
onPressTryAgain?: () => void onPressTryAgain?: () => void
onScroll?: OnScrollHandler onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element ListHeaderComponent?: () => JSX.Element
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
@ -135,7 +134,6 @@ export function Feed({
[isFetchingNextPage], [isFetchingNextPage],
) )
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
{error && ( {error && (
@ -146,7 +144,7 @@ export function Feed({
/> />
</CenteredView> </CenteredView>
)} )}
<FlatList <List
testID="notifsFeed" testID="notifsFeed"
ref={scrollElRef} ref={scrollElRef}
data={items} data={items}
@ -164,8 +162,7 @@ export function Feed({
} }
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
onScroll={scrollHandler} onScrolledDownChange={onScrolledDownChange}
scrollEventThrottle={1}
contentContainerStyle={s.contentContainer} contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight

View File

@ -1,7 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { import {
LayoutChangeEvent, LayoutChangeEvent,
FlatList,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
View, View,
@ -20,17 +19,14 @@ import Animated, {
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from './TabBar' import {TabBar} from './TabBar'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {ListMethods} from '../util/List'
const SCROLLED_DOWN_LIMIT = 200 import {ScrollProvider} from '#/lib/ScrollContext'
export interface PagerWithHeaderChildParams { export interface PagerWithHeaderChildParams {
headerHeight: number headerHeight: number
isFocused: boolean isFocused: boolean
onScroll: OnScrollHandler scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null>
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
} }
export interface PagerWithHeaderProps { export interface PagerWithHeaderProps {
@ -62,7 +58,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const [currentPage, setCurrentPage] = React.useState(0) const [currentPage, setCurrentPage] = React.useState(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0) const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollY = useSharedValue(0) const scrollY = useSharedValue(0)
const headerHeight = headerOnlyHeight + tabBarHeight const headerHeight = headerOnlyHeight + tabBarHeight
@ -155,15 +150,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
if (!throttleTimeout.current) { if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => { throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)() 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 */) }, 80 /* Sync often enough you're unlikely to catch it unsynced */)
} }
}) })
@ -211,7 +198,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
index={i} index={i}
isReady={isReady} isReady={isReady}
isFocused={i === currentPage} isFocused={i === currentPage}
isScrolledDown={isScrolledDown}
onScrollWorklet={i === currentPage ? onScrollWorklet : noop} onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
registerRef={registerRef} registerRef={registerRef}
renderTab={child} renderTab={child}
@ -293,7 +279,6 @@ function PagerItem({
index, index,
isReady, isReady,
isFocused, isFocused,
isScrolledDown,
onScrollWorklet, onScrollWorklet,
renderTab, renderTab,
registerRef, registerRef,
@ -302,7 +287,6 @@ function PagerItem({
index: number index: number
isFocused: boolean isFocused: boolean
isReady: boolean isReady: boolean
isScrolledDown: boolean
registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void
onScrollWorklet: (e: NativeScrollEvent) => void onScrollWorklet: (e: NativeScrollEvent) => void
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
@ -316,24 +300,21 @@ function PagerItem({
} }
}, [scrollElRef, registerRef, index]) }, [scrollElRef, registerRef, index])
const scrollHandler = React.useMemo(
() => ({onScroll: onScrollWorklet}),
[onScrollWorklet],
)
if (!isReady || renderTab == null) { if (!isReady || renderTab == null) {
return null return null
} }
return renderTab({ return (
headerHeight, <ScrollProvider onScroll={onScrollWorklet}>
isFocused, {renderTab({
isScrolledDown, headerHeight,
onScroll: scrollHandler, isFocused,
scrollElRef: scrollElRef as React.MutableRefObject< scrollElRef: scrollElRef as React.MutableRefObject<
FlatList<any> | ScrollView | null ListMethods | ScrollView | null
>, >,
}) })}
</ScrollProvider>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,7 +1,8 @@
import React, {useCallback, useMemo, useState} from 'react' import React, {useCallback, useMemo, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' 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 {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -84,7 +85,7 @@ export function PostLikedBy({uri}: {uri: string}) {
// loaded // loaded
// = // =
return ( return (
<FlatList <List
data={likes} data={likes}
keyExtractor={item => item.actor.did} keyExtractor={item => item.actor.did}
refreshControl={ refreshControl={

View File

@ -1,7 +1,8 @@
import React, {useMemo, useCallback, useState} from 'react' import React, {useMemo, useCallback, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 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 {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -85,7 +86,7 @@ export function PostRepostedBy({uri}: {uri: string}) {
// loaded // loaded
// = // =
return ( return (
<FlatList <List
data={repostedBy} data={repostedBy}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshControl={

View File

@ -8,7 +8,8 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api' import {AppBskyFeedDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views' import {CenteredView} from '../util/Views'
import {List, ListMethods} from '../util/List'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
@ -140,7 +141,7 @@ function PostThreadLoaded({
const {_} = useLingui() const {_} = useLingui()
const pal = usePalette('default') const pal = usePalette('default')
const {isTablet, isDesktop} = useWebMediaQueries() const {isTablet, isDesktop} = useWebMediaQueries()
const ref = useRef<FlatList>(null) const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null) const highlightedPostRef = useRef<View | null>(null)
const needsScrollAdjustment = useRef<boolean>( const needsScrollAdjustment = useRef<boolean>(
!isNative || // web always uses scroll adjustment !isNative || // web always uses scroll adjustment
@ -335,7 +336,7 @@ function PostThreadLoaded({
) )
return ( return (
<FlatList <List
ref={ref} ref={ref}
data={posts} data={posts}
initialNumToRender={!isNative ? posts.length : undefined} initialNumToRender={!isNative ? posts.length : undefined}

View File

@ -1,4 +1,4 @@
import React, {memo, MutableRefObject} from 'react' import React, {memo} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
AppState, AppState,
@ -10,15 +10,13 @@ import {
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views' import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {FeedErrorMessage} from './FeedErrorMessage' import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice' import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import { import {
@ -45,9 +43,8 @@ let Feed = ({
enabled, enabled,
pollInterval, pollInterval,
scrollElRef, scrollElRef,
onScroll, onScrolledDownChange,
onHasNew, onHasNew,
scrollEventThrottle,
renderEmptyState, renderEmptyState,
renderEndOfFeed, renderEndOfFeed,
testID, testID,
@ -62,10 +59,9 @@ let Feed = ({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
enabled?: boolean enabled?: boolean
pollInterval?: number pollInterval?: number
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: ListRef
onHasNew?: (v: boolean) => void onHasNew?: (v: boolean) => void
onScroll?: OnScrollHandler onScrolledDownChange?: (isScrolledDown: boolean) => void
scrollEventThrottle?: number
renderEmptyState: () => JSX.Element renderEmptyState: () => JSX.Element
renderEndOfFeed?: () => JSX.Element renderEndOfFeed?: () => JSX.Element
testID?: string testID?: string
@ -270,10 +266,9 @@ let Feed = ({
) )
}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
<FlatList <List
testID={testID ? `${testID}-flatlist` : undefined} testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef} ref={scrollElRef}
data={feedItems} data={feedItems}
@ -294,8 +289,7 @@ let Feed = ({
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}} style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined} onScrolledDownChange={onScrolledDownChange}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={2} // number of posts left to trigger load more onEndReachedThreshold={2} // number of posts left to trigger load more

View File

@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 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 {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -86,7 +87,7 @@ export function ProfileFollowers({name}: {name: string}) {
// loaded // loaded
// = // =
return ( return (
<FlatList <List
data={followers} data={followers}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshControl={

View File

@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 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 {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -86,7 +87,7 @@ export function ProfileFollows({name}: {name: string}) {
// loaded // loaded
// = // =
return ( return (
<FlatList <List
data={follows} data={follows}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshControl={

View File

@ -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

View File

@ -1,30 +1,18 @@
import {useState, useCallback, useMemo} from 'react' import React, {useCallback} from 'react'
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' import {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import { import {useSharedValue, interpolate} from 'react-native-reanimated'
useSharedValue,
interpolate,
runOnJS,
ScrollHandlers,
} from 'react-native-reanimated'
function clamp(num: number, min: number, max: number) { function clamp(num: number, min: number, max: number) {
'worklet' 'worklet'
return Math.min(Math.max(num, min), max) return Math.min(Math.max(num, min), max)
} }
export type OnScrollCb = ( export function MainScrollProvider({children}: {children: React.ReactNode}) {
event: NativeSyntheticEvent<NativeScrollEvent>,
) => void
export type OnScrollHandler = ScrollHandlers<any>
export type ResetCb = () => void
export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const [isScrolledDown, setIsScrolledDown] = useState(false)
const mode = useMinimalShellMode() const mode = useMinimalShellMode()
const setMode = useSetMinimalShellMode() const setMode = useSetMinimalShellMode()
const startDragOffset = useSharedValue<number | null>(null) const startDragOffset = useSharedValue<number | null>(null)
@ -58,13 +46,6 @@ export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
const onScroll = useCallback( const onScroll = useCallback(
(e: NativeScrollEvent) => { (e: NativeScrollEvent) => {
'worklet' 'worklet'
// Keep track of whether we want to show "scroll to top".
if (!isScrolledDown && e.contentOffset.y > s.window.height) {
runOnJS(setIsScrolledDown)(true)
} else if (isScrolledDown && e.contentOffset.y < s.window.height) {
runOnJS(setIsScrolledDown)(false)
}
if (startDragOffset.value === null || startMode.value === null) { if (startDragOffset.value === null || startMode.value === null) {
if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) { if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
// If we're close enough to the top, always show the shell. // If we're close enough to the top, always show the shell.
@ -102,24 +83,15 @@ export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
startMode.value = mode.value startMode.value = mode.value
} }
}, },
[headerHeight, mode, setMode, isScrolledDown, startDragOffset, startMode], [headerHeight, mode, setMode, startDragOffset, startMode],
) )
const scrollHandler: ScrollHandlers<any> = useMemo( return (
() => ({ <ScrollProvider
onBeginDrag, onBeginDrag={onBeginDrag}
onEndDrag, onEndDrag={onEndDrag}
onScroll, onScroll={onScroll}>
}), {children}
[onBeginDrag, onEndDrag, onScroll], </ScrollProvider>
) )
return [
scrollHandler,
isScrolledDown,
useCallback(() => {
setIsScrolledDown(false)
setMode(false)
}, [setMode]),
]
} }

View File

@ -1,13 +1,14 @@
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import { import {
NativeSyntheticEvent,
NativeScrollEvent,
Pressable, Pressable,
RefreshControl, RefreshControl,
StyleSheet, StyleSheet,
View, View,
ScrollView, ScrollView,
} from 'react-native' } from 'react-native'
import {FlatList} from './Views' import {FlatList_INTERNAL} from './Views'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Text} from './text/Text' import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -38,7 +39,7 @@ export const ViewSelector = React.forwardRef<
| null | null
| undefined | undefined
onSelectView?: (viewIndex: number) => void onSelectView?: (viewIndex: number) => void
onScroll?: OnScrollCb onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
onRefresh?: () => void onRefresh?: () => void
onEndReached?: (info: {distanceFromEnd: number}) => void onEndReached?: (info: {distanceFromEnd: number}) => void
} }
@ -59,7 +60,7 @@ export const ViewSelector = React.forwardRef<
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0) const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = React.useRef<FlatList>(null) const flatListRef = React.useRef<FlatList_INTERNAL>(null)
// events // events
// = // =
@ -110,7 +111,7 @@ export const ViewSelector = React.forwardRef<
[items], [items],
) )
return ( return (
<FlatList <FlatList_INTERNAL
ref={flatListRef} ref={flatListRef}
data={data} data={data}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {ViewProps} from 'react-native' import {ViewProps} from 'react-native'
export {FlatList, ScrollView} from 'react-native' export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native'
export function CenteredView({ export function CenteredView({
style, style,
sideBorders, sideBorders,

View File

@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
export const FlatList = Animated.FlatList export const FlatList_INTERNAL = Animated.FlatList
export const ScrollView = Animated.ScrollView export const ScrollView = Animated.ScrollView
export function CenteredView(props) { export function CenteredView(props) {
return <View {...props} /> return <View {...props} />

View File

@ -49,7 +49,7 @@ export function CenteredView({
return <View style={style} {...props} /> return <View style={style} {...props} />
} }
export const FlatList = React.forwardRef(function FlatListImpl<ItemT>( export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
{ {
contentContainerStyle, contentContainerStyle,
style, style,

View File

@ -19,7 +19,7 @@ import {
import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {Text} from 'view/com/util/text/Text' 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 {useFocusEffect} from '@react-navigation/native'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
@ -481,7 +481,7 @@ export function FeedsScreen(_props: Props) {
{preferences ? <View /> : <ActivityIndicator />} {preferences ? <View /> : <ActivityIndicator />}
<FlatList <List
style={[!isTabletOrDesktop && s.flex1, styles.list]} style={[!isTabletOrDesktop && s.flex1, styles.list]}
data={items} data={items}
keyExtractor={item => item.key} keyExtractor={item => item.key}

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {FlatList, View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import { import {
@ -9,8 +9,9 @@ import {
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 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 {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
@ -35,8 +36,8 @@ type Props = NativeStackScreenProps<
export function NotificationsScreen({}: Props) { export function NotificationsScreen({}: Props) {
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollElRef = React.useRef<FlatList>(null) const scrollElRef = React.useRef<ListMethods>(null)
const checkLatestRef = React.useRef<() => void | null>() const checkLatestRef = React.useRef<() => void | null>()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
@ -50,8 +51,8 @@ export function NotificationsScreen({}: Props) {
// = // =
const scrollToTop = React.useCallback(() => { const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0}) scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0})
resetMainScroll() setMinimalShellMode(false)
}, [scrollElRef, resetMainScroll]) }, [scrollElRef, setMinimalShellMode])
const onPressLoadLatest = React.useCallback(() => { const onPressLoadLatest = React.useCallback(() => {
scrollToTop() scrollToTop()
@ -130,11 +131,13 @@ export function NotificationsScreen({}: Props) {
return ( return (
<View testID="notificationsScreen" style={s.hContentRegion}> <View testID="notificationsScreen" style={s.hContentRegion}>
<ViewHeader title={_(msg`Notifications`)} canGoBack={false} /> <ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
<Feed <MainScrollProvider>
onScroll={onMainScroll} <Feed
scrollElRef={scrollElRef} onScrolledDownChange={setIsScrolledDown}
ListHeaderComponent={ListHeaderComponent} scrollElRef={scrollElRef}
/> ListHeaderComponent={ListHeaderComponent}
/>
</MainScrollProvider>
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
onPress={onPressLoadLatest} onPress={onPressLoadLatest}

View File

@ -5,7 +5,8 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 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 {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {Feed} from 'view/com/posts/Feed' import {Feed} from 'view/com/posts/Feed'
import {ProfileLists} from '../com/lists/ProfileLists' import {ProfileLists} from '../com/lists/ProfileLists'
@ -20,7 +21,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {ComposeIcon2} from 'lib/icons' import {ComposeIcon2} from 'lib/icons'
import {useSetTitle} from 'lib/hooks/useSetTitle' import {useSetTitle} from 'lib/hooks/useSetTitle'
import {combinedDisplayName} from 'lib/strings/display-names' import {combinedDisplayName} from 'lib/strings/display-names'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {FeedDescriptor} from '#/state/queries/post-feed' import {FeedDescriptor} from '#/state/queries/post-feed'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
@ -277,103 +277,67 @@ function ProfileScreenLoaded({
onPageSelected={onPageSelected} onPageSelected={onPageSelected}
onCurrentPageSelected={onCurrentPageSelected} onCurrentPageSelected={onCurrentPageSelected}
renderHeader={renderHeader}> renderHeader={renderHeader}>
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => ( {({headerHeight, isFocused, scrollElRef}) => (
<FeedSection <FeedSection
ref={postsSectionRef} ref={postsSectionRef}
feed={`author|${profile.did}|posts_and_author_threads`} feed={`author|${profile.did}|posts_and_author_threads`}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isFocused={isFocused} isFocused={isFocused}
isScrolledDown={isScrolledDown} scrollElRef={scrollElRef as ListRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
ignoreFilterFor={profile.did} ignoreFilterFor={profile.did}
/> />
)} )}
{showRepliesTab {showRepliesTab
? ({ ? ({headerHeight, isFocused, scrollElRef}) => (
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
}) => (
<FeedSection <FeedSection
ref={repliesSectionRef} ref={repliesSectionRef}
feed={`author|${profile.did}|posts_with_replies`} feed={`author|${profile.did}|posts_with_replies`}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isFocused={isFocused} isFocused={isFocused}
isScrolledDown={isScrolledDown} scrollElRef={scrollElRef as ListRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
ignoreFilterFor={profile.did} ignoreFilterFor={profile.did}
/> />
) )
: null} : null}
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => ( {({headerHeight, isFocused, scrollElRef}) => (
<FeedSection <FeedSection
ref={mediaSectionRef} ref={mediaSectionRef}
feed={`author|${profile.did}|posts_with_media`} feed={`author|${profile.did}|posts_with_media`}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isFocused={isFocused} isFocused={isFocused}
isScrolledDown={isScrolledDown} scrollElRef={scrollElRef as ListRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
ignoreFilterFor={profile.did} ignoreFilterFor={profile.did}
/> />
)} )}
{showLikesTab {showLikesTab
? ({ ? ({headerHeight, isFocused, scrollElRef}) => (
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
}) => (
<FeedSection <FeedSection
ref={likesSectionRef} ref={likesSectionRef}
feed={`likes|${profile.did}`} feed={`likes|${profile.did}`}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isFocused={isFocused} isFocused={isFocused}
isScrolledDown={isScrolledDown} scrollElRef={scrollElRef as ListRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
ignoreFilterFor={profile.did} ignoreFilterFor={profile.did}
/> />
) )
: null} : null}
{showFeedsTab {showFeedsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => ( ? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedgens <ProfileFeedgens
ref={feedsSectionRef} ref={feedsSectionRef}
did={profile.did} did={profile.did}
scrollElRef={ scrollElRef={scrollElRef as ListRef}
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollEventThrottle={1}
headerOffset={headerHeight} headerOffset={headerHeight}
enabled={isFocused} enabled={isFocused}
/> />
) )
: null} : null}
{showListsTab {showListsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => ( ? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileLists <ProfileLists
ref={listsSectionRef} ref={listsSectionRef}
did={profile.did} did={profile.did}
scrollElRef={ scrollElRef={scrollElRef as ListRef}
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollEventThrottle={1}
headerOffset={headerHeight} headerOffset={headerHeight}
enabled={isFocused} enabled={isFocused}
/> />
@ -396,28 +360,19 @@ function ProfileScreenLoaded({
interface FeedSectionProps { interface FeedSectionProps {
feed: FeedDescriptor feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number headerHeight: number
isFocused: boolean isFocused: boolean
isScrolledDown: boolean scrollElRef: ListRef
scrollElRef: React.MutableRefObject<FlatList<any> | null>
ignoreFilterFor?: string ignoreFilterFor?: string
} }
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl( function FeedSectionImpl(
{ {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
feed,
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
ignoreFilterFor,
},
ref, ref,
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const onScrollToTop = React.useCallback(() => { const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({ scrollElRef.current?.scrollToOffset({
@ -443,8 +398,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
feed={feed} feed={feed}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
onHasNew={setHasNew} onHasNew={setHasNew}
onScroll={onScroll} onScrolledDownChange={setIsScrolledDown}
scrollEventThrottle={1}
renderEmptyState={renderPostsEmpty} renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight} headerOffset={headerHeight}
renderEndOfFeed={ProfileEndOfFeed} renderEndOfFeed={ProfileEndOfFeed}

View File

@ -1,11 +1,5 @@
import React, {useMemo, useCallback} from 'react' import React, {useMemo, useCallback} from 'react'
import { import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
Dimensions,
StyleSheet,
View,
ActivityIndicator,
FlatList,
} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' 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 {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed' import {Feed} from 'view/com/posts/Feed'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {RichText} from 'view/com/util/text/RichText' 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 * as Toast from 'view/com/util/Toast'
import {useSetTitle} from 'lib/hooks/useSetTitle' import {useSetTitle} from 'lib/hooks/useSetTitle'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {Haptics} from 'lib/haptics' import {Haptics} from 'lib/haptics'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' 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 {makeCustomFeedLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {CenteredView, ScrollView} from 'view/com/util/Views' import {CenteredView, ScrollView} from 'view/com/util/Views'
@ -46,7 +42,6 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import { import {
useFeedSourceInfoQuery, useFeedSourceInfoQuery,
FeedSourceFeedInfo, FeedSourceFeedInfo,
@ -403,17 +398,13 @@ export function ProfileFeedScreenInner({
isHeaderReady={true} isHeaderReady={true}
renderHeader={renderHeader} renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}> onCurrentPageSelected={onCurrentPageSelected}>
{({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) => {({headerHeight, scrollElRef, isFocused}) =>
isPublicResponse?.isPublic ? ( isPublicResponse?.isPublic ? (
<FeedSection <FeedSection
ref={feedSectionRef} ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`} feed={`feedgen|${feedInfo.uri}`}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isScrolledDown={isScrolledDown} scrollElRef={scrollElRef as ListRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
isFocused={isFocused} isFocused={isFocused}
/> />
) : ( ) : (
@ -422,13 +413,12 @@ export function ProfileFeedScreenInner({
</CenteredView> </CenteredView>
) )
} }
{({onScroll, headerHeight, scrollElRef}) => ( {({headerHeight, scrollElRef}) => (
<AboutSection <AboutSection
feedOwnerDid={feedInfo.creatorDid} feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey} feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo} feedInfo={feedInfo}
headerHeight={headerHeight} headerHeight={headerHeight}
onScroll={onScroll}
scrollElRef={ scrollElRef={
scrollElRef as React.MutableRefObject<ScrollView | null> scrollElRef as React.MutableRefObject<ScrollView | null>
} }
@ -497,18 +487,14 @@ function NonPublicFeedMessage({rawError}: {rawError?: Error}) {
interface FeedSectionProps { interface FeedSectionProps {
feed: FeedDescriptor feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number headerHeight: number
isScrolledDown: boolean scrollElRef: ListRef
scrollElRef: React.MutableRefObject<FlatList<any> | null>
isFocused: boolean isFocused: boolean
} }
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl( function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
{feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused},
ref,
) {
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const onScrollToTop = useCallback(() => { const onScrollToTop = useCallback(() => {
@ -536,8 +522,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
pollInterval={30e3} pollInterval={30e3}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
onHasNew={setHasNew} onHasNew={setHasNew}
onScroll={onScroll} onScrolledDownChange={setIsScrolledDown}
scrollEventThrottle={5}
renderEmptyState={renderPostsEmpty} renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight} headerOffset={headerHeight}
/> />
@ -558,7 +543,6 @@ function AboutSection({
feedRkey, feedRkey,
feedInfo, feedInfo,
headerHeight, headerHeight,
onScroll,
scrollElRef, scrollElRef,
isOwner, isOwner,
}: { }: {
@ -566,13 +550,13 @@ function AboutSection({
feedRkey: string feedRkey: string
feedInfo: FeedSourceFeedInfo feedInfo: FeedSourceFeedInfo
headerHeight: number headerHeight: number
onScroll: OnScrollHandler
scrollElRef: React.MutableRefObject<ScrollView | null> scrollElRef: React.MutableRefObject<ScrollView | null>
isOwner: boolean isOwner: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const scrollHandler = useAnimatedScrollHandler(onScroll) const scrollHandlers = useScrollHandlers()
const onScroll = useAnimatedScrollHandler(scrollHandlers)
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
const {hasSession} = useSession() const {hasSession} = useSession()
const {track} = useAnalytics() const {track} = useAnalytics()
@ -608,12 +592,12 @@ function AboutSection({
return ( return (
<ScrollView <ScrollView
ref={scrollElRef} ref={scrollElRef}
onScroll={onScroll}
scrollEventThrottle={1} scrollEventThrottle={1}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: headerHeight, paddingTop: headerHeight,
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}>
onScroll={scrollHandler}>
<View <View
style={[ style={[
{ {

View File

@ -1,11 +1,5 @@
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
import { import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
ActivityIndicator,
FlatList,
Pressable,
StyleSheet,
View,
} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {useNavigation} from '@react-navigation/native' 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 {RichText} from 'view/com/util/text/RichText'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import * as Toast from 'view/com/util/Toast' import * as Toast from 'view/com/util/Toast'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB' 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 {useSetTitle} from 'lib/hooks/useSetTitle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
@ -165,36 +159,22 @@ function ProfileListScreenLoaded({
isHeaderReady={true} isHeaderReady={true}
renderHeader={renderHeader} renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}> onCurrentPageSelected={onCurrentPageSelected}>
{({ {({headerHeight, scrollElRef, isFocused}) => (
onScroll,
headerHeight,
isScrolledDown,
scrollElRef,
isFocused,
}) => (
<FeedSection <FeedSection
ref={feedSectionRef} ref={feedSectionRef}
feed={`list|${uri}`} feed={`list|${uri}`}
scrollElRef={ scrollElRef={scrollElRef as ListRef}
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
isFocused={isFocused} isFocused={isFocused}
/> />
)} )}
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( {({headerHeight, scrollElRef}) => (
<AboutSection <AboutSection
ref={aboutSectionRef} ref={aboutSectionRef}
scrollElRef={ scrollElRef={scrollElRef as ListRef}
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
list={list} list={list}
onPressAddUser={onPressAddUser} onPressAddUser={onPressAddUser}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
/> />
)} )}
</PagerWithHeader> </PagerWithHeader>
@ -221,16 +201,12 @@ function ProfileListScreenLoaded({
items={SECTION_TITLES_MOD} items={SECTION_TITLES_MOD}
isHeaderReady={true} isHeaderReady={true}
renderHeader={renderHeader}> renderHeader={renderHeader}>
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( {({headerHeight, scrollElRef}) => (
<AboutSection <AboutSection
list={list} list={list}
scrollElRef={ scrollElRef={scrollElRef as ListRef}
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onPressAddUser={onPressAddUser} onPressAddUser={onPressAddUser}
onScroll={onScroll}
headerHeight={headerHeight} headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
/> />
)} )}
</PagerWithHeader> </PagerWithHeader>
@ -615,19 +591,15 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
interface FeedSectionProps { interface FeedSectionProps {
feed: FeedDescriptor feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number headerHeight: number
isScrolledDown: boolean scrollElRef: ListRef
scrollElRef: React.MutableRefObject<FlatList<any> | null>
isFocused: boolean isFocused: boolean
} }
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl( function FeedSectionImpl({feed, scrollElRef, headerHeight, isFocused}, ref) {
{feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused},
ref,
) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const onScrollToTop = useCallback(() => { const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({ scrollElRef.current?.scrollToOffset({
@ -654,8 +626,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
pollInterval={30e3} pollInterval={30e3}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
onHasNew={setHasNew} onHasNew={setHasNew}
onScroll={onScroll} onScrolledDownChange={setIsScrolledDown}
scrollEventThrottle={1}
renderEmptyState={renderPostsEmpty} renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight} headerOffset={headerHeight}
/> />
@ -674,20 +645,19 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
interface AboutSectionProps { interface AboutSectionProps {
list: AppBskyGraphDefs.ListView list: AppBskyGraphDefs.ListView
onPressAddUser: () => void onPressAddUser: () => void
onScroll: OnScrollHandler
headerHeight: number headerHeight: number
isScrolledDown: boolean scrollElRef: ListRef
scrollElRef: React.MutableRefObject<FlatList<any> | null>
} }
const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
function AboutSectionImpl( function AboutSectionImpl(
{list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef}, {list, onPressAddUser, headerHeight, scrollElRef},
ref, ref,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isOwner = list.creator.did === currentAccount?.did const isOwner = list.creator.did === currentAccount?.did
@ -817,8 +787,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
renderHeader={renderHeader} renderHeader={renderHeader}
renderEmptyState={renderEmptyState} renderEmptyState={renderEmptyState}
headerOffset={headerHeight} headerOffset={headerHeight}
onScroll={onScroll} onScrolledDownChange={setIsScrolledDown}
scrollEventThrottle={1}
/> />
{isScrolledDown && ( {isScrolledDown && (
<LoadLatestBtn <LoadLatestBtn

View File

@ -8,7 +8,8 @@ import {
Pressable, Pressable,
Platform, Platform,
} from 'react-native' } 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 {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@ -155,7 +156,7 @@ function SearchScreenSuggestedFollows() {
}, [currentAccount, setSuggestions, getSuggestedFollowsByActor]) }, [currentAccount, setSuggestions, getSuggestedFollowsByActor])
return suggestions.length ? ( return suggestions.length ? (
<FlatList <List
data={suggestions} data={suggestions}
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />} renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />}
keyExtractor={item => item.did} keyExtractor={item => item.did}
@ -243,7 +244,7 @@ function SearchScreenPostResults({query}: {query: string}) {
{isFetched ? ( {isFetched ? (
<> <>
{posts.length ? ( {posts.length ? (
<FlatList <List
data={items} data={items}
renderItem={({item}) => { renderItem={({item}) => {
if (item.type === 'post') { if (item.type === 'post') {
@ -284,7 +285,7 @@ function SearchScreenUserResults({query}: {query: string}) {
return isFetched && results ? ( return isFetched && results ? (
<> <>
{results.length ? ( {results.length ? (
<FlatList <List
data={results} data={results}
renderItem={({item}) => ( renderItem={({item}) => (
<ProfileCardWithFollowBtn profile={item} noBg /> <ProfileCardWithFollowBtn profile={item} noBg />