Implement "scroll to top" for profile tabs (#1973)

* Hook up scroll to top handlers

* Scroll and invalidate Feeds/Lists

* Fix index calc due to conditional tabs

* Reorder lines for clarity
zio/stable
dan 2023-11-22 04:25:11 +00:00 committed by GitHub
parent 3de1d556a9
commit 08333002cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 275 additions and 181 deletions

View File

@ -8,13 +8,14 @@ import {
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
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} from '#/state/queries/profile-feedgens'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
@ -29,7 +30,26 @@ const EMPTY = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export function ProfileFeedgens({
interface SectionRef {
scrollToTop: () => void
}
interface ProfileFeedgensProps {
did: string
scrollElRef: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
testID?: string
}
export const ProfileFeedgens = React.forwardRef<
SectionRef,
ProfileFeedgensProps
>(function ProfileFeedgensImpl(
{
did,
scrollElRef,
onScroll,
@ -38,16 +58,9 @@ export function ProfileFeedgens({
enabled,
style,
testID,
}: {
did: string
scrollElRef?: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
testID?: string
}) {
},
ref,
) {
const pal = usePalette('default')
const theme = useTheme()
const [isPTRing, setIsPTRing] = React.useState(false)
@ -88,6 +101,17 @@ export function ProfileFeedgens({
// events
// =
const queryClient = useQueryClient()
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerOffset})
queryClient.invalidateQueries({queryKey: RQKEY(did)})
}, [scrollElRef, queryClient, headerOffset, did])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
@ -192,7 +216,7 @@ export function ProfileFeedgens({
/>
</View>
)
}
})
const styles = StyleSheet.create({
item: {

View File

@ -8,6 +8,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {ListCard} from './ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
@ -15,7 +16,7 @@ import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery} from '#/state/queries/profile-lists'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
@ -28,7 +29,24 @@ const EMPTY = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export function ProfileLists({
interface SectionRef {
scrollToTop: () => void
}
interface ProfileListsProps {
did: string
scrollElRef: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
testID?: string
}
export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
function ProfileListsImpl(
{
did,
scrollElRef,
onScroll,
@ -37,16 +55,9 @@ export function ProfileLists({
enabled,
style,
testID,
}: {
did: string
scrollElRef?: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
testID?: string
}) {
},
ref,
) {
const pal = usePalette('default')
const theme = useTheme()
const {track} = useAnalytics()
@ -92,6 +103,17 @@ export function ProfileLists({
// events
// =
const queryClient = useQueryClient()
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerOffset})
queryClient.invalidateQueries({queryKey: RQKEY(did)})
}, [scrollElRef, queryClient, headerOffset, did])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
const onRefresh = React.useCallback(async () => {
track('Lists:onRefresh')
setIsPTRing(true)
@ -135,7 +157,10 @@ export function ProfileLists({
)
} else if (item === ERROR_ITEM) {
return (
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
<ErrorMessage
message={cleanError(error)}
onPressTryAgain={refetch}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
@ -195,7 +220,8 @@ export function ProfileLists({
/>
</View>
)
}
},
)
const styles = StyleSheet.create({
item: {

View File

@ -140,6 +140,12 @@ function ProfileScreenLoaded({
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
const postsSectionRef = React.useRef<SectionRef>(null)
const repliesSectionRef = React.useRef<SectionRef>(null)
const mediaSectionRef = React.useRef<SectionRef>(null)
const likesSectionRef = React.useRef<SectionRef>(null)
const feedsSectionRef = React.useRef<SectionRef>(null)
const listsSectionRef = React.useRef<SectionRef>(null)
useSetTitle(combinedDisplayName(profile))
@ -163,6 +169,23 @@ function ProfileScreenLoaded({
].filter(Boolean) as string[]
}, [showLikesTab, showFeedsTab, showListsTab])
let nextIndex = 0
const postsIndex = nextIndex++
const repliesIndex = nextIndex++
const mediaIndex = nextIndex++
let likesIndex: number | null = null
if (showLikesTab) {
likesIndex = nextIndex++
}
let feedsIndex: number | null = null
if (showFeedsTab) {
feedsIndex = nextIndex++
}
let listsIndex: number | null = null
if (showListsTab) {
listsIndex = nextIndex++
}
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
@ -202,6 +225,25 @@ function ProfileScreenLoaded({
[setCurrentPage],
)
const onCurrentPageSelected = React.useCallback(
(index: number) => {
if (index === postsIndex) {
postsSectionRef.current?.scrollToTop()
} else if (index === repliesIndex) {
repliesSectionRef.current?.scrollToTop()
} else if (index === mediaIndex) {
mediaSectionRef.current?.scrollToTop()
} else if (index === likesIndex) {
likesSectionRef.current?.scrollToTop()
} else if (index === feedsIndex) {
feedsSectionRef.current?.scrollToTop()
} else if (index === listsIndex) {
listsSectionRef.current?.scrollToTop()
}
},
[postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
)
// rendering
// =
@ -225,10 +267,11 @@ function ProfileScreenLoaded({
isHeaderReady={true}
items={sectionTitles}
onPageSelected={onPageSelected}
onCurrentPageSelected={onCurrentPageSelected}
renderHeader={renderHeader}>
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
<FeedSection
ref={null}
ref={postsSectionRef}
feed={`author|${profile.did}|posts_no_replies`}
onScroll={onScroll}
headerHeight={headerHeight}
@ -241,7 +284,7 @@ function ProfileScreenLoaded({
)}
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
<FeedSection
ref={null}
ref={repliesSectionRef}
feed={`author|${profile.did}|posts_with_replies`}
onScroll={onScroll}
headerHeight={headerHeight}
@ -254,7 +297,7 @@ function ProfileScreenLoaded({
)}
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
<FeedSection
ref={null}
ref={mediaSectionRef}
feed={`author|${profile.did}|posts_with_media`}
onScroll={onScroll}
headerHeight={headerHeight}
@ -274,7 +317,7 @@ function ProfileScreenLoaded({
scrollElRef,
}) => (
<FeedSection
ref={null}
ref={likesSectionRef}
feed={`likes|${profile.did}`}
onScroll={onScroll}
headerHeight={headerHeight}
@ -289,6 +332,7 @@ function ProfileScreenLoaded({
{showFeedsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedgens
ref={feedsSectionRef}
did={profile.did}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
@ -303,6 +347,7 @@ function ProfileScreenLoaded({
{showListsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
<ProfileLists
ref={listsSectionRef}
did={profile.did}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>

View File

@ -143,8 +143,7 @@ function ProfileListScreenLoaded({
(index: number) => {
if (index === 0) {
feedSectionRef.current?.scrollToTop()
}
if (index === 1) {
} else if (index === 1) {
aboutSectionRef.current?.scrollToTop()
}
},