Adds a dismiss button to user suggestions (#9484)

* Add dismiss button to user suggestions

* Adds dismiss button to suggested user cards, behind a feature gate

* Reverse gate check, best practice

* Sync DISMISS_ANIMATION_DURATION

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Alex Benzer
2025-12-17 13:45:21 -08:00
committed by GitHub
parent f02b9c323f
commit e80e2f66c3
4 changed files with 493 additions and 79 deletions
+305 -74
View File
@@ -1,12 +1,13 @@
import React, {useCallback, useEffect, useRef} from 'react' import React, {useCallback, useEffect, useRef} from 'react'
import {ScrollView, View} from 'react-native' import {ScrollView, View} from 'react-native'
import Animated, {LinearTransition} from 'react-native-reanimated'
import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent, useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics' import {type MetricEvents} from '#/logger/metrics'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
@@ -14,7 +15,10 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {useGetPopularFeedsQuery} from '#/state/queries/feed'
import {type FeedDescriptor} from '#/state/queries/post-feed' import {type FeedDescriptor} from '#/state/queries/post-feed'
import {useProfilesQuery} from '#/state/queries/profile' import {useProfilesQuery} from '#/state/queries/profile'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' import {
useSuggestedFollowsByActorQuery,
useSuggestedFollowsQuery,
} from '#/state/queries/suggested-follows'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory' import * as userActionHistory from '#/state/userActionHistory'
import {type SeenPost} from '#/state/userActionHistory' import {type SeenPost} from '#/state/userActionHistory'
@@ -31,6 +35,7 @@ import {useDialogControl} from '#/components/Dialog'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -38,6 +43,8 @@ import type * as bsky from '#/types/bsky'
import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog' import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog'
import {ProgressGuideList} from './ProgressGuide/List' import {ProgressGuideList} from './ProgressGuide/List'
const DISMISS_ANIMATION_DURATION = 200
const MOBILE_CARD_WIDTH = 165 const MOBILE_CARD_WIDTH = 165
const FINAL_CARD_WIDTH = 120 const FINAL_CARD_WIDTH = 120
@@ -202,6 +209,9 @@ export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
} }
export function SuggestedFollowsProfile({did}: {did: string}) { export function SuggestedFollowsProfile({did}: {did: string}) {
const {gtMobile} = useBreakpoints()
const moderationOpts = useModerationOpts()
const maxLength = gtMobile ? 4 : 6
const { const {
isLoading: isSuggestionsLoading, isLoading: isSuggestionsLoading,
data, data,
@@ -209,29 +219,194 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
} = useSuggestedFollowsByActorQuery({ } = useSuggestedFollowsByActorQuery({
did, did,
}) })
const {
data: moreSuggestions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useSuggestedFollowsQuery({limit: 25})
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
new Set(),
)
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
new Set(),
)
const onDismiss = React.useCallback((dismissedDid: string) => {
// Start the fade animation
setDismissingDids(prev => new Set(prev).add(dismissedDid))
// After animation completes, actually remove from list
setTimeout(() => {
setDismissedDids(prev => new Set(prev).add(dismissedDid))
setDismissingDids(prev => {
const next = new Set(prev)
next.delete(dismissedDid)
return next
})
}, DISMISS_ANIMATION_DURATION)
}, [])
// Combine profiles from the actor-specific query with fallback suggestions
const allProfiles = React.useMemo(() => {
const actorProfiles = data?.suggestions ?? []
const fallbackProfiles =
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
// Dedupe by did, preferring actor-specific profiles
const seen = new Set<string>()
const combined: bsky.profile.AnyProfileView[] = []
for (const profile of actorProfiles) {
if (!seen.has(profile.did)) {
seen.add(profile.did)
combined.push(profile)
}
}
for (const profile of fallbackProfiles) {
if (!seen.has(profile.did) && profile.did !== did) {
seen.add(profile.did)
combined.push(profile)
}
}
return combined
}, [data?.suggestions, moreSuggestions?.pages, did])
const filteredProfiles = React.useMemo(() => {
return allProfiles.filter(p => !dismissedDids.has(p.did))
}, [allProfiles, dismissedDids])
// Fetch more when running low
React.useEffect(() => {
if (
moderationOpts &&
filteredProfiles.length < maxLength &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage()
}
}, [
filteredProfiles.length,
maxLength,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
moderationOpts,
])
return ( return (
<ProfileGrid <ProfileGrid
isSuggestionsLoading={isSuggestionsLoading} isSuggestionsLoading={isSuggestionsLoading}
profiles={data?.suggestions ?? []} profiles={filteredProfiles}
totalProfileCount={allProfiles.length}
recId={data?.recId} recId={data?.recId}
error={error} error={error}
viewContext="profile" viewContext="profile"
onDismiss={onDismiss}
dismissingDids={dismissingDids}
/> />
) )
} }
export function SuggestedFollowsHome() { export function SuggestedFollowsHome() {
const {gtMobile} = useBreakpoints()
const moderationOpts = useModerationOpts()
const maxLength = gtMobile ? 4 : 6
const { const {
isLoading: isSuggestionsLoading, isLoading: isSuggestionsLoading,
profiles, profiles: experimentalProfiles,
error, error: experimentalError,
} = useExperimentalSuggestedUsersQuery() } = useExperimentalSuggestedUsersQuery()
const {
data: moreSuggestions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
error: suggestionsError,
} = useSuggestedFollowsQuery({limit: 25})
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
new Set(),
)
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
new Set(),
)
const onDismiss = React.useCallback((did: string) => {
// Start the fade animation
setDismissingDids(prev => new Set(prev).add(did))
// After animation completes, actually remove from list
setTimeout(() => {
setDismissedDids(prev => new Set(prev).add(did))
setDismissingDids(prev => {
const next = new Set(prev)
next.delete(did)
return next
})
}, DISMISS_ANIMATION_DURATION)
}, [])
// Combine profiles from experimental query with paginated suggestions
const allProfiles = React.useMemo(() => {
const fallbackProfiles =
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
// Dedupe by did, preferring experimental profiles
const seen = new Set<string>()
const combined: bsky.profile.AnyProfileView[] = []
for (const profile of experimentalProfiles) {
if (!seen.has(profile.did)) {
seen.add(profile.did)
combined.push(profile)
}
}
for (const profile of fallbackProfiles) {
if (!seen.has(profile.did)) {
seen.add(profile.did)
combined.push(profile)
}
}
return combined
}, [experimentalProfiles, moreSuggestions?.pages])
const filteredProfiles = React.useMemo(() => {
return allProfiles.filter(p => !dismissedDids.has(p.did))
}, [allProfiles, dismissedDids])
// Fetch more when running low
React.useEffect(() => {
if (
moderationOpts &&
filteredProfiles.length < maxLength &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage()
}
}, [
filteredProfiles.length,
maxLength,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
moderationOpts,
])
return ( return (
<ProfileGrid <ProfileGrid
isSuggestionsLoading={isSuggestionsLoading} isSuggestionsLoading={isSuggestionsLoading}
profiles={profiles} profiles={filteredProfiles}
error={error} totalProfileCount={allProfiles.length}
error={experimentalError || suggestionsError}
viewContext="feed" viewContext="feed"
onDismiss={onDismiss}
dismissingDids={dismissingDids}
/> />
) )
} }
@@ -240,19 +415,26 @@ export function ProfileGrid({
isSuggestionsLoading, isSuggestionsLoading,
error, error,
profiles, profiles,
totalProfileCount,
recId, recId,
viewContext = 'feed', viewContext = 'feed',
onDismiss,
dismissingDids,
isVisible = true, isVisible = true,
}: { }: {
isSuggestionsLoading: boolean isSuggestionsLoading: boolean
profiles: bsky.profile.AnyProfileView[] profiles: bsky.profile.AnyProfileView[]
totalProfileCount?: number
recId?: number recId?: number
error: Error | null error: Error | null
dismissingDids?: Set<string>
viewContext: 'profile' | 'profileHeader' | 'feed' viewContext: 'profile' | 'profileHeader' | 'feed'
onDismiss?: (did: string) => void
isVisible?: boolean isVisible?: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const gate = useGate()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const followDialogControl = useDialogControl() const followDialogControl = useDialogControl()
@@ -260,6 +442,7 @@ export function ProfileGrid({
const isLoading = isSuggestionsLoading || !moderationOpts const isLoading = isSuggestionsLoading || !moderationOpts
const isProfileHeaderContext = viewContext === 'profileHeader' const isProfileHeaderContext = viewContext === 'profileHeader'
const isFeedContext = viewContext === 'feed' const isFeedContext = viewContext === 'feed'
const showDismissButton = onDismiss && gate('suggested_users_dismiss')
const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6 const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6
const minLength = gtMobile ? 3 : 4 const minLength = gtMobile ? 3 : 4
@@ -363,20 +546,9 @@ export function ProfileGrid({
: error || !profiles.length : error || !profiles.length
? null ? null
: profiles.slice(0, maxLength).map((profile, index) => ( : profiles.slice(0, maxLength).map((profile, index) => (
<ProfileCard.Link <Animated.View
key={profile.did} key={profile.did}
profile={profile} layout={LinearTransition.duration(DISMISS_ANIMATION_DURATION)}
onPress={() => {
logEvent('suggestedUser:press', {
logContext: isFeedContext
? 'InterstitialDiscover'
: 'InterstitialProfile',
recId,
position: index,
suggestedDid: profile.did,
category: null,
})
}}
style={[ style={[
a.flex_1, a.flex_1,
gtMobile && gtMobile &&
@@ -385,68 +557,127 @@ export function ProfileGrid({
a.flex_grow, a.flex_grow,
{width: `calc(30% - ${a.gap_md.gap / 2}px)`}, {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
]), ]),
{
opacity: dismissingDids?.has(profile.did) ? 0 : 1,
transitionProperty: 'opacity',
transitionDuration: `${DISMISS_ANIMATION_DURATION}ms`,
},
]}> ]}>
{({hovered, pressed}) => ( <ProfileCard.Link
<CardOuter profile={profile}
style={[(hovered || pressed) && t.atoms.border_contrast_high]}> onPress={() => {
<ProfileCard.Outer> logEvent('suggestedUser:press', {
<View logContext: isFeedContext
style={[ ? 'InterstitialDiscover'
a.flex_col, : 'InterstitialProfile',
a.align_center, recId,
a.gap_sm, position: index,
a.pb_sm, suggestedDid: profile.did,
a.mb_auto, category: null,
]}> })
<ProfileCard.Avatar }}>
profile={profile} {({hovered, pressed}) => (
moderationOpts={moderationOpts} <CardOuter
disabledPreview style={[
size={88} (hovered || pressed) && t.atoms.border_contrast_high,
/> ]}>
<View style={[a.flex_col, a.align_center, a.max_w_full]}> <ProfileCard.Outer>
<ProfileCard.Name {showDismissButton && (
<Button
label={_(msg`Dismiss this suggestion`)}
onPress={e => {
e.preventDefault()
onDismiss!(profile.did)
logEvent('suggestedUser:dismiss', {
logContext: isFeedContext
? 'InterstitialDiscover'
: 'InterstitialProfile',
position: index,
suggestedDid: profile.did,
recId,
})
}}
style={[
a.absolute,
a.z_10,
a.p_xs,
{top: -4, right: -4},
]}>
{({
hovered: dismissHovered,
pressed: dismissPressed,
}) => (
<X
size="xs"
fill={
dismissHovered || dismissPressed
? t.atoms.text.color
: t.atoms.text_contrast_medium.color
}
/>
)}
</Button>
)}
<View
style={[
a.flex_col,
a.align_center,
a.gap_sm,
a.pb_sm,
a.mb_auto,
]}>
<ProfileCard.Avatar
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
disabledPreview
size={88}
/> />
<ProfileCard.Description <View style={[a.flex_col, a.align_center, a.max_w_full]}>
profile={profile} <ProfileCard.Name
numberOfLines={2} profile={profile}
style={[ moderationOpts={moderationOpts}
t.atoms.text_contrast_medium, />
a.text_center, <ProfileCard.Description
a.text_xs, profile={profile}
]} numberOfLines={2}
/> style={[
t.atoms.text_contrast_medium,
a.text_center,
a.text_xs,
]}
/>
</View>
</View> </View>
</View>
<ProfileCard.FollowButton <ProfileCard.FollowButton
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
logContext="FeedInterstitial" logContext="FeedInterstitial"
withIcon={false} withIcon={false}
style={[a.rounded_sm]} style={[a.rounded_sm]}
onFollow={() => { onFollow={() => {
logEvent('suggestedUser:follow', { logEvent('suggestedUser:follow', {
logContext: isFeedContext logContext: isFeedContext
? 'InterstitialDiscover' ? 'InterstitialDiscover'
: 'InterstitialProfile', : 'InterstitialProfile',
location: 'Card', location: 'Card',
recId, recId,
position: index, position: index,
suggestedDid: profile.did, suggestedDid: profile.did,
category: null, category: null,
}) })
}} }}
/> />
</ProfileCard.Outer> </ProfileCard.Outer>
</CardOuter> </CardOuter>
)} )}
</ProfileCard.Link> </ProfileCard.Link>
</Animated.View>
)) ))
if (error || (!isLoading && profiles.length < minLength)) { // Use totalProfileCount (before dismissals) for minLength check on initial render.
const profileCountForMinCheck = totalProfileCount ?? profiles.length
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
logger.debug(`Not enough profiles to show suggested follows`) logger.debug(`Not enough profiles to show suggested follows`)
return null return null
} }
+1
View File
@@ -12,5 +12,6 @@ export type Gate =
| 'onboarding_suggested_starterpacks' | 'onboarding_suggested_starterpacks'
| 'remove_show_latest_button' | 'remove_show_latest_button'
| 'show_composer_prompt' | 'show_composer_prompt'
| 'suggested_users_dismiss'
| 'test_gate_1' | 'test_gate_1'
| 'test_gate_2' | 'test_gate_2'
+6
View File
@@ -379,6 +379,12 @@ export type MetricEvents = {
| 'Profile' | 'Profile'
| 'Onboarding' | 'Onboarding'
} }
'suggestedUser:dismiss': {
logContext: 'InterstitialDiscover' | 'InterstitialProfile'
recId?: number
position: number
suggestedDid: string
}
'profile:unfollow': { 'profile:unfollow': {
logContext: logContext:
| 'RecommendedFollowsItem' | 'RecommendedFollowsItem'
+181 -5
View File
@@ -1,20 +1,113 @@
import React from 'react'
import {type AppBskyActorDefs} from '@atproto/api'
import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
import {isAndroid} from '#/platform/detection' import {isAndroid} from '#/platform/detection'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
useSuggestedFollowsByActorQuery,
useSuggestedFollowsQuery,
} from '#/state/queries/suggested-follows'
import {useBreakpoints} from '#/alf'
import {ProfileGrid} from '#/components/FeedInterstitials' import {ProfileGrid} from '#/components/FeedInterstitials'
const DISMISS_ANIMATION_DURATION = 200
export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) { export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) {
const {gtMobile} = useBreakpoints()
const moderationOpts = useModerationOpts()
const maxLength = gtMobile ? 4 : 12
const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
did: actorDid, did: actorDid,
}) })
const {
data: moreSuggestions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useSuggestedFollowsQuery({limit: 25})
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
new Set(),
)
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
new Set(),
)
const onDismiss = React.useCallback((did: string) => {
// Start the fade animation
setDismissingDids(prev => new Set(prev).add(did))
// After animation completes, actually remove from list
setTimeout(() => {
setDismissedDids(prev => new Set(prev).add(did))
setDismissingDids(prev => {
const next = new Set(prev)
next.delete(did)
return next
})
}, DISMISS_ANIMATION_DURATION)
}, [])
// Combine profiles from the actor-specific query with fallback suggestions
const allProfiles = React.useMemo(() => {
const actorProfiles = data?.suggestions ?? []
const fallbackProfiles =
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
// Dedupe by did, preferring actor-specific profiles
const seen = new Set<string>()
const combined: AppBskyActorDefs.ProfileView[] = []
for (const profile of actorProfiles) {
if (!seen.has(profile.did)) {
seen.add(profile.did)
combined.push(profile)
}
}
for (const profile of fallbackProfiles) {
if (!seen.has(profile.did) && profile.did !== actorDid) {
seen.add(profile.did)
combined.push(profile)
}
}
return combined
}, [data?.suggestions, moreSuggestions?.pages, actorDid])
const filteredProfiles = React.useMemo(() => {
return allProfiles.filter(p => !dismissedDids.has(p.did))
}, [allProfiles, dismissedDids])
// Fetch more when running low
React.useEffect(() => {
if (
moderationOpts &&
filteredProfiles.length < maxLength &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage()
}
}, [
filteredProfiles.length,
maxLength,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
moderationOpts,
])
return ( return (
<ProfileGrid <ProfileGrid
isSuggestionsLoading={isLoading} isSuggestionsLoading={isLoading}
profiles={data?.suggestions ?? []} profiles={filteredProfiles}
totalProfileCount={allProfiles.length}
recId={data?.recId} recId={data?.recId}
error={error} error={error}
viewContext="profileHeader" viewContext="profileHeader"
onDismiss={onDismiss}
dismissingDids={dismissingDids}
/> />
) )
} }
@@ -26,11 +119,91 @@ export function AnimatedProfileHeaderSuggestedFollows({
isExpanded: boolean isExpanded: boolean
actorDid: string actorDid: string
}) { }) {
const {gtMobile} = useBreakpoints()
const moderationOpts = useModerationOpts()
const maxLength = gtMobile ? 4 : 12
const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
did: actorDid, did: actorDid,
}) })
const {
data: moreSuggestions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useSuggestedFollowsQuery({limit: 25})
if (!data?.suggestions?.length) return null const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
new Set(),
)
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
new Set(),
)
const onDismiss = React.useCallback((did: string) => {
// Start the fade animation
setDismissingDids(prev => new Set(prev).add(did))
// After animation completes, actually remove from list
setTimeout(() => {
setDismissedDids(prev => new Set(prev).add(did))
setDismissingDids(prev => {
const next = new Set(prev)
next.delete(did)
return next
})
}, DISMISS_ANIMATION_DURATION)
}, [])
// Combine profiles from the actor-specific query with fallback suggestions
const allProfiles = React.useMemo(() => {
const actorProfiles = data?.suggestions ?? []
const fallbackProfiles =
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
// Dedupe by did, preferring actor-specific profiles
const seen = new Set<string>()
const combined: AppBskyActorDefs.ProfileView[] = []
for (const profile of actorProfiles) {
if (!seen.has(profile.did)) {
seen.add(profile.did)
combined.push(profile)
}
}
for (const profile of fallbackProfiles) {
if (!seen.has(profile.did) && profile.did !== actorDid) {
seen.add(profile.did)
combined.push(profile)
}
}
return combined
}, [data?.suggestions, moreSuggestions?.pages, actorDid])
const filteredProfiles = React.useMemo(() => {
return allProfiles.filter(p => !dismissedDids.has(p.did))
}, [allProfiles, dismissedDids])
// Fetch more when running low
React.useEffect(() => {
if (
moderationOpts &&
filteredProfiles.length < maxLength &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage()
}
}, [
filteredProfiles.length,
maxLength,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
moderationOpts,
])
if (!allProfiles.length && !isLoading) return null
/* NOTE (caidanw): /* NOTE (caidanw):
* Android does not work well with this feature yet. * Android does not work well with this feature yet.
@@ -43,10 +216,13 @@ export function AnimatedProfileHeaderSuggestedFollows({
<AccordionAnimation isExpanded={isExpanded}> <AccordionAnimation isExpanded={isExpanded}>
<ProfileGrid <ProfileGrid
isSuggestionsLoading={isLoading} isSuggestionsLoading={isLoading}
profiles={data.suggestions} profiles={filteredProfiles}
recId={data.recId} totalProfileCount={allProfiles.length}
recId={data?.recId}
error={error} error={error}
viewContext="profileHeader" viewContext="profileHeader"
onDismiss={onDismiss}
dismissingDids={dismissingDids}
isVisible={isExpanded} isVisible={isExpanded}
/> />
</AccordionAnimation> </AccordionAnimation>