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

Configure Feed

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

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