diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-09-10 18:49:04 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-09-10 18:49:04 +0300 |
commit | 5b8631d1887a08aa746a2b832688873e8ce3b1f2 (patch) | |
tree | 6b916e84a2905e5db59925c62628ffb9e2e889cf | |
parent | 5be753ecdfaf486c5d01d90c34008a1f5f275f00 (diff) | |
download | voidsky-5b8631d1887a08aa746a2b832688873e8ce3b1f2.tar.zst |
Fix profile lists/feeds/starterpacks tabs position issue (#8935)
-rw-r--r-- | src/components/StarterPack/ProfileStarterPacks.tsx | 44 | ||||
-rw-r--r-- | src/screens/Profile/Sections/Feed.tsx | 38 | ||||
-rw-r--r-- | src/screens/Profile/Sections/Labels.tsx | 1 | ||||
-rw-r--r-- | src/view/com/feeds/ProfileFeedgens.tsx | 51 | ||||
-rw-r--r-- | src/view/com/lists/ProfileLists.tsx | 349 |
5 files changed, 250 insertions, 233 deletions
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index bbe0bc52b..7252a1162 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -1,13 +1,9 @@ -import React, { - useCallback, - useEffect, - useImperativeHandle, - useState, -} from 'react' +import {useCallback, useEffect, useImperativeHandle, useState} from 'react' import { findNodeHandle, type ListRenderItemInfo, type StyleProp, + useWindowDimensions, View, type ViewStyle, } from 'react-native' @@ -42,6 +38,7 @@ interface SectionRef { } interface ProfileFeedgensProps { + ref?: React.Ref<SectionRef> scrollElRef: ListRef did: string headerOffset: number @@ -56,24 +53,20 @@ function keyExtractor(item: AppBskyGraphDefs.StarterPackView) { return item.uri } -export const ProfileStarterPacks = React.forwardRef< - SectionRef, - ProfileFeedgensProps ->(function ProfileFeedgensImpl( - { - scrollElRef, - did, - headerOffset, - enabled, - style, - testID, - setScrollViewTag, - isMe, - }, +export function ProfileStarterPacks({ ref, -) { + scrollElRef, + did, + headerOffset, + enabled, + style, + testID, + setScrollViewTag, + isMe, +}: ProfileFeedgensProps) { const t = useTheme() const bottomBarOffset = useBottomBarOffset(100) + const {height} = useWindowDimensions() const [isPTRing, setIsPTRing] = useState(false) const { data, @@ -101,7 +94,7 @@ export const ProfileStarterPacks = React.forwardRef< setIsPTRing(false) }, [refetch, setIsPTRing]) - const onEndReached = React.useCallback(async () => { + const onEndReached = useCallback(async () => { if (isFetchingNextPage || !hasNextPage || isError) return try { await fetchNextPage() @@ -144,7 +137,10 @@ export const ProfileStarterPacks = React.forwardRef< refreshing={isPTRing} headerOffset={headerOffset} progressViewOffset={ios(0)} - contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} + contentContainerStyle={{ + minHeight: height + headerOffset, + paddingBottom: bottomBarOffset, + }} removeClippedSubviews={true} desktopFixedHeight onEndReached={onEndReached} @@ -158,7 +154,7 @@ export const ProfileStarterPacks = React.forwardRef< /> </View> ) -}) +} function CreateAnother() { const {_} = useLingui() diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index e0c3e221f..2f54eda7b 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback, useEffect, useImperativeHandle, useState} from 'react' import {findNodeHandle, View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -18,6 +18,7 @@ import {Text} from '#/components/Typography' import {type SectionRef} from './types' interface FeedSectionProps { + ref?: React.Ref<SectionRef> feed: FeedDescriptor headerHeight: number isFocused: boolean @@ -25,31 +26,26 @@ interface FeedSectionProps { ignoreFilterFor?: string setScrollViewTag: (tag: number | null) => void } -export const ProfileFeedSection = React.forwardRef< - SectionRef, - FeedSectionProps ->(function FeedSectionImpl( - { - feed, - headerHeight, - isFocused, - scrollElRef, - ignoreFilterFor, - setScrollViewTag, - }, +export function ProfileFeedSection({ ref, -) { + feed, + headerHeight, + isFocused, + scrollElRef, + ignoreFilterFor, + setScrollViewTag, +}: FeedSectionProps) { const {_} = useLingui() const queryClient = useQueryClient() - const [hasNew, setHasNew] = React.useState(false) - const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const [hasNew, setHasNew] = useState(false) + const [isScrolledDown, setIsScrolledDown] = useState(false) const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads') const isVideoFeed = isNative && feed.endsWith('posts_with_video') const adjustedInitialNumToRender = useInitialNumToRender({ screenHeightOffset: headerHeight, }) - const onScrollToTop = React.useCallback(() => { + const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ animated: isNative, offset: -headerHeight, @@ -58,15 +54,15 @@ export const ProfileFeedSection = React.forwardRef< setHasNew(false) }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) - React.useImperativeHandle(ref, () => ({ + useImperativeHandle(ref, () => ({ scrollToTop: onScrollToTop, })) - const renderPostsEmpty = React.useCallback(() => { + const renderPostsEmpty = useCallback(() => { return <EmptyState icon="growth" message={_(msg`No posts yet.`)} /> }, [_]) - React.useEffect(() => { + useEffect(() => { if (isIOS && isFocused && scrollElRef.current) { const nativeTag = findNodeHandle(scrollElRef.current) setScrollViewTag(nativeTag) @@ -101,7 +97,7 @@ export const ProfileFeedSection = React.forwardRef< )} </View> ) -}) +} function ProfileEndOfFeed() { const t = useTheme() diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index 669a5dbcc..120fb8225 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -33,6 +33,7 @@ interface LabelsSectionProps { isFocused: boolean setScrollViewTag: (tag: number | null) => void } + export function ProfileLabelsSection({ ref, isLabelerLoading, diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 5ba17e426..f53a5f5df 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -1,8 +1,15 @@ -import React from 'react' +import { + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react' import { findNodeHandle, type ListRenderItemInfo, type StyleProp, + useWindowDimensions, View, type ViewStyle, } from 'react-native' @@ -34,6 +41,7 @@ interface SectionRef { } interface ProfileFeedgensProps { + ref?: React.Ref<SectionRef> did: string scrollElRef: ListRef headerOffset: number @@ -43,17 +51,21 @@ interface ProfileFeedgensProps { setScrollViewTag: (tag: number | null) => void } -export const ProfileFeedgens = React.forwardRef< - SectionRef, - ProfileFeedgensProps ->(function ProfileFeedgensImpl( - {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, +export function ProfileFeedgens({ ref, -) { + did, + scrollElRef, + headerOffset, + enabled, + style, + testID, + setScrollViewTag, +}: ProfileFeedgensProps) { const {_} = useLingui() const t = useTheme() - const [isPTRing, setIsPTRing] = React.useState(false) - const opts = React.useMemo(() => ({enabled}), [enabled]) + const [isPTRing, setIsPTRing] = useState(false) + const {height} = useWindowDimensions() + const opts = useMemo(() => ({enabled}), [enabled]) const { data, isPending, @@ -67,7 +79,7 @@ export const ProfileFeedgens = React.forwardRef< const isEmpty = !isPending && !data?.pages[0]?.feeds.length const {data: preferences} = usePreferencesQuery() - const items = React.useMemo(() => { + const items = useMemo(() => { let items: any[] = [] if (isError && isEmpty) { items = items.concat([ERROR_ITEM]) @@ -91,7 +103,7 @@ export const ProfileFeedgens = React.forwardRef< const queryClient = useQueryClient() - const onScrollToTop = React.useCallback(() => { + const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ animated: isNative, offset: -headerOffset, @@ -99,11 +111,11 @@ export const ProfileFeedgens = React.forwardRef< queryClient.invalidateQueries({queryKey: RQKEY(did)}) }, [scrollElRef, queryClient, headerOffset, did]) - React.useImperativeHandle(ref, () => ({ + useImperativeHandle(ref, () => ({ scrollToTop: onScrollToTop, })) - const onRefresh = React.useCallback(async () => { + const onRefresh = useCallback(async () => { setIsPTRing(true) try { await refetch() @@ -113,7 +125,7 @@ export const ProfileFeedgens = React.forwardRef< setIsPTRing(false) }, [refetch, setIsPTRing]) - const onEndReached = React.useCallback(async () => { + const onEndReached = useCallback(async () => { if (isFetchingNextPage || !hasNextPage || isError) return try { @@ -123,14 +135,14 @@ export const ProfileFeedgens = React.forwardRef< } }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) - const onPressRetryLoadMore = React.useCallback(() => { + const onPressRetryLoadMore = useCallback(() => { fetchNextPage() }, [fetchNextPage]) // rendering // = - const renderItem = React.useCallback( + const renderItem = useCallback( ({item, index}: ListRenderItemInfo<any>) => { if (item === EMPTY) { return ( @@ -174,14 +186,14 @@ export const ProfileFeedgens = React.forwardRef< [_, t, error, refetch, onPressRetryLoadMore, preferences], ) - React.useEffect(() => { + useEffect(() => { if (isIOS && enabled && scrollElRef.current) { const nativeTag = findNodeHandle(scrollElRef.current) setScrollViewTag(nativeTag) } }, [enabled, scrollElRef, setScrollViewTag]) - const ProfileFeedgensFooter = React.useCallback(() => { + const ProfileFeedgensFooter = useCallback(() => { if (isEmpty) return null return ( <ListFooter @@ -217,10 +229,11 @@ export const ProfileFeedgens = React.forwardRef< removeClippedSubviews={true} desktopFixedHeight onEndReached={onEndReached} + contentContainerStyle={{minHeight: height + headerOffset}} /> </View> ) -}) +} function keyExtractor(item: any) { return item._reactKey || item.uri diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index 8fea51081..ce51cb337 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -1,8 +1,15 @@ -import React from 'react' +import { + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react' import { findNodeHandle, type ListRenderItemInfo, type StyleProp, + useWindowDimensions, View, type ViewStyle, } from 'react-native' @@ -33,6 +40,7 @@ interface SectionRef { } interface ProfileListsProps { + ref?: React.Ref<SectionRef> did: string scrollElRef: ListRef headerOffset: number @@ -42,182 +50,185 @@ interface ProfileListsProps { setScrollViewTag: (tag: number | null) => void } -export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( - function ProfileListsImpl( - {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, - ref, - ) { - const t = useTheme() - const {_} = useLingui() - const [isPTRing, setIsPTRing] = React.useState(false) - const opts = React.useMemo(() => ({enabled}), [enabled]) - const { - data, - isPending, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - isError, - error, - refetch, - } = useProfileListsQuery(did, opts) - const isEmpty = !isPending && !data?.pages[0]?.lists.length - - const items = React.useMemo(() => { - let items: any[] = [] - if (isError && isEmpty) { - items = items.concat([ERROR_ITEM]) - } - if (isPending) { - 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) - } - } - if (isError && !isEmpty) { - items = items.concat([LOAD_MORE_ERROR_ITEM]) - } - return items - }, [isError, isEmpty, isPending, data]) - - // events - // = - - const queryClient = useQueryClient() - - const onScrollToTop = React.useCallback(() => { - scrollElRef.current?.scrollToOffset({ - animated: isNative, - offset: -headerOffset, - }) - queryClient.invalidateQueries({queryKey: RQKEY(did)}) - }, [scrollElRef, queryClient, headerOffset, did]) - - React.useImperativeHandle(ref, () => ({ - scrollToTop: onScrollToTop, - })) - - const onRefresh = React.useCallback(async () => { - setIsPTRing(true) - try { - await refetch() - } catch (err) { - logger.error('Failed to refresh lists', {message: err}) +export function ProfileLists({ + ref, + did, + scrollElRef, + headerOffset, + enabled, + style, + testID, + setScrollViewTag, +}: ProfileListsProps) { + const t = useTheme() + const {_} = useLingui() + const {height} = useWindowDimensions() + const [isPTRing, setIsPTRing] = useState(false) + const opts = useMemo(() => ({enabled}), [enabled]) + const { + data, + isPending, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + isError, + error, + refetch, + } = useProfileListsQuery(did, opts) + const isEmpty = !isPending && !data?.pages[0]?.lists.length + + const items = useMemo(() => { + let items: any[] = [] + if (isError && isEmpty) { + items = items.concat([ERROR_ITEM]) + } + if (isPending) { + 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) } - setIsPTRing(false) - }, [refetch, setIsPTRing]) - - const onEndReached = React.useCallback(async () => { - if (isFetchingNextPage || !hasNextPage || isError) return - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more lists', {message: err}) - } - }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) - - const onPressRetryLoadMore = React.useCallback(() => { - fetchNextPage() - }, [fetchNextPage]) - - // rendering - // = - - const renderItemInner = React.useCallback( - ({item, index}: ListRenderItemInfo<any>) => { - if (item === EMPTY) { - return ( - <EmptyState - icon="list-ul" - message={_(msg`You have no lists.`)} - testID="listsEmpty" - /> - ) - } else if (item === ERROR_ITEM) { - return ( - <ErrorMessage - message={cleanError(error)} - onPressTryAgain={refetch} - /> - ) - } else if (item === LOAD_MORE_ERROR_ITEM) { - return ( - <LoadMoreRetryBtn - label={_( - msg`There was an issue fetching your lists. Tap here to try again.`, - )} - onPress={onPressRetryLoadMore} - /> - ) - } else if (item === LOADING) { - return <FeedLoadingPlaceholder /> - } + } + if (isError && !isEmpty) { + items = items.concat([LOAD_MORE_ERROR_ITEM]) + } + return items + }, [isError, isEmpty, isPending, data]) + + // events + // = + + const queryClient = useQueryClient() + + const onScrollToTop = useCallback(() => { + scrollElRef.current?.scrollToOffset({ + animated: isNative, + offset: -headerOffset, + }) + queryClient.invalidateQueries({queryKey: RQKEY(did)}) + }, [scrollElRef, queryClient, headerOffset, did]) + + useImperativeHandle(ref, () => ({ + scrollToTop: onScrollToTop, + })) + + const onRefresh = useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh lists', {message: err}) + } + setIsPTRing(false) + }, [refetch, setIsPTRing]) + + const onEndReached = useCallback(async () => { + if (isFetchingNextPage || !hasNextPage || isError) return + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more lists', {message: err}) + } + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) + + const onPressRetryLoadMore = useCallback(() => { + fetchNextPage() + }, [fetchNextPage]) + + // rendering + // = + + const renderItemInner = useCallback( + ({item, index}: ListRenderItemInfo<any>) => { + if (item === EMPTY) { return ( - <View - style={[ - (index !== 0 || isWeb) && a.border_t, - t.atoms.border_contrast_low, - a.px_lg, - a.py_lg, - ]}> - <ListCard.Default view={item} /> - </View> + <EmptyState + icon="list-ul" + message={_(msg`You have no lists.`)} + testID="listsEmpty" + /> ) - }, - [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], - ) - - React.useEffect(() => { - if (isIOS && enabled && scrollElRef.current) { - const nativeTag = findNodeHandle(scrollElRef.current) - setScrollViewTag(nativeTag) + } else if (item === ERROR_ITEM) { + return ( + <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> + ) + } else if (item === LOAD_MORE_ERROR_ITEM) { + return ( + <LoadMoreRetryBtn + label={_( + msg`There was an issue fetching your lists. Tap here to try again.`, + )} + onPress={onPressRetryLoadMore} + /> + ) + } else if (item === LOADING) { + return <FeedLoadingPlaceholder /> } - }, [enabled, scrollElRef, setScrollViewTag]) - - const ProfileListsFooter = React.useCallback(() => { - if (isEmpty) return null return ( - <ListFooter - hasNextPage={hasNextPage} - isFetchingNextPage={isFetchingNextPage} - onRetry={fetchNextPage} - error={cleanError(error)} - height={180 + headerOffset} - /> + <View + style={[ + (index !== 0 || isWeb) && a.border_t, + t.atoms.border_contrast_low, + a.px_lg, + a.py_lg, + ]}> + <ListCard.Default view={item} /> + </View> ) - }, [ - hasNextPage, - error, - isFetchingNextPage, - headerOffset, - fetchNextPage, - isEmpty, - ]) - + }, + [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], + ) + + useEffect(() => { + if (isIOS && enabled && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [enabled, scrollElRef, setScrollViewTag]) + + const ProfileListsFooter = useCallback(() => { + if (isEmpty) return null return ( - <View testID={testID} style={style}> - <List - testID={testID ? `${testID}-flatlist` : undefined} - ref={scrollElRef} - data={items} - keyExtractor={keyExtractor} - renderItem={renderItemInner} - ListFooterComponent={ProfileListsFooter} - refreshing={isPTRing} - onRefresh={onRefresh} - headerOffset={headerOffset} - progressViewOffset={ios(0)} - removeClippedSubviews={true} - desktopFixedHeight - onEndReached={onEndReached} - /> - </View> + <ListFooter + hasNextPage={hasNextPage} + isFetchingNextPage={isFetchingNextPage} + onRetry={fetchNextPage} + error={cleanError(error)} + height={180 + headerOffset} + /> ) - }, -) + }, [ + hasNextPage, + error, + isFetchingNextPage, + headerOffset, + fetchNextPage, + isEmpty, + ]) + + return ( + <View testID={testID} style={style}> + <List + testID={testID ? `${testID}-flatlist` : undefined} + ref={scrollElRef} + data={items} + keyExtractor={keyExtractor} + renderItem={renderItemInner} + ListFooterComponent={ProfileListsFooter} + refreshing={isPTRing} + onRefresh={onRefresh} + headerOffset={headerOffset} + progressViewOffset={ios(0)} + removeClippedSubviews={true} + desktopFixedHeight + onEndReached={onEndReached} + contentContainerStyle={{minHeight: height + headerOffset}} + /> + </View> + ) +} function keyExtractor(item: any) { return item._reactKey || item.uri |