[Explore] New suggested follows endpoint (#8130)

* Bump SDK

* Integrate new endpoint, add profile shadow, For You tab

* Format
This commit is contained in:
Eric Bailey
2025-04-04 18:44:02 -05:00
committed by GitHub
parent a0ff9b52aa
commit aca89d4aea
6 changed files with 108 additions and 106 deletions
+1 -1
View File
@@ -58,7 +58,7 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.14.19", "@atproto/api": "^0.14.20",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+20 -91
View File
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics' import {type MetricEvents} from '#/logger/metrics'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import { import {
type FeedPreviewItem, type FeedPreviewItem,
useFeedPreviews, useFeedPreviews,
@@ -23,8 +22,8 @@ import {
import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {useGetPopularFeedsQuery} from '#/state/queries/feed'
import {Nux, useNux} from '#/state/queries/nuxs' import {Nux, useNux} from '#/state/queries/nuxs'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery' import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery' import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
import {useProgressGuide} from '#/state/shell/progress-guide' import {useProgressGuide} from '#/state/shell/progress-guide'
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed' import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
@@ -57,7 +56,6 @@ import * as ModuleHeader from './components/ModuleHeader'
import { import {
SuggestedAccountsTabBar, SuggestedAccountsTabBar,
SuggestedProfileCard, SuggestedProfileCard,
useLoadEnoughProfiles,
} from './modules/ExploreSuggestedAccounts' } from './modules/ExploreSuggestedAccounts'
function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) { function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
@@ -144,7 +142,7 @@ type ExploreScreenItems =
| { | {
type: 'profile' type: 'profile'
key: string key: string
profile: AppBskyActorDefs.ProfileView profile: AppBskyActorDefs.ProfileViewBasic
recId?: number recId?: number
} }
| { | {
@@ -203,33 +201,13 @@ export function Explore({
const gate = useGate() const gate = useGate()
const guide = useProgressGuide('follow-10') const guide = useProgressGuide('follow-10')
const [selectedInterest, setSelectedInterest] = useState<string | null>(null) const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
// TODO always get at least 10 back
const { const {
data: suggestedProfiles, data: suggestedUsers,
hasNextPage: hasNextSuggestedProfilesPage, isLoading: suggestedUsersIsLoading,
isLoading: isLoadingSuggestedProfiles, error: suggestedUsersError,
isFetchingNextPage: isFetchingNextSuggestedProfilesPage, } = useGetSuggestedUsersQuery({
error: suggestedProfilesError, category: selectedInterest,
fetchNextPage: fetchNextSuggestedProfilesPage,
} = useSuggestedFollowsQuery({limit: 3, subsequentPageLimit: 10})
const {
data: interestProfiles,
hasNextPage: hasNextInterestProfilesPage,
isLoading: isLoadingInterestProfiles,
isFetchingNextPage: isFetchingNextInterestProfilesPage,
error: interestProfilesError,
fetchNextPage: fetchNextInterestProfilesPage,
} = useActorSearchPaginated({
query: selectedInterest || '',
enabled: !!selectedInterest,
limit: 10,
})
const {isReady: canShowSuggestedProfiles} = useLoadEnoughProfiles({
interest: selectedInterest,
data: interestProfiles,
isLoading: isLoadingInterestProfiles,
isFetchingNextPage: isFetchingNextInterestProfilesPage,
hasNextPage: hasNextInterestProfilesPage,
fetchNextPage: fetchNextInterestProfilesPage,
}) })
const { const {
data: feeds, data: feeds,
@@ -243,39 +221,6 @@ export function Explore({
const showInterestsNux = const showInterestsNux =
interestsNux.status === 'ready' && !interestsNux.nux?.completed interestsNux.status === 'ready' && !interestsNux.nux?.completed
const profiles: typeof suggestedProfiles & typeof interestProfiles =
!selectedInterest ? suggestedProfiles : interestProfiles
const hasNextProfilesPage = !selectedInterest
? hasNextSuggestedProfilesPage
: hasNextInterestProfilesPage
const isLoadingProfiles = !selectedInterest
? isLoadingSuggestedProfiles
: !canShowSuggestedProfiles
const isFetchingNextProfilesPage = !selectedInterest
? isFetchingNextSuggestedProfilesPage
: !canShowSuggestedProfiles
const profilesError = !selectedInterest
? suggestedProfilesError
: interestProfilesError
const fetchNextProfilesPage = !selectedInterest
? fetchNextSuggestedProfilesPage
: fetchNextInterestProfilesPage
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
const onLoadMoreProfiles = useCallback(async () => {
if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
return
try {
await fetchNextProfilesPage()
} catch (err) {
logger.error('Failed to load more suggested follows', {message: err})
}
}, [
isFetchingNextProfilesPage,
hasNextProfilesPage,
profilesError,
fetchNextProfilesPage,
])
const { const {
data: suggestedSPs, data: suggestedSPs,
isLoading: isLoadingSuggestedSPs, isLoading: isLoadingSuggestedSPs,
@@ -358,55 +303,42 @@ export function Explore({
}, },
}) })
if (!canShowSuggestedProfiles) { if (suggestedUsersIsLoading) {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
} else if (profilesError) { } else if (suggestedUsersError) {
i.push({ i.push({
type: 'error', type: 'error',
key: 'profilesError', key: 'suggestedUsersError',
message: _(msg`Failed to load suggested follows`), message: _(msg`Failed to load suggested follows`),
error: cleanError(profilesError), error: cleanError(suggestedUsersError),
}) })
} else { } else {
if (profiles !== undefined) { if (suggestedUsers !== undefined) {
if (profiles.pages.length > 0 && moderationOpts) { if (suggestedUsers.actors.length > 0 && moderationOpts) {
// Currently the responses contain duplicate items. // Currently the responses contain duplicate items.
// Needs to be fixed on backend, but let's dedupe to be safe. // Needs to be fixed on backend, but let's dedupe to be safe.
let seen = new Set() let seen = new Set()
const profileItems: ExploreScreenItems[] = [] const profileItems: ExploreScreenItems[] = []
for (const page of profiles.pages) { for (const actor of suggestedUsers.actors) {
for (const actor of page.actors) {
if (!seen.has(actor.did) && !actor.viewer?.following) { if (!seen.has(actor.did) && !actor.viewer?.following) {
seen.add(actor.did) seen.add(actor.did)
profileItems.push({ profileItems.push({
type: 'profile', type: 'profile',
key: actor.did, key: actor.did,
profile: actor, profile: actor,
recId: page.recId,
}) })
} }
} }
}
if (profileItems.length === 0) { if (profileItems.length === 0) {
if (!hasNextProfilesPage) {
// no items! remove the header // no items! remove the header
i.pop() i.pop()
}
} else { } else {
i.push(...profileItems) i.push(...profileItems)
} }
if (hasNextProfilesPage) {
i.push({
type: 'loadMore',
key: 'loadMoreProfiles',
message: _(msg`Load more suggested accounts`),
isLoadingMore: isLoadingMoreProfiles,
onLoadMore: onLoadMoreProfiles,
})
}
} else { } else {
console.log('no pages') // no items! remove the header
i.pop()
} }
} else { } else {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
@@ -414,14 +346,11 @@ export function Explore({
} }
return i return i
}, [ }, [
profiles,
_, _,
canShowSuggestedProfiles,
hasNextProfilesPage,
isLoadingMoreProfiles,
moderationOpts, moderationOpts,
onLoadMoreProfiles, suggestedUsers,
profilesError, suggestedUsersIsLoading,
suggestedUsersError,
]) ])
const suggestedFeedsModule = useMemo(() => { const suggestedFeedsModule = useMemo(() => {
const i: ExploreScreenItems[] = [] const i: ExploreScreenItems[] = []
@@ -83,7 +83,7 @@ export function SuggestedAccountsTabBar({
}} }}
hasSearchText={false} hasSearchText={false}
interestsDisplayNames={{ interestsDisplayNames={{
all: _(msg`All`), all: _(msg`For You`),
...interestsDisplayNames, ...interestsDisplayNames,
}} }}
TabComponent={Tab} TabComponent={Tab}
+2
View File
@@ -19,6 +19,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows' import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {castAsShadow, type Shadow} from './types' import {castAsShadow, type Shadow} from './types'
@@ -149,6 +150,7 @@ function* findProfilesInCache(
yield* findAllProfilesInProfileQueryData(queryClient, did) yield* findAllProfilesInProfileQueryData(queryClient, did)
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did) yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did) yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
yield* findAllProfilesInActorSearchQueryData(queryClient, did) yield* findAllProfilesInActorSearchQueryData(queryClient, did)
yield* findAllProfilesInListConvosQueryData(queryClient, did) yield* findAllProfilesInListConvosQueryData(queryClient, did)
@@ -0,0 +1,71 @@
import {
type AppBskyActorDefs,
type AppBskyUnspeccedGetSuggestedUsers,
} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {
aggregateUserInterests,
createBskyTopicsHeader,
} from '#/lib/api/feed/utils'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
export type QueryProps = {category?: string | null}
export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
getSuggestedUsersQueryKeyRoot,
...Object.values(props),
]
export function useGetSuggestedUsersQuery(props: QueryProps) {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
return useQuery({
enabled: !!preferences,
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.ONE,
queryKey: createGetSuggestedUsersQueryKey(props),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
{
category: props.category ?? undefined,
},
{
headers: {
...createBskyTopicsHeader(aggregateUserInterests(preferences)),
'Accept-Language': contentLangs,
},
},
)
return data
},
})
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({
queryKey: [getSuggestedUsersQueryKeyRoot],
})
for (const [_, response] of responses) {
if (!response) {
continue
}
for (const actor of response.actors) {
if (actor.did === did) {
yield actor
}
}
}
}
+4 -4
View File
@@ -80,10 +80,10 @@
tlds "^1.234.0" tlds "^1.234.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/api@^0.14.19": "@atproto/api@^0.14.20":
version "0.14.19" version "0.14.20"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.19.tgz#fef8994e2b14e69a9e3a0aef043c7fcb34d6bf8c" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.20.tgz#904c85a91748f3203fd929415cb8fb3bc78d35d3"
integrity sha512-YYTqM0K0qk2TP7PguktPzlAQGLTL1bEGz6PgY5kqKJNX4o1318kJYB22DzjJYqV2NUCq0JQ9Lb0oskLvTisEOg== integrity sha512-Daip22+u9N+EVPk9PsEEVrTfjIqGczXnAT7o2EHGd0JsOzMbp3a6wmW1beKqYDzPf+Dc36/39JeUYYqhB3fKjg==
dependencies: dependencies:
"@atproto/common-web" "^0.4.1" "@atproto/common-web" "^0.4.1"
"@atproto/lexicon" "^0.4.10" "@atproto/lexicon" "^0.4.10"