Files
bsky-social-app/src/view/com/profile/ProfileFollows.tsx
T
Anastasiya Uraleva d0649e9a9e [APP-1403] Profile empty states (#8969)
* update: Empty state component

* update type error fixes

* add empty state icon

* update translations on labels

* update: lint error for empty state

* remove unused prompt

* update type errors

* fix lint errors and update profile followers and profile follows

* updated starterpack

* fix lint errors

* address feedback

* update icons to be a react element

* update icons

* update viewbox for icons

* optimize: icons and update profile lists

* optimize: icons and update profile lists

* lint error fix

* update iconSize from the rebase
2025-11-27 03:22:03 -08:00

190 lines
5.2 KiB
TypeScript

import React from 'react'
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {type NavigationProp} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSession} from '#/state/session'
import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from './ProfileCard'
function renderItem({
item,
index,
contextProfileDid,
}: {
item: ActorDefs.ProfileView
index: number
contextProfileDid: string | undefined
}) {
return (
<ProfileCardWithFollowBtn
key={item.did}
profile={item}
noBorder={index === 0}
position={index + 1}
contextProfileDid={contextProfileDid}
/>
)
}
function keyExtractor(item: ActorDefs.ProfileViewBasic) {
return item.did
}
export function ProfileFollows({name}: {name: string}) {
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession()
const navigation = useNavigation<NavigationProp>()
const onPressFindAccounts = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Search', {})
} else {
navigation.navigate('SearchTab')
navigation.popToTop()
}
}, [navigation])
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedDid,
isLoading: isDidLoading,
error: resolveError,
} = useResolveDidQuery(name)
const {
data,
isLoading: isFollowsLoading,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
error,
refetch,
} = useProfileFollowsQuery(resolvedDid)
const isError = !!resolveError || !!error
const isMe = resolvedDid === currentAccount?.did
const follows = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.follows)
}
return []
}, [data])
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh follows', {error: err})
}
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || !!error) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more follows', {error: err})
}
}, [error, fetchNextPage, hasNextPage, isFetchingNextPage])
const renderItemWithContext = React.useCallback(
({item, index}: {item: ActorDefs.ProfileView; index: number}) =>
renderItem({item, index, contextProfileDid: resolvedDid}),
[resolvedDid],
)
// track seen items
const seenItemsRef = React.useRef<Set<string>>(new Set())
React.useEffect(() => {
seenItemsRef.current.clear()
}, [resolvedDid])
const onItemSeen = React.useCallback(
(item: ActorDefs.ProfileView) => {
if (seenItemsRef.current.has(item.did)) {
return
}
seenItemsRef.current.add(item.did)
const position = follows.findIndex(p => p.did === item.did) + 1
if (position === 0) {
return
}
logger.metric(
'profileCard:seen',
{
profileDid: item.did,
position,
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
},
{statsig: false},
)
},
[follows, resolvedDid],
)
if (follows.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowsLoading}
isError={isError}
emptyType="results"
emptyMessage={
isMe
? _(msg`You are not following anyone yet`)
: _(msg`This user isn't following anyone.`)
}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
sideBorders={false}
useEmptyState={true}
emptyStateIcon={PeopleRemoveIcon}
emptyStateButton={{
label: _(msg`See suggested accounts`),
text: _(msg`See suggested accounts`),
onPress: onPressFindAccounts,
size: 'tiny',
color: 'primary',
}}
/>
)
}
return (
<List
data={follows}
renderItem={renderItemWithContext}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={onItemSeen}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
sideBorders={false}
/>
)
}