Bluesky app fork with some witchin' additions 💫

Fix profile lists/feeds/starterpacks tabs position issue (#8935)

authored by samuel.fm and committed by

GitHub 5b8631d1 5be753ec

+239 -222
+20 -24
src/components/StarterPack/ProfileStarterPacks.tsx
··· 1 - import React, { 2 - useCallback, 3 - useEffect, 4 - useImperativeHandle, 5 - useState, 6 - } from 'react' 1 + import {useCallback, useEffect, useImperativeHandle, useState} from 'react' 7 2 import { 8 3 findNodeHandle, 9 4 type ListRenderItemInfo, 10 5 type StyleProp, 6 + useWindowDimensions, 11 7 View, 12 8 type ViewStyle, 13 9 } from 'react-native' ··· 42 38 } 43 39 44 40 interface ProfileFeedgensProps { 41 + ref?: React.Ref<SectionRef> 45 42 scrollElRef: ListRef 46 43 did: string 47 44 headerOffset: number ··· 56 53 return item.uri 57 54 } 58 55 59 - export const ProfileStarterPacks = React.forwardRef< 60 - SectionRef, 61 - ProfileFeedgensProps 62 - >(function ProfileFeedgensImpl( 63 - { 64 - scrollElRef, 65 - did, 66 - headerOffset, 67 - enabled, 68 - style, 69 - testID, 70 - setScrollViewTag, 71 - isMe, 72 - }, 56 + export function ProfileStarterPacks({ 73 57 ref, 74 - ) { 58 + scrollElRef, 59 + did, 60 + headerOffset, 61 + enabled, 62 + style, 63 + testID, 64 + setScrollViewTag, 65 + isMe, 66 + }: ProfileFeedgensProps) { 75 67 const t = useTheme() 76 68 const bottomBarOffset = useBottomBarOffset(100) 69 + const {height} = useWindowDimensions() 77 70 const [isPTRing, setIsPTRing] = useState(false) 78 71 const { 79 72 data, ··· 101 94 setIsPTRing(false) 102 95 }, [refetch, setIsPTRing]) 103 96 104 - const onEndReached = React.useCallback(async () => { 97 + const onEndReached = useCallback(async () => { 105 98 if (isFetchingNextPage || !hasNextPage || isError) return 106 99 try { 107 100 await fetchNextPage() ··· 144 137 refreshing={isPTRing} 145 138 headerOffset={headerOffset} 146 139 progressViewOffset={ios(0)} 147 - contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} 140 + contentContainerStyle={{ 141 + minHeight: height + headerOffset, 142 + paddingBottom: bottomBarOffset, 143 + }} 148 144 removeClippedSubviews={true} 149 145 desktopFixedHeight 150 146 onEndReached={onEndReached} ··· 158 154 /> 159 155 </View> 160 156 ) 161 - }) 157 + } 162 158 163 159 function CreateAnother() { 164 160 const {_} = useLingui()
+17 -21
src/screens/Profile/Sections/Feed.tsx
··· 1 - import React from 'react' 1 + import {useCallback, useEffect, useImperativeHandle, useState} from 'react' 2 2 import {findNodeHandle, View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' ··· 18 18 import {type SectionRef} from './types' 19 19 20 20 interface FeedSectionProps { 21 + ref?: React.Ref<SectionRef> 21 22 feed: FeedDescriptor 22 23 headerHeight: number 23 24 isFocused: boolean ··· 25 26 ignoreFilterFor?: string 26 27 setScrollViewTag: (tag: number | null) => void 27 28 } 28 - export const ProfileFeedSection = React.forwardRef< 29 - SectionRef, 30 - FeedSectionProps 31 - >(function FeedSectionImpl( 32 - { 33 - feed, 34 - headerHeight, 35 - isFocused, 36 - scrollElRef, 37 - ignoreFilterFor, 38 - setScrollViewTag, 39 - }, 29 + export function ProfileFeedSection({ 40 30 ref, 41 - ) { 31 + feed, 32 + headerHeight, 33 + isFocused, 34 + scrollElRef, 35 + ignoreFilterFor, 36 + setScrollViewTag, 37 + }: FeedSectionProps) { 42 38 const {_} = useLingui() 43 39 const queryClient = useQueryClient() 44 - const [hasNew, setHasNew] = React.useState(false) 45 - const [isScrolledDown, setIsScrolledDown] = React.useState(false) 40 + const [hasNew, setHasNew] = useState(false) 41 + const [isScrolledDown, setIsScrolledDown] = useState(false) 46 42 const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads') 47 43 const isVideoFeed = isNative && feed.endsWith('posts_with_video') 48 44 const adjustedInitialNumToRender = useInitialNumToRender({ 49 45 screenHeightOffset: headerHeight, 50 46 }) 51 47 52 - const onScrollToTop = React.useCallback(() => { 48 + const onScrollToTop = useCallback(() => { 53 49 scrollElRef.current?.scrollToOffset({ 54 50 animated: isNative, 55 51 offset: -headerHeight, ··· 58 54 setHasNew(false) 59 55 }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) 60 56 61 - React.useImperativeHandle(ref, () => ({ 57 + useImperativeHandle(ref, () => ({ 62 58 scrollToTop: onScrollToTop, 63 59 })) 64 60 65 - const renderPostsEmpty = React.useCallback(() => { 61 + const renderPostsEmpty = useCallback(() => { 66 62 return <EmptyState icon="growth" message={_(msg`No posts yet.`)} /> 67 63 }, [_]) 68 64 69 - React.useEffect(() => { 65 + useEffect(() => { 70 66 if (isIOS && isFocused && scrollElRef.current) { 71 67 const nativeTag = findNodeHandle(scrollElRef.current) 72 68 setScrollViewTag(nativeTag) ··· 101 97 )} 102 98 </View> 103 99 ) 104 - }) 100 + } 105 101 106 102 function ProfileEndOfFeed() { 107 103 const t = useTheme()
+1
src/screens/Profile/Sections/Labels.tsx
··· 33 33 isFocused: boolean 34 34 setScrollViewTag: (tag: number | null) => void 35 35 } 36 + 36 37 export function ProfileLabelsSection({ 37 38 ref, 38 39 isLabelerLoading,
+32 -19
src/view/com/feeds/ProfileFeedgens.tsx
··· 1 - import React from 'react' 1 + import { 2 + useCallback, 3 + useEffect, 4 + useImperativeHandle, 5 + useMemo, 6 + useState, 7 + } from 'react' 2 8 import { 3 9 findNodeHandle, 4 10 type ListRenderItemInfo, 5 11 type StyleProp, 12 + useWindowDimensions, 6 13 View, 7 14 type ViewStyle, 8 15 } from 'react-native' ··· 34 41 } 35 42 36 43 interface ProfileFeedgensProps { 44 + ref?: React.Ref<SectionRef> 37 45 did: string 38 46 scrollElRef: ListRef 39 47 headerOffset: number ··· 43 51 setScrollViewTag: (tag: number | null) => void 44 52 } 45 53 46 - export const ProfileFeedgens = React.forwardRef< 47 - SectionRef, 48 - ProfileFeedgensProps 49 - >(function ProfileFeedgensImpl( 50 - {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, 54 + export function ProfileFeedgens({ 51 55 ref, 52 - ) { 56 + did, 57 + scrollElRef, 58 + headerOffset, 59 + enabled, 60 + style, 61 + testID, 62 + setScrollViewTag, 63 + }: ProfileFeedgensProps) { 53 64 const {_} = useLingui() 54 65 const t = useTheme() 55 - const [isPTRing, setIsPTRing] = React.useState(false) 56 - const opts = React.useMemo(() => ({enabled}), [enabled]) 66 + const [isPTRing, setIsPTRing] = useState(false) 67 + const {height} = useWindowDimensions() 68 + const opts = useMemo(() => ({enabled}), [enabled]) 57 69 const { 58 70 data, 59 71 isPending, ··· 67 79 const isEmpty = !isPending && !data?.pages[0]?.feeds.length 68 80 const {data: preferences} = usePreferencesQuery() 69 81 70 - const items = React.useMemo(() => { 82 + const items = useMemo(() => { 71 83 let items: any[] = [] 72 84 if (isError && isEmpty) { 73 85 items = items.concat([ERROR_ITEM]) ··· 91 103 92 104 const queryClient = useQueryClient() 93 105 94 - const onScrollToTop = React.useCallback(() => { 106 + const onScrollToTop = useCallback(() => { 95 107 scrollElRef.current?.scrollToOffset({ 96 108 animated: isNative, 97 109 offset: -headerOffset, ··· 99 111 queryClient.invalidateQueries({queryKey: RQKEY(did)}) 100 112 }, [scrollElRef, queryClient, headerOffset, did]) 101 113 102 - React.useImperativeHandle(ref, () => ({ 114 + useImperativeHandle(ref, () => ({ 103 115 scrollToTop: onScrollToTop, 104 116 })) 105 117 106 - const onRefresh = React.useCallback(async () => { 118 + const onRefresh = useCallback(async () => { 107 119 setIsPTRing(true) 108 120 try { 109 121 await refetch() ··· 113 125 setIsPTRing(false) 114 126 }, [refetch, setIsPTRing]) 115 127 116 - const onEndReached = React.useCallback(async () => { 128 + const onEndReached = useCallback(async () => { 117 129 if (isFetchingNextPage || !hasNextPage || isError) return 118 130 119 131 try { ··· 123 135 } 124 136 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 125 137 126 - const onPressRetryLoadMore = React.useCallback(() => { 138 + const onPressRetryLoadMore = useCallback(() => { 127 139 fetchNextPage() 128 140 }, [fetchNextPage]) 129 141 130 142 // rendering 131 143 // = 132 144 133 - const renderItem = React.useCallback( 145 + const renderItem = useCallback( 134 146 ({item, index}: ListRenderItemInfo<any>) => { 135 147 if (item === EMPTY) { 136 148 return ( ··· 174 186 [_, t, error, refetch, onPressRetryLoadMore, preferences], 175 187 ) 176 188 177 - React.useEffect(() => { 189 + useEffect(() => { 178 190 if (isIOS && enabled && scrollElRef.current) { 179 191 const nativeTag = findNodeHandle(scrollElRef.current) 180 192 setScrollViewTag(nativeTag) 181 193 } 182 194 }, [enabled, scrollElRef, setScrollViewTag]) 183 195 184 - const ProfileFeedgensFooter = React.useCallback(() => { 196 + const ProfileFeedgensFooter = useCallback(() => { 185 197 if (isEmpty) return null 186 198 return ( 187 199 <ListFooter ··· 217 229 removeClippedSubviews={true} 218 230 desktopFixedHeight 219 231 onEndReached={onEndReached} 232 + contentContainerStyle={{minHeight: height + headerOffset}} 220 233 /> 221 234 </View> 222 235 ) 223 - }) 236 + } 224 237 225 238 function keyExtractor(item: any) { 226 239 return item._reactKey || item.uri
+169 -158
src/view/com/lists/ProfileLists.tsx
··· 1 - import React from 'react' 1 + import { 2 + useCallback, 3 + useEffect, 4 + useImperativeHandle, 5 + useMemo, 6 + useState, 7 + } from 'react' 2 8 import { 3 9 findNodeHandle, 4 10 type ListRenderItemInfo, 5 11 type StyleProp, 12 + useWindowDimensions, 6 13 View, 7 14 type ViewStyle, 8 15 } from 'react-native' ··· 33 40 } 34 41 35 42 interface ProfileListsProps { 43 + ref?: React.Ref<SectionRef> 36 44 did: string 37 45 scrollElRef: ListRef 38 46 headerOffset: number ··· 42 50 setScrollViewTag: (tag: number | null) => void 43 51 } 44 52 45 - export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( 46 - function ProfileListsImpl( 47 - {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, 48 - ref, 49 - ) { 50 - const t = useTheme() 51 - const {_} = useLingui() 52 - const [isPTRing, setIsPTRing] = React.useState(false) 53 - const opts = React.useMemo(() => ({enabled}), [enabled]) 54 - const { 55 - data, 56 - isPending, 57 - hasNextPage, 58 - fetchNextPage, 59 - isFetchingNextPage, 60 - isError, 61 - error, 62 - refetch, 63 - } = useProfileListsQuery(did, opts) 64 - const isEmpty = !isPending && !data?.pages[0]?.lists.length 53 + export function ProfileLists({ 54 + ref, 55 + did, 56 + scrollElRef, 57 + headerOffset, 58 + enabled, 59 + style, 60 + testID, 61 + setScrollViewTag, 62 + }: ProfileListsProps) { 63 + const t = useTheme() 64 + const {_} = useLingui() 65 + const {height} = useWindowDimensions() 66 + const [isPTRing, setIsPTRing] = useState(false) 67 + const opts = useMemo(() => ({enabled}), [enabled]) 68 + const { 69 + data, 70 + isPending, 71 + hasNextPage, 72 + fetchNextPage, 73 + isFetchingNextPage, 74 + isError, 75 + error, 76 + refetch, 77 + } = useProfileListsQuery(did, opts) 78 + const isEmpty = !isPending && !data?.pages[0]?.lists.length 65 79 66 - const items = React.useMemo(() => { 67 - let items: any[] = [] 68 - if (isError && isEmpty) { 69 - items = items.concat([ERROR_ITEM]) 70 - } 71 - if (isPending) { 72 - items = items.concat([LOADING]) 73 - } else if (isEmpty) { 74 - items = items.concat([EMPTY]) 75 - } else if (data?.pages) { 76 - for (const page of data?.pages) { 77 - items = items.concat(page.lists) 78 - } 79 - } 80 - if (isError && !isEmpty) { 81 - items = items.concat([LOAD_MORE_ERROR_ITEM]) 80 + const items = useMemo(() => { 81 + let items: any[] = [] 82 + if (isError && isEmpty) { 83 + items = items.concat([ERROR_ITEM]) 84 + } 85 + if (isPending) { 86 + items = items.concat([LOADING]) 87 + } else if (isEmpty) { 88 + items = items.concat([EMPTY]) 89 + } else if (data?.pages) { 90 + for (const page of data?.pages) { 91 + items = items.concat(page.lists) 82 92 } 83 - return items 84 - }, [isError, isEmpty, isPending, data]) 93 + } 94 + if (isError && !isEmpty) { 95 + items = items.concat([LOAD_MORE_ERROR_ITEM]) 96 + } 97 + return items 98 + }, [isError, isEmpty, isPending, data]) 85 99 86 - // events 87 - // = 100 + // events 101 + // = 88 102 89 - const queryClient = useQueryClient() 103 + const queryClient = useQueryClient() 90 104 91 - const onScrollToTop = React.useCallback(() => { 92 - scrollElRef.current?.scrollToOffset({ 93 - animated: isNative, 94 - offset: -headerOffset, 95 - }) 96 - queryClient.invalidateQueries({queryKey: RQKEY(did)}) 97 - }, [scrollElRef, queryClient, headerOffset, did]) 105 + const onScrollToTop = useCallback(() => { 106 + scrollElRef.current?.scrollToOffset({ 107 + animated: isNative, 108 + offset: -headerOffset, 109 + }) 110 + queryClient.invalidateQueries({queryKey: RQKEY(did)}) 111 + }, [scrollElRef, queryClient, headerOffset, did]) 98 112 99 - React.useImperativeHandle(ref, () => ({ 100 - scrollToTop: onScrollToTop, 101 - })) 113 + useImperativeHandle(ref, () => ({ 114 + scrollToTop: onScrollToTop, 115 + })) 102 116 103 - const onRefresh = React.useCallback(async () => { 104 - setIsPTRing(true) 105 - try { 106 - await refetch() 107 - } catch (err) { 108 - logger.error('Failed to refresh lists', {message: err}) 109 - } 110 - setIsPTRing(false) 111 - }, [refetch, setIsPTRing]) 117 + const onRefresh = useCallback(async () => { 118 + setIsPTRing(true) 119 + try { 120 + await refetch() 121 + } catch (err) { 122 + logger.error('Failed to refresh lists', {message: err}) 123 + } 124 + setIsPTRing(false) 125 + }, [refetch, setIsPTRing]) 112 126 113 - const onEndReached = React.useCallback(async () => { 114 - if (isFetchingNextPage || !hasNextPage || isError) return 115 - try { 116 - await fetchNextPage() 117 - } catch (err) { 118 - logger.error('Failed to load more lists', {message: err}) 119 - } 120 - }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 127 + const onEndReached = useCallback(async () => { 128 + if (isFetchingNextPage || !hasNextPage || isError) return 129 + try { 130 + await fetchNextPage() 131 + } catch (err) { 132 + logger.error('Failed to load more lists', {message: err}) 133 + } 134 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 121 135 122 - const onPressRetryLoadMore = React.useCallback(() => { 123 - fetchNextPage() 124 - }, [fetchNextPage]) 136 + const onPressRetryLoadMore = useCallback(() => { 137 + fetchNextPage() 138 + }, [fetchNextPage]) 125 139 126 - // rendering 127 - // = 140 + // rendering 141 + // = 128 142 129 - const renderItemInner = React.useCallback( 130 - ({item, index}: ListRenderItemInfo<any>) => { 131 - if (item === EMPTY) { 132 - return ( 133 - <EmptyState 134 - icon="list-ul" 135 - message={_(msg`You have no lists.`)} 136 - testID="listsEmpty" 137 - /> 138 - ) 139 - } else if (item === ERROR_ITEM) { 140 - return ( 141 - <ErrorMessage 142 - message={cleanError(error)} 143 - onPressTryAgain={refetch} 144 - /> 145 - ) 146 - } else if (item === LOAD_MORE_ERROR_ITEM) { 147 - return ( 148 - <LoadMoreRetryBtn 149 - label={_( 150 - msg`There was an issue fetching your lists. Tap here to try again.`, 151 - )} 152 - onPress={onPressRetryLoadMore} 153 - /> 154 - ) 155 - } else if (item === LOADING) { 156 - return <FeedLoadingPlaceholder /> 157 - } 143 + const renderItemInner = useCallback( 144 + ({item, index}: ListRenderItemInfo<any>) => { 145 + if (item === EMPTY) { 158 146 return ( 159 - <View 160 - style={[ 161 - (index !== 0 || isWeb) && a.border_t, 162 - t.atoms.border_contrast_low, 163 - a.px_lg, 164 - a.py_lg, 165 - ]}> 166 - <ListCard.Default view={item} /> 167 - </View> 147 + <EmptyState 148 + icon="list-ul" 149 + message={_(msg`You have no lists.`)} 150 + testID="listsEmpty" 151 + /> 168 152 ) 169 - }, 170 - [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], 171 - ) 172 - 173 - React.useEffect(() => { 174 - if (isIOS && enabled && scrollElRef.current) { 175 - const nativeTag = findNodeHandle(scrollElRef.current) 176 - setScrollViewTag(nativeTag) 153 + } else if (item === ERROR_ITEM) { 154 + return ( 155 + <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 156 + ) 157 + } else if (item === LOAD_MORE_ERROR_ITEM) { 158 + return ( 159 + <LoadMoreRetryBtn 160 + label={_( 161 + msg`There was an issue fetching your lists. Tap here to try again.`, 162 + )} 163 + onPress={onPressRetryLoadMore} 164 + /> 165 + ) 166 + } else if (item === LOADING) { 167 + return <FeedLoadingPlaceholder /> 177 168 } 178 - }, [enabled, scrollElRef, setScrollViewTag]) 179 - 180 - const ProfileListsFooter = React.useCallback(() => { 181 - if (isEmpty) return null 182 169 return ( 183 - <ListFooter 184 - hasNextPage={hasNextPage} 185 - isFetchingNextPage={isFetchingNextPage} 186 - onRetry={fetchNextPage} 187 - error={cleanError(error)} 188 - height={180 + headerOffset} 189 - /> 170 + <View 171 + style={[ 172 + (index !== 0 || isWeb) && a.border_t, 173 + t.atoms.border_contrast_low, 174 + a.px_lg, 175 + a.py_lg, 176 + ]}> 177 + <ListCard.Default view={item} /> 178 + </View> 190 179 ) 191 - }, [ 192 - hasNextPage, 193 - error, 194 - isFetchingNextPage, 195 - headerOffset, 196 - fetchNextPage, 197 - isEmpty, 198 - ]) 180 + }, 181 + [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], 182 + ) 183 + 184 + useEffect(() => { 185 + if (isIOS && enabled && scrollElRef.current) { 186 + const nativeTag = findNodeHandle(scrollElRef.current) 187 + setScrollViewTag(nativeTag) 188 + } 189 + }, [enabled, scrollElRef, setScrollViewTag]) 199 190 191 + const ProfileListsFooter = useCallback(() => { 192 + if (isEmpty) return null 200 193 return ( 201 - <View testID={testID} style={style}> 202 - <List 203 - testID={testID ? `${testID}-flatlist` : undefined} 204 - ref={scrollElRef} 205 - data={items} 206 - keyExtractor={keyExtractor} 207 - renderItem={renderItemInner} 208 - ListFooterComponent={ProfileListsFooter} 209 - refreshing={isPTRing} 210 - onRefresh={onRefresh} 211 - headerOffset={headerOffset} 212 - progressViewOffset={ios(0)} 213 - removeClippedSubviews={true} 214 - desktopFixedHeight 215 - onEndReached={onEndReached} 216 - /> 217 - </View> 194 + <ListFooter 195 + hasNextPage={hasNextPage} 196 + isFetchingNextPage={isFetchingNextPage} 197 + onRetry={fetchNextPage} 198 + error={cleanError(error)} 199 + height={180 + headerOffset} 200 + /> 218 201 ) 219 - }, 220 - ) 202 + }, [ 203 + hasNextPage, 204 + error, 205 + isFetchingNextPage, 206 + headerOffset, 207 + fetchNextPage, 208 + isEmpty, 209 + ]) 210 + 211 + return ( 212 + <View testID={testID} style={style}> 213 + <List 214 + testID={testID ? `${testID}-flatlist` : undefined} 215 + ref={scrollElRef} 216 + data={items} 217 + keyExtractor={keyExtractor} 218 + renderItem={renderItemInner} 219 + ListFooterComponent={ProfileListsFooter} 220 + refreshing={isPTRing} 221 + onRefresh={onRefresh} 222 + headerOffset={headerOffset} 223 + progressViewOffset={ios(0)} 224 + removeClippedSubviews={true} 225 + desktopFixedHeight 226 + onEndReached={onEndReached} 227 + contentContainerStyle={{minHeight: height + headerOffset}} 228 + /> 229 + </View> 230 + ) 231 + } 221 232 222 233 function keyExtractor(item: any) { 223 234 return item._reactKey || item.uri