Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix loading jumps and footer on feeds tab (#8063)

* Fix loading jumps and footer on feeds tab

* same for lists/starter packs

authored by

Samuel Newman and committed by
GitHub
318b29d3 4db3ccbe

+86 -81
+32 -27
src/components/StarterPack/ProfileStarterPacks.tsx
··· 6 6 } from 'react' 7 7 import { 8 8 findNodeHandle, 9 - ListRenderItemInfo, 10 - StyleProp, 9 + type ListRenderItemInfo, 10 + type StyleProp, 11 11 View, 12 - ViewStyle, 12 + type ViewStyle, 13 13 } from 'react-native' 14 - import {AppBskyGraphDefs} from '@atproto/api' 14 + import {type AppBskyGraphDefs} from '@atproto/api' 15 15 import {msg, Trans} from '@lingui/macro' 16 16 import {useLingui} from '@lingui/react' 17 17 import {useNavigation} from '@react-navigation/native' ··· 20 20 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 21 21 import {useEmail} from '#/lib/hooks/useEmail' 22 22 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 23 - import {NavigationProp} from '#/lib/routes/types' 23 + import {type NavigationProp} from '#/lib/routes/types' 24 24 import {parseStarterPackUri} from '#/lib/strings/starter-pack' 25 25 import {logger} from '#/logger' 26 26 import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' 27 - import {List, ListRef} from '#/view/com/util/List' 27 + import {List, type ListRef} from '#/view/com/util/List' 28 28 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 29 29 import {atoms as a, ios, useTheme} from '#/alf' 30 30 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 75 75 const t = useTheme() 76 76 const bottomBarOffset = useBottomBarOffset(100) 77 77 const [isPTRing, setIsPTRing] = useState(false) 78 - const {data, refetch, isFetching, hasNextPage, fetchNextPage} = 79 - useActorStarterPacksQuery({did, enabled}) 78 + const { 79 + data, 80 + refetch, 81 + isError, 82 + hasNextPage, 83 + isFetchingNextPage, 84 + fetchNextPage, 85 + } = useActorStarterPacksQuery({did, enabled}) 80 86 const {isTabletOrDesktop} = useWebMediaQueries() 81 87 82 88 const items = data?.pages.flatMap(page => page.starterPacks) ··· 95 101 setIsPTRing(false) 96 102 }, [refetch, setIsPTRing]) 97 103 98 - const onEndReached = useCallback(async () => { 99 - if (isFetching || !hasNextPage) return 100 - 104 + const onEndReached = React.useCallback(async () => { 105 + if (isFetchingNextPage || !hasNextPage || isError) return 101 106 try { 102 107 await fetchNextPage() 103 108 } catch (err) { 104 109 logger.error('Failed to load more starter packs', {message: err}) 105 110 } 106 - }, [isFetching, hasNextPage, fetchNextPage]) 111 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 107 112 108 113 useEffect(() => { 109 114 if (enabled && scrollElRef.current) { ··· 112 117 } 113 118 }, [enabled, scrollElRef, setScrollViewTag]) 114 119 115 - const renderItem = ({ 116 - item, 117 - index, 118 - }: ListRenderItemInfo<AppBskyGraphDefs.StarterPackView>) => { 119 - return ( 120 - <View 121 - style={[ 122 - a.p_lg, 123 - (isTabletOrDesktop || index !== 0) && a.border_t, 124 - t.atoms.border_contrast_low, 125 - ]}> 126 - <StarterPackCard starterPack={item} /> 127 - </View> 128 - ) 129 - } 120 + const renderItem = useCallback( 121 + ({item, index}: ListRenderItemInfo<AppBskyGraphDefs.StarterPackView>) => { 122 + return ( 123 + <View 124 + style={[ 125 + a.p_lg, 126 + (isTabletOrDesktop || index !== 0) && a.border_t, 127 + t.atoms.border_contrast_low, 128 + ]}> 129 + <StarterPackCard starterPack={item} /> 130 + </View> 131 + ) 132 + }, 133 + [isTabletOrDesktop, t.atoms.border_contrast_low], 134 + ) 130 135 131 136 return ( 132 137 <View testID={testID} style={style}>
+27 -26
src/view/com/feeds/ProfileFeedgens.tsx
··· 1 1 import React from 'react' 2 2 import { 3 - ActivityIndicator, 4 3 findNodeHandle, 5 - ListRenderItemInfo, 6 - StyleProp, 7 - StyleSheet, 4 + type ListRenderItemInfo, 5 + type StyleProp, 8 6 View, 9 - ViewStyle, 7 + type ViewStyle, 10 8 } from 'react-native' 11 9 import {msg} from '@lingui/macro' 12 10 import {useLingui} from '@lingui/react' 13 11 import {useQueryClient} from '@tanstack/react-query' 14 12 15 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 16 13 import {cleanError} from '#/lib/strings/errors' 17 14 import {logger} from '#/logger' 18 15 import {isNative, isWeb} from '#/platform/detection' 19 16 import {usePreferencesQuery} from '#/state/queries/preferences' 20 17 import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 21 18 import {EmptyState} from '#/view/com/util/EmptyState' 19 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 20 + import {List, type ListRef} from '#/view/com/util/List' 22 21 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 22 + import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 23 23 import {atoms as a, ios, useTheme} from '#/alf' 24 24 import * as FeedCard from '#/components/FeedCard' 25 - import {ErrorMessage} from '../util/error/ErrorMessage' 26 - import {List, ListRef} from '../util/List' 27 - import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 25 + import {ListFooter} from '#/components/Lists' 28 26 29 27 const LOADING = {_reactKey: '__loading__'} 30 28 const EMPTY = {_reactKey: '__empty__'} ··· 58 56 const opts = React.useMemo(() => ({enabled}), [enabled]) 59 57 const { 60 58 data, 61 - isFetching, 62 - isFetched, 59 + isPending, 63 60 isFetchingNextPage, 64 61 hasNextPage, 65 62 fetchNextPage, ··· 67 64 error, 68 65 refetch, 69 66 } = useProfileFeedgensQuery(did, opts) 70 - const isEmpty = !isFetching && !data?.pages[0]?.feeds.length 67 + const isEmpty = !isPending && !data?.pages[0]?.feeds.length 71 68 const {data: preferences} = usePreferencesQuery() 72 - const {isMobile} = useWebMediaQueries() 73 69 74 70 const items = React.useMemo(() => { 75 71 let items: any[] = [] 76 72 if (isError && isEmpty) { 77 73 items = items.concat([ERROR_ITEM]) 78 74 } 79 - if (!isFetched || isFetching) { 75 + if (isPending) { 80 76 items = items.concat([LOADING]) 81 77 } else if (isEmpty) { 82 78 items = items.concat([EMPTY]) ··· 88 84 items = items.concat([LOAD_MORE_ERROR_ITEM]) 89 85 } 90 86 return items 91 - }, [isError, isEmpty, isFetched, isFetching, data]) 87 + }, [isError, isEmpty, isPending, data]) 92 88 93 89 // events 94 90 // = ··· 118 114 }, [refetch, setIsPTRing]) 119 115 120 116 const onEndReached = React.useCallback(async () => { 121 - if (isFetching || !hasNextPage || isError) return 117 + if (isFetchingNextPage || !hasNextPage || isError) return 122 118 123 119 try { 124 120 await fetchNextPage() 125 121 } catch (err) { 126 122 logger.error('Failed to load more feeds', {message: err}) 127 123 } 128 - }, [isFetching, hasNextPage, isError, fetchNextPage]) 124 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 129 125 130 126 const onPressRetryLoadMore = React.useCallback(() => { 131 127 fetchNextPage() ··· 186 182 }, [enabled, scrollElRef, setScrollViewTag]) 187 183 188 184 const ProfileFeedgensFooter = React.useCallback(() => { 189 - return isFetchingNextPage ? ( 190 - <ActivityIndicator style={[styles.footer]} /> 191 - ) : null 192 - }, [isFetchingNextPage]) 185 + return ( 186 + <ListFooter 187 + hasNextPage={hasNextPage} 188 + isFetchingNextPage={isFetchingNextPage} 189 + onRetry={fetchNextPage} 190 + error={cleanError(error)} 191 + height={180 + headerOffset} 192 + /> 193 + ) 194 + }, [hasNextPage, error, isFetchingNextPage, headerOffset, fetchNextPage]) 193 195 194 196 return ( 195 197 <View testID={testID} style={style}> ··· 197 199 testID={testID ? `${testID}-flatlist` : undefined} 198 200 ref={scrollElRef} 199 201 data={items} 200 - keyExtractor={(item: any) => item._reactKey || item.uri} 202 + keyExtractor={keyExtractor} 201 203 renderItem={renderItem} 202 204 ListFooterComponent={ProfileFeedgensFooter} 203 205 refreshing={isPTRing} 204 206 onRefresh={onRefresh} 205 207 headerOffset={headerOffset} 206 208 progressViewOffset={ios(0)} 207 - contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}} 208 209 removeClippedSubviews={true} 209 210 desktopFixedHeight 210 211 onEndReached={onEndReached} ··· 213 214 ) 214 215 }) 215 216 216 - const styles = StyleSheet.create({ 217 - footer: {paddingTop: 20}, 218 - }) 217 + function keyExtractor(item: any) { 218 + return item._reactKey || item.uri 219 + }
+27 -28
src/view/com/lists/ProfileLists.tsx
··· 1 1 import React from 'react' 2 2 import { 3 - ActivityIndicator, 4 3 findNodeHandle, 5 - ListRenderItemInfo, 6 - StyleProp, 7 - StyleSheet, 4 + type ListRenderItemInfo, 5 + type StyleProp, 8 6 View, 9 - ViewStyle, 7 + type ViewStyle, 10 8 } from 'react-native' 11 9 import {msg} from '@lingui/macro' 12 10 import {useLingui} from '@lingui/react' 13 11 import {useQueryClient} from '@tanstack/react-query' 14 12 15 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 16 13 import {cleanError} from '#/lib/strings/errors' 17 14 import {logger} from '#/logger' 18 15 import {isNative, isWeb} from '#/platform/detection' 19 16 import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' 20 17 import {EmptyState} from '#/view/com/util/EmptyState' 18 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 19 + import {List, type ListRef} from '#/view/com/util/List' 21 20 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 21 + import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 22 22 import {atoms as a, ios, useTheme} from '#/alf' 23 23 import * as ListCard from '#/components/ListCard' 24 - import {ErrorMessage} from '../util/error/ErrorMessage' 25 - import {List, ListRef} from '../util/List' 26 - import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 24 + import {ListFooter} from '#/components/Lists' 27 25 28 26 const LOADING = {_reactKey: '__loading__'} 29 27 const EMPTY = {_reactKey: '__empty__'} ··· 55 53 const opts = React.useMemo(() => ({enabled}), [enabled]) 56 54 const { 57 55 data, 58 - isFetching, 59 - isFetched, 56 + isPending, 60 57 hasNextPage, 61 58 fetchNextPage, 62 59 isFetchingNextPage, ··· 64 61 error, 65 62 refetch, 66 63 } = useProfileListsQuery(did, opts) 67 - const {isMobile} = useWebMediaQueries() 68 - const isEmpty = !isFetching && !data?.pages[0]?.lists.length 64 + const isEmpty = !isPending && !data?.pages[0]?.lists.length 69 65 70 66 const items = React.useMemo(() => { 71 67 let items: any[] = [] 72 68 if (isError && isEmpty) { 73 69 items = items.concat([ERROR_ITEM]) 74 70 } 75 - if (!isFetched || isFetching) { 71 + if (isPending) { 76 72 items = items.concat([LOADING]) 77 73 } else if (isEmpty) { 78 74 items = items.concat([EMPTY]) ··· 85 81 items = items.concat([LOAD_MORE_ERROR_ITEM]) 86 82 } 87 83 return items 88 - }, [isError, isEmpty, isFetched, isFetching, data]) 84 + }, [isError, isEmpty, isPending, data]) 89 85 90 86 // events 91 87 // = ··· 115 111 }, [refetch, setIsPTRing]) 116 112 117 113 const onEndReached = React.useCallback(async () => { 118 - if (isFetching || !hasNextPage || isError) return 114 + if (isFetchingNextPage || !hasNextPage || isError) return 119 115 try { 120 116 await fetchNextPage() 121 117 } catch (err) { 122 118 logger.error('Failed to load more lists', {message: err}) 123 119 } 124 - }, [isFetching, hasNextPage, isError, fetchNextPage]) 120 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 125 121 126 122 const onPressRetryLoadMore = React.useCallback(() => { 127 123 fetchNextPage() ··· 182 178 }, [enabled, scrollElRef, setScrollViewTag]) 183 179 184 180 const ProfileListsFooter = React.useCallback(() => { 185 - return isFetchingNextPage ? ( 186 - <ActivityIndicator style={[styles.footer]} /> 187 - ) : null 188 - }, [isFetchingNextPage]) 181 + return ( 182 + <ListFooter 183 + hasNextPage={hasNextPage} 184 + isFetchingNextPage={isFetchingNextPage} 185 + onRetry={fetchNextPage} 186 + error={cleanError(error)} 187 + height={180 + headerOffset} 188 + /> 189 + ) 190 + }, [hasNextPage, error, isFetchingNextPage, headerOffset, fetchNextPage]) 189 191 190 192 return ( 191 193 <View testID={testID} style={style}> ··· 193 195 testID={testID ? `${testID}-flatlist` : undefined} 194 196 ref={scrollElRef} 195 197 data={items} 196 - keyExtractor={(item: any) => item._reactKey || item.uri} 198 + keyExtractor={keyExtractor} 197 199 renderItem={renderItemInner} 198 200 ListFooterComponent={ProfileListsFooter} 199 201 refreshing={isPTRing} 200 202 onRefresh={onRefresh} 201 203 headerOffset={headerOffset} 202 204 progressViewOffset={ios(0)} 203 - contentContainerStyle={ 204 - isMobile && {paddingBottom: headerOffset + 100} 205 - } 206 205 removeClippedSubviews={true} 207 206 desktopFixedHeight 208 207 onEndReached={onEndReached} ··· 212 211 }, 213 212 ) 214 213 215 - const styles = StyleSheet.create({ 216 - footer: {paddingTop: 20}, 217 - }) 214 + function keyExtractor(item: any) { 215 + return item._reactKey || item.uri 216 + }