diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 775eedd769..e75faa3c71 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -2,12 +2,17 @@ import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import {TextInput, View, type ViewToken} from 'react-native' import {type ModerationOpts} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' +import {useQueryClient} from '@tanstack/react-query' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorSearch} from '#/state/queries/actor-search' import {usePreferencesQuery} from '#/state/queries/preferences' +import { + getAllDidsInDiscoverCache, + useGetSuggestedUsersForDiscoverQuery, +} from '#/state/queries/trending/useGetSuggestedUsersForDiscoverQuery' import {useGetSuggestedUsersForSeeMoreQuery} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery' import {useSession} from '#/state/session' import {type Follow10ProgressGuide} from '#/state/shell/progress-guide' @@ -109,21 +114,33 @@ export function FollowDialogWithoutGuide({ let lastSelectedInterest = '' let lastSearchText = '' +const FOR_YOU_TAB = 'all' + function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { const {t: l} = useLingui() const ax = useAnalytics() - const interestsDisplayNames = useInterestsDisplayNames() + const queryClient = useQueryClient() + const rawInterestsDisplayNames = useInterestsDisplayNames() const {data: preferences} = usePreferencesQuery() const personalizedInterests = preferences?.interests?.tags - const interests = Object.keys(interestsDisplayNames) - .sort(boostInterests(popularInterests)) - .sort(boostInterests(personalizedInterests)) + const interests = useMemo( + () => [ + FOR_YOU_TAB, + ...Object.keys(rawInterestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(personalizedInterests)), + ], + [rawInterestsDisplayNames, personalizedInterests], + ) + const interestsDisplayNames = useMemo( + () => ({ + [FOR_YOU_TAB]: l`For You`, + ...rawInterestsDisplayNames, + }), + [l, rawInterestsDisplayNames], + ) const [selectedInterest, setSelectedInterest] = useState( - () => - lastSelectedInterest || - (personalizedInterests && interests.includes(personalizedInterests[0]) - ? personalizedInterests[0] - : interests[0]), + () => lastSelectedInterest || FOR_YOU_TAB, ) const [searchText, setSearchText] = useState(lastSearchText) const moderationOpts = useModerationOpts() @@ -137,14 +154,31 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { lastSelectedInterest = selectedInterest }, [searchText, selectedInterest]) - const { - data: suggestions, - isFetching: isFetchingSuggestions, - error: suggestionsError, - } = useGetSuggestedUsersForSeeMoreQuery({ - category: selectedInterest, + const isForYou = selectedInterest === FOR_YOU_TAB + + // Snapshot the DIDs already shown by the home-feed Discover interstitial at + // dialog open time. The endpoint has no cursor; we dedup client-side so the + // For You tab doesn't re-show profiles the viewer just saw. Snapshotting + // once (lazy init) prevents our own `limit: 50` fetch from excluding itself + // after it lands in the shared cache. + const [alreadyShownDids] = useState(() => + getAllDidsInDiscoverCache(queryClient), + ) + + const discoverQuery = useGetSuggestedUsersForDiscoverQuery({ limit: 50, + enabled: isForYou, }) + const seeMoreQuery = useGetSuggestedUsersForSeeMoreQuery({ + category: isForYou ? undefined : selectedInterest, + limit: 50, + enabled: !isForYou, + }) + const suggestions = isForYou ? discoverQuery.data : seeMoreQuery.data + const isFetchingSuggestions = isForYou + ? discoverQuery.isFetching + : seeMoreQuery.isFetching + const suggestionsError = isForYou ? discoverQuery.error : seeMoreQuery.error const { data: searchResults, isFetching: isFetchingSearchResults, @@ -188,6 +222,10 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { if (seen.has(profile.did)) continue if (profile.did === currentAccount?.did) continue if (profile.viewer?.following) continue + // On the For You tab, skip profiles the viewer was already shown by + // the home-feed Discover interstitial. + if (isForYou && !hasSearchText && alreadyShownDids.has(profile.did)) + continue seen.add(profile.did) @@ -222,6 +260,8 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { hasSearchText, resultsKey, isSearchResultsError, + isForYou, + alreadyShownDids, ]) const isGuide = Boolean(guide) @@ -277,7 +317,10 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { recId: recIdForLogging, position: position !== -1 ? position : 0, suggestedDid: item.profile.did, - category: selectedInterestRef.current, + category: + selectedInterestRef.current === FOR_YOU_TAB + ? null + : selectedInterestRef.current, }) } } diff --git a/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts index efd02a1dce..517df06756 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts @@ -15,21 +15,23 @@ import {useAgent} from '#/state/session' export type QueryProps = { limit?: number + enabled?: boolean } export const getSuggestedUsersForDiscoverQueryKeyRoot = 'unspecced-suggested-users-for-explore' -export const createGetSuggestedUsersForDiscoverQueryKey = ( - props: QueryProps, -) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit] +export const createGetSuggestedUsersForDiscoverQueryKey = (props: { + limit?: number +}) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit] export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) { const agent = useAgent() const {data: preferences} = usePreferencesQuery() return useQuery({ + enabled: props.enabled ?? true, staleTime: STALE.MINUTES.THREE, - queryKey: createGetSuggestedUsersForDiscoverQueryKey(props), + queryKey: createGetSuggestedUsersForDiscoverQueryKey({limit: props.limit}), queryFn: async () => { const contentLangs = getContentLanguages().join(',') const userInterests = aggregateUserInterests(preferences) @@ -73,3 +75,33 @@ export function* findAllProfilesInQueryData( } } } + +/** + * Collects every DID currently cached under any `getSuggestedUsersForDiscover` + * query. Used by the See More dialog's For You tab to filter out profiles the + * viewer has already been shown by the home-feed interstitial (the endpoint + * has no cursor/offset, so we dedup client-side). + * + * The query key root is shared with `getSuggestedUsersForSeeMore`, so we + * narrow by key length (Discover keys are `[root, limit]`, SeeMore keys are + * `[root, category, limit]`). + */ +export function getAllDidsInDiscoverCache( + queryClient: QueryClient, +): Set { + const dids = new Set() + const responses = + queryClient.getQueriesData( + { + queryKey: [getSuggestedUsersForDiscoverQueryKeyRoot], + }, + ) + for (const [key, response] of responses) { + if (!response) continue + if (key.length !== 2) continue + for (const actor of response.actors) { + dids.add(actor.did) + } + } + return dids +} diff --git a/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts index eec392d4c9..e816f0cedb 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts @@ -16,21 +16,27 @@ import {useAgent} from '#/state/session' export type QueryProps = { category?: string | null limit?: number + enabled?: boolean } export const getSuggestedUsersForSeeMoreQueryKeyRoot = 'unspecced-suggested-users-for-explore' -export const createGetSuggestedUsersForSeeMoreQueryKey = ( - props: QueryProps, -) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit] +export const createGetSuggestedUsersForSeeMoreQueryKey = (props: { + category?: string | null + limit?: number +}) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit] export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) { const agent = useAgent() const {data: preferences} = usePreferencesQuery() return useQuery({ + enabled: props.enabled ?? true, staleTime: STALE.MINUTES.THREE, - queryKey: createGetSuggestedUsersForSeeMoreQueryKey(props), + queryKey: createGetSuggestedUsersForSeeMoreQueryKey({ + category: props.category, + limit: props.limit, + }), queryFn: async () => { const contentLangs = getContentLanguages().join(',') const userInterests = aggregateUserInterests(preferences)