Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at readme-update 271 lines 7.3 kB view raw
1import { 2 useCallback, 3 useEffect, 4 useImperativeHandle, 5 useMemo, 6 useState, 7} from 'react' 8import { 9 findNodeHandle, 10 type ListRenderItemInfo, 11 type StyleProp, 12 useWindowDimensions, 13 View, 14 type ViewStyle, 15} from 'react-native' 16import {msg} from '@lingui/macro' 17import {useLingui} from '@lingui/react' 18import {useNavigation} from '@react-navigation/native' 19import {useQueryClient} from '@tanstack/react-query' 20 21import {cleanError} from '#/lib/strings/errors' 22import {logger} from '#/logger' 23import {usePreferencesQuery} from '#/state/queries/preferences' 24import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 25import {useSession} from '#/state/session' 26import {EmptyState} from '#/view/com/util/EmptyState' 27import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 28import {List, type ListRef} from '#/view/com/util/List' 29import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 30import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 31import {atoms as a, ios, useTheme} from '#/alf' 32import * as FeedCard from '#/components/FeedCard' 33import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 34import {ListFooter} from '#/components/Lists' 35import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 36 37const LOADING = {_reactKey: '__loading__'} 38const EMPTY = {_reactKey: '__empty__'} 39const ERROR_ITEM = {_reactKey: '__error__'} 40const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} 41 42interface SectionRef { 43 scrollToTop: () => void 44} 45 46interface ProfileFeedgensProps { 47 ref?: React.Ref<SectionRef> 48 did: string 49 scrollElRef: ListRef 50 headerOffset: number 51 enabled?: boolean 52 style?: StyleProp<ViewStyle> 53 testID?: string 54 setScrollViewTag: (tag: number | null) => void 55} 56 57export function ProfileFeedgens({ 58 ref, 59 did, 60 scrollElRef, 61 headerOffset, 62 enabled, 63 style, 64 testID, 65 setScrollViewTag, 66}: ProfileFeedgensProps) { 67 const {_} = useLingui() 68 const t = useTheme() 69 const [isPTRing, setIsPTRing] = useState(false) 70 const {height} = useWindowDimensions() 71 const opts = useMemo(() => ({enabled}), [enabled]) 72 const { 73 data, 74 isPending, 75 isFetchingNextPage, 76 hasNextPage, 77 fetchNextPage, 78 isError, 79 error, 80 refetch, 81 } = useProfileFeedgensQuery(did, opts) 82 const isEmpty = !isPending && !data?.pages[0]?.feeds.length 83 const {data: preferences} = usePreferencesQuery() 84 const navigation = useNavigation() 85 const {currentAccount} = useSession() 86 const isSelf = currentAccount?.did === did 87 88 const items = useMemo(() => { 89 let items: any[] = [] 90 if (isError && isEmpty) { 91 items = items.concat([ERROR_ITEM]) 92 } 93 if (isPending) { 94 items = items.concat([LOADING]) 95 } else if (isEmpty) { 96 items = items.concat([EMPTY]) 97 } else if (data?.pages) { 98 for (const page of data?.pages) { 99 items = items.concat(page.feeds) 100 } 101 } else if (isError && !isEmpty) { 102 items = items.concat([LOAD_MORE_ERROR_ITEM]) 103 } 104 return items 105 }, [isError, isEmpty, isPending, data]) 106 107 // events 108 // = 109 110 const queryClient = useQueryClient() 111 112 const onScrollToTop = useCallback(() => { 113 scrollElRef.current?.scrollToOffset({ 114 animated: IS_NATIVE, 115 offset: -headerOffset, 116 }) 117 queryClient.invalidateQueries({queryKey: RQKEY(did)}) 118 }, [scrollElRef, queryClient, headerOffset, did]) 119 120 useImperativeHandle(ref, () => ({ 121 scrollToTop: onScrollToTop, 122 })) 123 124 const onRefresh = useCallback(async () => { 125 setIsPTRing(true) 126 try { 127 await refetch() 128 } catch (err) { 129 logger.error('Failed to refresh feeds', {message: err}) 130 } 131 setIsPTRing(false) 132 }, [refetch, setIsPTRing]) 133 134 const onEndReached = useCallback(async () => { 135 if (isFetchingNextPage || !hasNextPage || isError) return 136 137 try { 138 await fetchNextPage() 139 } catch (err) { 140 logger.error('Failed to load more feeds', {message: err}) 141 } 142 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 143 144 const onPressRetryLoadMore = useCallback(() => { 145 fetchNextPage() 146 }, [fetchNextPage]) 147 148 // rendering 149 // = 150 151 const renderItem = useCallback( 152 ({item, index}: ListRenderItemInfo<any>) => { 153 if (item === EMPTY) { 154 return ( 155 <EmptyState 156 style={{width: '100%'}} 157 icon={HashtagWideIcon} 158 message={ 159 isSelf 160 ? _(msg`You haven't made any custom feeds yet.`) 161 : _(msg`No custom feeds yet`) 162 } 163 textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 164 button={ 165 isSelf 166 ? { 167 label: _(msg`Browse custom feeds`), 168 text: _(msg`Browse custom feeds`), 169 onPress: () => navigation.navigate('Feeds' as never), 170 size: 'small', 171 color: 'secondary', 172 } 173 : undefined 174 } 175 /> 176 ) 177 } else if (item === ERROR_ITEM) { 178 return ( 179 <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 180 ) 181 } else if (item === LOAD_MORE_ERROR_ITEM) { 182 return ( 183 <LoadMoreRetryBtn 184 label={_( 185 msg`There was an issue fetching your lists. Tap here to try again.`, 186 )} 187 onPress={onPressRetryLoadMore} 188 /> 189 ) 190 } else if (item === LOADING) { 191 return <FeedLoadingPlaceholder /> 192 } 193 if (preferences) { 194 return ( 195 <View 196 style={[ 197 (index !== 0 || IS_WEB) && a.border_t, 198 t.atoms.border_contrast_low, 199 a.px_lg, 200 a.py_lg, 201 ]}> 202 <FeedCard.Default view={item} /> 203 </View> 204 ) 205 } 206 return null 207 }, 208 [ 209 _, 210 t, 211 error, 212 refetch, 213 onPressRetryLoadMore, 214 preferences, 215 navigation, 216 isSelf, 217 ], 218 ) 219 220 useEffect(() => { 221 if (IS_IOS && enabled && scrollElRef.current) { 222 const nativeTag = findNodeHandle(scrollElRef.current) 223 setScrollViewTag(nativeTag) 224 } 225 }, [enabled, scrollElRef, setScrollViewTag]) 226 227 const ProfileFeedgensFooter = useCallback(() => { 228 if (isEmpty) return null 229 return ( 230 <ListFooter 231 hasNextPage={hasNextPage} 232 isFetchingNextPage={isFetchingNextPage} 233 onRetry={fetchNextPage} 234 error={cleanError(error)} 235 height={180 + headerOffset} 236 /> 237 ) 238 }, [ 239 hasNextPage, 240 error, 241 isFetchingNextPage, 242 headerOffset, 243 fetchNextPage, 244 isEmpty, 245 ]) 246 247 return ( 248 <View testID={testID} style={style}> 249 <List 250 testID={testID ? `${testID}-flatlist` : undefined} 251 ref={scrollElRef} 252 data={items} 253 keyExtractor={keyExtractor} 254 renderItem={renderItem} 255 ListFooterComponent={ProfileFeedgensFooter} 256 refreshing={isPTRing} 257 onRefresh={onRefresh} 258 headerOffset={headerOffset} 259 progressViewOffset={ios(0)} 260 removeClippedSubviews={true} 261 desktopFixedHeight 262 onEndReached={onEndReached} 263 contentContainerStyle={{minHeight: height + headerOffset}} 264 /> 265 </View> 266 ) 267} 268 269function keyExtractor(item: any) { 270 return item._reactKey || item.uri 271}