Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 270 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/core/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, useProfileListsQuery} from '#/state/queries/profile-lists' 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 {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 33import * as ListCard from '#/components/ListCard' 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 ProfileListsProps { 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 ProfileLists({ 58 ref, 59 did, 60 scrollElRef, 61 headerOffset, 62 enabled, 63 style, 64 testID, 65 setScrollViewTag, 66}: ProfileListsProps) { 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 } = useProfileListsQuery(did, opts) 82 const isEmpty = !isPending && !data?.pages[0]?.lists.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 listItems: any[] = [] 90 if (isError && isEmpty) { 91 listItems = listItems.concat([ERROR_ITEM]) 92 } 93 if (isPending) { 94 listItems = listItems.concat([LOADING]) 95 } else if (isEmpty) { 96 listItems = listItems.concat([EMPTY]) 97 } else if (data?.pages) { 98 for (const page of data?.pages) { 99 listItems = listItems.concat(page.lists) 100 } 101 } else if (isError && !isEmpty) { 102 listItems = listItems.concat([LOAD_MORE_ERROR_ITEM]) 103 } 104 return listItems 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 lists', {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 lists', {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 icon={ListIcon} 157 message={ 158 isSelf 159 ? _(msg`You haven't created any lists yet.`) 160 : _(msg`No lists`) 161 } 162 textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 163 button={ 164 isSelf 165 ? { 166 label: _(msg`Create a list`), 167 text: _(msg`Create a list`), 168 onPress: () => navigation.navigate('Lists' as never), 169 size: 'small', 170 color: 'primary', 171 } 172 : undefined 173 } 174 /> 175 ) 176 } else if (item === ERROR_ITEM) { 177 return ( 178 <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 179 ) 180 } else if (item === LOAD_MORE_ERROR_ITEM) { 181 return ( 182 <LoadMoreRetryBtn 183 label={_( 184 msg`There was an issue fetching your lists. Tap here to try again.`, 185 )} 186 onPress={onPressRetryLoadMore} 187 /> 188 ) 189 } else if (item === LOADING) { 190 return <FeedLoadingPlaceholder /> 191 } 192 if (preferences) { 193 return ( 194 <View 195 style={[ 196 (index !== 0 || IS_WEB) && a.border_t, 197 t.atoms.border_contrast_low, 198 a.px_lg, 199 a.py_lg, 200 ]}> 201 <ListCard.Default view={item} /> 202 </View> 203 ) 204 } 205 return null 206 }, 207 [ 208 _, 209 t, 210 error, 211 refetch, 212 onPressRetryLoadMore, 213 preferences, 214 navigation, 215 isSelf, 216 ], 217 ) 218 219 useEffect(() => { 220 if (IS_IOS && enabled && scrollElRef.current) { 221 const nativeTag = findNodeHandle(scrollElRef.current) 222 setScrollViewTag(nativeTag) 223 } 224 }, [enabled, scrollElRef, setScrollViewTag]) 225 226 const ProfileListsFooter = useCallback(() => { 227 if (isEmpty) return null 228 return ( 229 <ListFooter 230 hasNextPage={hasNextPage} 231 isFetchingNextPage={isFetchingNextPage} 232 onRetry={fetchNextPage} 233 error={cleanError(error)} 234 height={180 + headerOffset} 235 /> 236 ) 237 }, [ 238 hasNextPage, 239 error, 240 isFetchingNextPage, 241 headerOffset, 242 fetchNextPage, 243 isEmpty, 244 ]) 245 246 return ( 247 <View testID={testID} style={style}> 248 <List 249 testID={testID ? `${testID}-flatlist` : undefined} 250 ref={scrollElRef} 251 data={items} 252 keyExtractor={keyExtractor} 253 renderItem={renderItem} 254 ListFooterComponent={ProfileListsFooter} 255 refreshing={isPTRing} 256 onRefresh={onRefresh} 257 headerOffset={headerOffset} 258 progressViewOffset={ios(0)} 259 removeClippedSubviews={true} 260 desktopFixedHeight 261 onEndReached={onEndReached} 262 contentContainerStyle={{minHeight: height + headerOffset}} 263 /> 264 </View> 265 ) 266} 267 268function keyExtractor(item: any) { 269 return item._reactKey || item.uri 270}