From ac939dc84ee9ec4537ecdb876c413d651a0c6e07 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:35:00 -0800 Subject: [PATCH] Update onboarding user suggestions with new endpoint (#9872) --- .../StepSuggestedAccounts/index.tsx | 5 +- .../util/useSuggestedOnboardingUsers.ts | 66 +++++++++++ src/screens/Search/util/useSuggestedUsers.ts | 9 +- src/state/cache/profile-shadow.ts | 2 + .../useGetSuggestedOnboardingUsersQuery.ts | 103 ++++++++++++++++++ .../trending/useGetSuggestedUsersQuery.ts | 11 +- 6 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 src/screens/Search/util/useSuggestedOnboardingUsers.ts create mode 100644 src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 98addf0c2f..1c82a0cf01 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -20,7 +20,7 @@ import { OnboardingTitleText, } from '#/screens/Onboarding/Layout' import {useOnboardingInternalState} from '#/screens/Onboarding/state' -import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers' +import {useSuggestedOnboardingUsers} from '#/screens/Search/util/useSuggestedOnboardingUsers' import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -64,13 +64,14 @@ export function StepSuggestedAccounts() { const interests = Object.keys(interestsDisplayNames) .sort(boostInterests(popularInterests)) .sort(boostInterests(state.interestsStepResults.selectedInterests)) + const { data: suggestedUsers, isLoading, error, isRefetching, refetch, - } = useSuggestedUsers({ + } = useSuggestedOnboardingUsers({ category: selectedInterest || (useFullExperience ? null : interests[0]), search: !useFullExperience, overrideInterests: state.interestsStepResults.selectedInterests, diff --git a/src/screens/Search/util/useSuggestedOnboardingUsers.ts b/src/screens/Search/util/useSuggestedOnboardingUsers.ts new file mode 100644 index 0000000000..2067204bac --- /dev/null +++ b/src/screens/Search/util/useSuggestedOnboardingUsers.ts @@ -0,0 +1,66 @@ +import {useMemo} from 'react' + +import {useInterestsDisplayNames} from '#/lib/interests' +import {useActorSearch} from '#/state/queries/actor-search' +import {useGetSuggestedOnboardingUsersQuery} from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery' + +/** + * Conditional hook, used in case a user is a non-english speaker, in which + * case we fall back to searching for users instead of our more curated set. + */ +export function useSuggestedOnboardingUsers({ + category = null, + search = false, + overrideInterests, +}: { + category?: string | null + /** + * If true, we'll search for users using the translated value of `category`, + * based on the user's app language setting + */ + search?: boolean + /** + * In onboarding, interests haven't been saved to prefs yet, so we need to + * pass them down through here + */ + overrideInterests: string[] +}) { + const interestsDisplayNames = useInterestsDisplayNames() + const curated = useGetSuggestedOnboardingUsersQuery({ + enabled: !search, + category, + overrideInterests, + }) + const searched = useActorSearch({ + enabled: !!search, + // use user's app language translation for this value + query: category ? interestsDisplayNames[category] : '', + limit: 10, + }) + + return useMemo(() => { + if (search) { + return { + // we're not paginating right now + data: searched?.data + ? { + actors: searched.data.pages.flatMap(p => p.actors) ?? [], + recId: undefined, + } + : undefined, + isLoading: searched.isLoading, + error: searched.error, + isRefetching: searched.isRefetching, + refetch: searched.refetch, + } + } else { + return { + data: curated.data, + isLoading: curated.isLoading, + error: curated.error, + isRefetching: curated.isRefetching, + refetch: curated.refetch, + } + } + }, [curated, searched, search]) +} diff --git a/src/screens/Search/util/useSuggestedUsers.ts b/src/screens/Search/util/useSuggestedUsers.ts index ea7ff2d9c0..557baff70b 100644 --- a/src/screens/Search/util/useSuggestedUsers.ts +++ b/src/screens/Search/util/useSuggestedUsers.ts @@ -11,25 +11,18 @@ import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggeste export function useSuggestedUsers({ category = null, search = false, - overrideInterests, }: { category?: string | null /** * If true, we'll search for users using the translated value of `category`, - * based on the user's "app language setting + * based on the user's app language setting */ search?: boolean - /** - * In onboarding, interests haven't been saved to prefs yet, so we need to - * pass them down through here - */ - overrideInterests?: string[] }) { const interestsDisplayNames = useInterestsDisplayNames() const curated = useGetSuggestedUsersQuery({ enabled: !search, category, - overrideInterests, }) const searched = useActorSearch({ enabled: !!search, diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 0fdef08ad0..2308316c9b 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -25,6 +25,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '# import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows' +import {findAllProfilesInQueryData as findAllProfilesInSuggestedOnboardingUsersQueryData} from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery' import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery' import {findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' import type * as bsky from '#/types/bsky' @@ -247,6 +248,7 @@ function* findProfilesInCache( yield* findAllProfilesInProfileQueryData(queryClient, did) yield* findAllProfilesInProfileFollowersQueryData(queryClient, did) yield* findAllProfilesInProfileFollowsQueryData(queryClient, did) + yield* findAllProfilesInSuggestedOnboardingUsersQueryData(queryClient, did) yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did) yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) yield* findAllProfilesInActorSearchQueryData(queryClient, did) diff --git a/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts new file mode 100644 index 0000000000..0834063371 --- /dev/null +++ b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts @@ -0,0 +1,103 @@ +import { + type AppBskyActorDefs, + type AppBskyUnspeccedGetSuggestedOnboardingUsers, +} from '@atproto/api' +import {type QueryClient, useQuery} from '@tanstack/react-query' + +import {createBskyTopicsHeader} from '#/lib/api/feed/utils' +import {logger} from '#/logger' +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 + limit?: number + enabled?: boolean + overrideInterests: string[] +} + +export const getSuggestedOnboardingUsersQueryKeyRoot = + 'unspecced-suggested-onboarding-users' +export const createGetSuggestedOnboardingUsersQueryKey = ( + props: QueryProps, +) => [ + getSuggestedOnboardingUsersQueryKeyRoot, + props.category, + props.limit, + props.overrideInterests.join(','), +] + +export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) { + const agent = useAgent() + const {data: preferences} = usePreferencesQuery() + + return useQuery({ + enabled: !!preferences && props.enabled !== false, + staleTime: STALE.MINUTES.THREE, + queryKey: createGetSuggestedOnboardingUsersQueryKey(props), + queryFn: async () => { + const contentLangs = getContentLanguages().join(',') + + const overrideInterests = props.overrideInterests.join(',') + + const {data} = await agent.app.bsky.unspecced.getSuggestedOnboardingUsers( + { + category: props.category ?? undefined, + limit: props.limit || 10, + }, + { + headers: { + ...createBskyTopicsHeader(overrideInterests), + 'Accept-Language': contentLangs, + }, + }, + ) + // FALLBACK: if no results for 'all', try again with no interests specified + if (!props.category && data.actors.length === 0) { + logger.error( + `Did not get any suggested onboarding users, falling back - interests: ${overrideInterests}`, + ) + const {data: fallbackData} = + await agent.app.bsky.unspecced.getSuggestedOnboardingUsers( + { + category: props.category ?? undefined, + limit: props.limit || 10, + }, + { + headers: { + 'Accept-Language': contentLangs, + }, + }, + ) + return fallbackData + } + + return data + }, + }) +} + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const responses = + queryClient.getQueriesData( + { + queryKey: [getSuggestedOnboardingUsersQueryKeyRoot], + }, + ) + for (const [_key, response] of responses) { + if (!response) { + continue + } + + for (const actor of response.actors) { + if (actor.did === did) { + yield actor + } + } + } +} diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts index 547d7f44fd..30090b9a54 100644 --- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts @@ -18,7 +18,6 @@ export type QueryProps = { category?: string | null limit?: number enabled?: boolean - overrideInterests?: string[] } export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users' @@ -26,7 +25,6 @@ export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [ getSuggestedUsersQueryKeyRoot, props.category, props.limit, - props.overrideInterests?.join(','), ] export function useGetSuggestedUsersQuery(props: QueryProps) { @@ -41,11 +39,6 @@ export function useGetSuggestedUsersQuery(props: QueryProps) { const contentLangs = getContentLanguages().join(',') const userInterests = aggregateUserInterests(preferences) - const interests = - props.overrideInterests && props.overrideInterests.length > 0 - ? props.overrideInterests.join(',') - : userInterests - const {data} = await agent.app.bsky.unspecced.getSuggestedUsers( { category: props.category ?? undefined, @@ -53,7 +46,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) { }, { headers: { - ...createBskyTopicsHeader(interests), + ...createBskyTopicsHeader(userInterests), 'Accept-Language': contentLangs, }, }, @@ -61,7 +54,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) { // FALLBACK: if no results for 'all', try again with no interests specified if (!props.category && data.actors.length === 0) { logger.error( - `Did not get any suggested users, falling back - interests: ${interests}`, + `Did not get any suggested users, falling back - interests: ${userInterests}`, ) const {data: fallbackData} = await agent.app.bsky.unspecced.getSuggestedUsers(