Bluesky app fork with some witchin' additions 馃挮
fork

Configure Feed

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

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