Bluesky app fork with some witchin' additions 馃挮
at main 227 lines 6.6 kB view raw
1import React from 'react' 2import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' 3import {msg} from '@lingui/macro' 4import {useLingui} from '@lingui/react' 5import {useNavigation} from '@react-navigation/native' 6 7import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 8import {type NavigationProp} from '#/lib/routes/types' 9import {cleanError} from '#/lib/strings/errors' 10import {logger} from '#/logger' 11import {isWeb} from '#/platform/detection' 12import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 13import {useResolveDidQuery} from '#/state/queries/resolve-uri' 14import {useSession} from '#/state/session' 15import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX' 16import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2' 17import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 18import {List} from '../util/List' 19import {ProfileCardWithFollowBtn} from './ProfileCard' 20 21function renderItem({ 22 item, 23 index, 24 contextProfileDid, 25}: { 26 item: ActorDefs.ProfileView 27 index: number 28 contextProfileDid: string | undefined 29}) { 30 return ( 31 <ProfileCardWithFollowBtn 32 key={item.did} 33 profile={item} 34 noBorder={index === 0} 35 position={index + 1} 36 contextProfileDid={contextProfileDid} 37 /> 38 ) 39} 40 41function keyExtractor(item: ActorDefs.ProfileViewBasic) { 42 return item.did 43} 44 45export function ProfileFollows({name}: {name: string}) { 46 const {_} = useLingui() 47 const initialNumToRender = useInitialNumToRender() 48 const {currentAccount} = useSession() 49 const navigation = useNavigation<NavigationProp>() 50 51 const onPressFindAccounts = React.useCallback(() => { 52 if (isWeb) { 53 navigation.navigate('Search', {}) 54 } else { 55 navigation.navigate('SearchTab') 56 navigation.popToTop() 57 } 58 }, [navigation]) 59 60 const [isPTRing, setIsPTRing] = React.useState(false) 61 const { 62 data: resolvedDid, 63 isLoading: isDidLoading, 64 error: resolveError, 65 } = useResolveDidQuery(name) 66 const { 67 data, 68 isLoading: isFollowsLoading, 69 isFetchingNextPage, 70 hasNextPage, 71 fetchNextPage, 72 error, 73 refetch, 74 } = useProfileFollowsQuery(resolvedDid) 75 76 const isError = !!resolveError || !!error 77 const isMe = resolvedDid === currentAccount?.did 78 79 const follows = React.useMemo(() => { 80 if (data?.pages) { 81 return data.pages.flatMap(page => page.follows) 82 } 83 return [] 84 }, [data]) 85 86 // Track pagination events - fire for page 3+ (pages 1-2 may auto-load) 87 const paginationTrackingRef = React.useRef<{ 88 did: string | undefined 89 page: number 90 }>({did: undefined, page: 0}) 91 React.useEffect(() => { 92 const currentPageCount = data?.pages?.length || 0 93 // Reset tracking when profile changes 94 if (paginationTrackingRef.current.did !== resolvedDid) { 95 paginationTrackingRef.current = {did: resolvedDid, page: currentPageCount} 96 return 97 } 98 if ( 99 resolvedDid && 100 currentPageCount >= 3 && 101 currentPageCount > paginationTrackingRef.current.page 102 ) { 103 logger.metric('profile:following:paginate', { 104 contextProfileDid: resolvedDid, 105 itemCount: follows.length, 106 page: currentPageCount, 107 }) 108 } 109 paginationTrackingRef.current.page = currentPageCount 110 }, [data?.pages?.length, resolvedDid, follows.length]) 111 112 const onRefresh = React.useCallback(async () => { 113 setIsPTRing(true) 114 try { 115 await refetch() 116 } catch (err) { 117 logger.error('Failed to refresh follows', {error: err}) 118 } 119 setIsPTRing(false) 120 }, [refetch, setIsPTRing]) 121 122 const onEndReached = React.useCallback(async () => { 123 if (isFetchingNextPage || !hasNextPage || !!error) return 124 try { 125 await fetchNextPage() 126 } catch (err) { 127 logger.error('Failed to load more follows', {error: err}) 128 } 129 }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) 130 131 const renderItemWithContext = React.useCallback( 132 ({item, index}: {item: ActorDefs.ProfileView; index: number}) => 133 renderItem({item, index, contextProfileDid: resolvedDid}), 134 [resolvedDid], 135 ) 136 137 // track pageview 138 React.useEffect(() => { 139 if (resolvedDid) { 140 logger.metric('profile:following:view', { 141 contextProfileDid: resolvedDid, 142 isOwnProfile: isMe, 143 }) 144 } 145 }, [resolvedDid, isMe]) 146 147 // track seen items 148 const seenItemsRef = React.useRef<Set<string>>(new Set()) 149 React.useEffect(() => { 150 seenItemsRef.current.clear() 151 }, [resolvedDid]) 152 const onItemSeen = React.useCallback( 153 (item: ActorDefs.ProfileView) => { 154 if (seenItemsRef.current.has(item.did)) { 155 return 156 } 157 seenItemsRef.current.add(item.did) 158 const position = follows.findIndex(p => p.did === item.did) + 1 159 if (position === 0) { 160 return 161 } 162 logger.metric( 163 'profileCard:seen', 164 { 165 profileDid: item.did, 166 position, 167 ...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}), 168 }, 169 {statsig: false}, 170 ) 171 }, 172 [follows, resolvedDid], 173 ) 174 175 if (follows.length < 1) { 176 return ( 177 <ListMaybePlaceholder 178 isLoading={isDidLoading || isFollowsLoading} 179 isError={isError} 180 emptyType="results" 181 emptyMessage={ 182 isMe 183 ? _(msg`You are not following anyone yet`) 184 : _(msg`This user isn't following anyone.`) 185 } 186 errorMessage={cleanError(resolveError || error)} 187 onRetry={isError ? refetch : undefined} 188 sideBorders={false} 189 useEmptyState={true} 190 emptyStateIcon={PeopleRemoveIcon} 191 emptyStateButton={{ 192 label: _(msg`See suggested accounts`), 193 text: _(msg`See suggested accounts`), 194 onPress: onPressFindAccounts, 195 size: 'tiny', 196 color: 'primary', 197 }} 198 /> 199 ) 200 } 201 202 return ( 203 <List 204 data={follows} 205 renderItem={renderItemWithContext} 206 keyExtractor={keyExtractor} 207 refreshing={isPTRing} 208 onRefresh={onRefresh} 209 onEndReached={onEndReached} 210 onEndReachedThreshold={4} 211 onItemSeen={onItemSeen} 212 ListHeaderComponent={<FindContactsBannerNUX />} 213 ListFooterComponent={ 214 <ListFooter 215 isFetchingNextPage={isFetchingNextPage} 216 error={cleanError(error)} 217 onRetry={fetchNextPage} 218 /> 219 } 220 // @ts-ignore our .web version only -prf 221 desktopFixedHeight 222 initialNumToRender={initialNumToRender} 223 windowSize={11} 224 sideBorders={false} 225 /> 226 ) 227}