diff options
-rw-r--r-- | src/view/com/feeds/ProfileFeedgens.tsx | 52 | ||||
-rw-r--r-- | src/view/com/lists/ProfileLists.tsx | 340 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 53 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 3 |
4 files changed, 271 insertions, 177 deletions
diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index a3c914595..77da2fd0b 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -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,25 +30,37 @@ const EMPTY = {_reactKey: '__empty__'} const ERROR_ITEM = {_reactKey: '__error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} -export function ProfileFeedgens({ - did, - scrollElRef, - onScroll, - scrollEventThrottle, - headerOffset, - enabled, - style, - testID, -}: { +interface SectionRef { + scrollToTop: () => void +} + +interface ProfileFeedgensProps { did: string - scrollElRef?: MutableRefObject<FlatList<any> | null> + 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, + scrollEventThrottle, + headerOffset, + enabled, + style, + testID, + }, + 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: { diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index 692891e47..63c23fcaf 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -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,174 +29,199 @@ const EMPTY = {_reactKey: '__empty__'} const ERROR_ITEM = {_reactKey: '__error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} -export function ProfileLists({ - did, - scrollElRef, - onScroll, - scrollEventThrottle, - headerOffset, - enabled, - style, - testID, -}: { +interface SectionRef { + scrollToTop: () => void +} + +interface ProfileListsProps { did: string - scrollElRef?: MutableRefObject<FlatList<any> | null> + scrollElRef: MutableRefObject<FlatList<any> | null> onScroll?: OnScrollHandler scrollEventThrottle?: number headerOffset: number enabled?: boolean style?: StyleProp<ViewStyle> testID?: string -}) { - const pal = usePalette('default') - const theme = useTheme() - const {track} = useAnalytics() - const [isPTRing, setIsPTRing] = React.useState(false) - const opts = React.useMemo(() => ({enabled}), [enabled]) - const { - data, - isFetching, - isFetched, - hasNextPage, - fetchNextPage, - isError, - error, - refetch, - } = useProfileListsQuery(did, opts) - const isEmpty = !isFetching && !data?.pages[0]?.lists.length - - const items = React.useMemo(() => { - let items: any[] = [] - if (isError && isEmpty) { - items = items.concat([ERROR_ITEM]) - } - if (!isFetched && isFetching) { - items = items.concat([LOADING]) - } else if (isEmpty) { - items = items.concat([EMPTY]) - } else if (data?.pages) { - for (const page of data?.pages) { - items = items.concat( - page.lists.map(l => ({ - ...l, - _reactKey: l.uri, - })), - ) +} + +export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( + function ProfileListsImpl( + { + did, + scrollElRef, + onScroll, + scrollEventThrottle, + headerOffset, + enabled, + style, + testID, + }, + ref, + ) { + const pal = usePalette('default') + const theme = useTheme() + const {track} = useAnalytics() + const [isPTRing, setIsPTRing] = React.useState(false) + const opts = React.useMemo(() => ({enabled}), [enabled]) + const { + data, + isFetching, + isFetched, + hasNextPage, + fetchNextPage, + isError, + error, + refetch, + } = useProfileListsQuery(did, opts) + const isEmpty = !isFetching && !data?.pages[0]?.lists.length + + const items = React.useMemo(() => { + let items: any[] = [] + if (isError && isEmpty) { + items = items.concat([ERROR_ITEM]) } - } - if (isError && !isEmpty) { - items = items.concat([LOAD_MORE_ERROR_ITEM]) - } - return items - }, [isError, isEmpty, isFetched, isFetching, data]) - - // events - // = - - const onRefresh = React.useCallback(async () => { - track('Lists:onRefresh') - setIsPTRing(true) - try { - await refetch() - } catch (err) { - logger.error('Failed to refresh lists', {error: err}) - } - setIsPTRing(false) - }, [refetch, track, setIsPTRing]) - - const onEndReached = React.useCallback(async () => { - if (isFetching || !hasNextPage || isError) return - - track('Lists:onEndReached') - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more lists', {error: err}) - } - }, [isFetching, hasNextPage, isError, fetchNextPage, track]) - - const onPressRetryLoadMore = React.useCallback(() => { - fetchNextPage() - }, [fetchNextPage]) - - // rendering - // = - - const renderItemInner = React.useCallback( - ({item}: {item: any}) => { - if (item === EMPTY) { - return ( - <View - testID="listsEmpty" - style={[{padding: 18, borderTopWidth: 1}, pal.border]}> - <Text style={pal.textLight}> - <Trans>You have no lists.</Trans> - </Text> - </View> - ) - } else if (item === ERROR_ITEM) { - return ( - <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> - ) - } else if (item === LOAD_MORE_ERROR_ITEM) { + if (!isFetched && isFetching) { + items = items.concat([LOADING]) + } else if (isEmpty) { + items = items.concat([EMPTY]) + } else if (data?.pages) { + for (const page of data?.pages) { + items = items.concat( + page.lists.map(l => ({ + ...l, + _reactKey: l.uri, + })), + ) + } + } + if (isError && !isEmpty) { + items = items.concat([LOAD_MORE_ERROR_ITEM]) + } + return items + }, [isError, isEmpty, isFetched, isFetching, data]) + + // 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) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh lists', {error: err}) + } + setIsPTRing(false) + }, [refetch, track, setIsPTRing]) + + const onEndReached = React.useCallback(async () => { + if (isFetching || !hasNextPage || isError) return + + track('Lists:onEndReached') + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more lists', {error: err}) + } + }, [isFetching, hasNextPage, isError, fetchNextPage, track]) + + const onPressRetryLoadMore = React.useCallback(() => { + fetchNextPage() + }, [fetchNextPage]) + + // rendering + // = + + const renderItemInner = React.useCallback( + ({item}: {item: any}) => { + if (item === EMPTY) { + return ( + <View + testID="listsEmpty" + style={[{padding: 18, borderTopWidth: 1}, pal.border]}> + <Text style={pal.textLight}> + <Trans>You have no lists.</Trans> + </Text> + </View> + ) + } else if (item === ERROR_ITEM) { + return ( + <ErrorMessage + message={cleanError(error)} + onPressTryAgain={refetch} + /> + ) + } else if (item === LOAD_MORE_ERROR_ITEM) { + return ( + <LoadMoreRetryBtn + label="There was an issue fetching your lists. Tap here to try again." + onPress={onPressRetryLoadMore} + /> + ) + } else if (item === LOADING) { + return ( + <View style={{padding: 20}}> + <ActivityIndicator /> + </View> + ) + } return ( - <LoadMoreRetryBtn - label="There was an issue fetching your lists. Tap here to try again." - onPress={onPressRetryLoadMore} + <ListCard + list={item} + testID={`list-${item.name}`} + style={styles.item} /> ) - } else if (item === LOADING) { - return ( - <View style={{padding: 20}}> - <ActivityIndicator /> - </View> - ) - } - return ( - <ListCard - list={item} - testID={`list-${item.name}`} - style={styles.item} + }, + [error, refetch, onPressRetryLoadMore, pal], + ) + + const scrollHandler = useAnimatedScrollHandler(onScroll || {}) + return ( + <View testID={testID} style={style}> + <FlatList + testID={testID ? `${testID}-flatlist` : undefined} + ref={scrollElRef} + data={items} + keyExtractor={(item: any) => item._reactKey} + renderItem={renderItemInner} + refreshControl={ + <RefreshControl + refreshing={isPTRing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + progressViewOffset={headerOffset} + /> + } + contentContainerStyle={{ + 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}} + // @ts-ignore our .web version only -prf + desktopFixedHeight + onEndReached={onEndReached} /> - ) - }, - [error, refetch, onPressRetryLoadMore, pal], - ) - - const scrollHandler = useAnimatedScrollHandler(onScroll || {}) - return ( - <View testID={testID} style={style}> - <FlatList - testID={testID ? `${testID}-flatlist` : undefined} - ref={scrollElRef} - data={items} - keyExtractor={(item: any) => item._reactKey} - renderItem={renderItemInner} - refreshControl={ - <RefreshControl - refreshing={isPTRing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - progressViewOffset={headerOffset} - /> - } - contentContainerStyle={{ - 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}} - // @ts-ignore our .web version only -prf - desktopFixedHeight - onEndReached={onEndReached} - /> - </View> - ) -} + </View> + ) + }, +) const styles = StyleSheet.create({ item: { diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 7b25e012c..1d03f2801 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -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> diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index b5a650643..a8c55250f 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -143,8 +143,7 @@ function ProfileListScreenLoaded({ (index: number) => { if (index === 0) { feedSectionRef.current?.scrollToTop() - } - if (index === 1) { + } else if (index === 1) { aboutSectionRef.current?.scrollToTop() } }, |