Remove getSuggestedFollowsByActor fallbacks and use recIdStr (#9988)

This commit is contained in:
DS Boyce
2026-03-12 16:57:38 -07:00
committed by GitHub
parent 35cb2bcf94
commit 0b6ff8000d
6 changed files with 148 additions and 336 deletions
@@ -75,6 +75,8 @@ let ProfileHeaderStandard = ({
const [, queueUnblock] = useProfileBlockMutationQueue(profile)
const unblockPromptControl = Prompt.usePromptControl()
const [showSuggestedFollows, setShowSuggestedFollows] = useState(false)
const [hasSeenAllSuggestedFollows, setHasSeenAllSuggestedFollows] =
useState(false)
const isBlockedUser =
profile.viewer?.blocking ||
profile.viewer?.blockedBy ||
@@ -84,7 +86,8 @@ let ProfileHeaderStandard = ({
try {
await queueUnblock()
Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
} catch (e: any) {
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'})
@@ -92,6 +95,11 @@ let ProfileHeaderStandard = ({
}
}
const onRequestHide = () => {
setHasSeenAllSuggestedFollows(true)
setShowSuggestedFollows(false)
}
const isMe = currentAccount?.did === profile.did
const {isActive: live} = useActorStatus(profile)
@@ -192,7 +200,9 @@ let ProfileHeaderStandard = ({
description={_(
msg`The account will be able to interact with you after unblocking.`,
)}
onConfirm={unblockAccount}
onConfirm={() => {
void unblockAccount()
}}
confirmButtonCta={
profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
}
@@ -201,8 +211,9 @@ let ProfileHeaderStandard = ({
</ProfileHeaderShell>
<ProfileHeaderSuggestedFollows
isExpanded={showSuggestedFollows}
isExpanded={!hasSeenAllSuggestedFollows && showSuggestedFollows}
actorDid={profile.did}
onRequestHide={onRequestHide}
/>
</>
)
@@ -254,7 +265,8 @@ export function HeaderStandardButtons({
)}`,
),
)
} catch (e: any) {
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
@@ -280,7 +292,8 @@ export function HeaderStandardButtons({
),
{type: 'default'},
)
} catch (e: any) {
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
@@ -295,7 +308,8 @@ export function HeaderStandardButtons({
try {
await queueUnblock()
Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
} catch (e: any) {
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'})
@@ -400,7 +414,9 @@ export function HeaderStandardButtons({
description={_(
msg`The account will be able to interact with you after unblocking.`,
)}
onConfirm={unblockAccount}
onConfirm={() => {
void unblockAccount()
}}
confirmButtonCta={_(msg`Unblock`)}
confirmButtonColor="negative"
/>
+34 -76
View File
@@ -1,12 +1,11 @@
import {useCallback, useEffect, useMemo, useState} from 'react'
import {useCallback, useMemo} from 'react'
import {useQueryClient} from '@tanstack/react-query'
import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
suggestedFollowsByActorQueryKey,
useSuggestedFollowsByActorQuery,
useSuggestedFollowsQuery,
} from '#/state/queries/suggested-follows'
import {useBreakpoints} from '#/alf'
import {ProfileGrid} from '#/components/FeedInterstitials'
import {IS_ANDROID} from '#/env'
import type * as bsky from '#/types/bsky'
@@ -14,15 +13,15 @@ import type * as bsky from '#/types/bsky'
export function ProfileHeaderSuggestedFollows({
isExpanded,
actorDid,
onRequestHide,
}: {
isExpanded: boolean
actorDid: string
onRequestHide: () => void
}) {
const {allProfiles, filteredProfiles, onDismiss, isLoading, error} =
const {profiles, onDismiss, isLoading, error} =
useProfileHeaderSuggestions(actorDid)
if (!allProfiles.length && !isLoading) return null
/* NOTE (caidanw):
* Android does not work well with this feature yet.
* This issue stems from Android not allowing dragging on clickable elements in the profile header.
@@ -34,92 +33,51 @@ export function ProfileHeaderSuggestedFollows({
<AccordionAnimation isExpanded={isExpanded}>
<ProfileGrid
isSuggestionsLoading={isLoading}
profiles={filteredProfiles}
totalProfileCount={allProfiles.length}
profiles={profiles}
totalProfileCount={profiles.length}
error={error}
viewContext="profileHeader"
onDismiss={onDismiss}
isVisible={isExpanded}
onRequestHide={onRequestHide}
/>
</AccordionAnimation>
)
}
function useProfileHeaderSuggestions(actorDid: string) {
const {gtMobile} = useBreakpoints()
const moderationOpts = useModerationOpts()
const maxLength = gtMobile ? 4 : 12
const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
did: actorDid,
})
const {
data: moreSuggestions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useSuggestedFollowsQuery({limit: 25})
const queryClient = useQueryClient()
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
const onDismiss = useCallback(
(dismissedDid: string) => {
queryClient.setQueryData(
suggestedFollowsByActorQueryKey(actorDid),
(previous: typeof data) => {
if (!previous) return previous
return {
...previous,
suggestions: previous.suggestions.filter(
s => s.did !== dismissedDid,
),
}
},
)
},
[actorDid, queryClient],
)
const onDismiss = useCallback((did: string) => {
setDismissedDids(prev => new Set(prev).add(did))
}, [])
// Combine profiles from the actor-specific query with fallback suggestions
const allProfiles = useMemo(() => {
const actorProfiles = data?.suggestions ?? []
const fallbackProfiles =
moreSuggestions?.pages.flatMap(page =>
page.actors.map(actor => ({actor, recId: page.recId})),
) ?? []
// Dedupe by did, preferring actor-specific profiles
const seen = new Set<string>()
const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = []
for (const profile of actorProfiles) {
if (!seen.has(profile.did)) {
seen.add(profile.did)
combined.push({actor: profile, recId: data?.recId})
}
}
for (const profile of fallbackProfiles) {
if (!seen.has(profile.actor.did) && profile.actor.did !== actorDid) {
seen.add(profile.actor.did)
combined.push(profile)
}
}
return combined
}, [data?.suggestions, moreSuggestions?.pages, actorDid, data?.recId])
const filteredProfiles = useMemo(() => {
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
}, [allProfiles, dismissedDids])
// Fetch more when running low
useEffect(() => {
if (
moderationOpts &&
filteredProfiles.length < maxLength &&
hasNextPage &&
!isFetchingNextPage
) {
void fetchNextPage()
}
}, [
filteredProfiles.length,
maxLength,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
moderationOpts,
])
const profiles = useMemo(() => {
return (data?.suggestions ?? []).map(profile => ({
actor: profile as bsky.profile.AnyProfileView,
recId: data?.recId,
}))
}, [data?.suggestions, data?.recId])
return {
allProfiles,
filteredProfiles,
profiles,
onDismiss,
isLoading,
error,