forked from
jollywhoppers.com/witchsky.app
fork
Configure Feed
Select the types of activity you want to include in your feed.
Bluesky app fork with some witchin' additions 馃挮
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}