about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-04-04 18:44:02 -0500
committerGitHub <noreply@github.com>2025-04-04 16:44:02 -0700
commitaca89d4aea61a50697187464e88ac1b9a4ef40bd (patch)
treee7f7459996b9ea3815bc909e7f0a154ad1961a58
parenta0ff9b52aad3349b24118a0222e0f3d78e695887 (diff)
downloadvoidsky-aca89d4aea61a50697187464e88ac1b9a4ef40bd.tar.zst
[Explore] New suggested follows endpoint (#8130)
* Bump SDK

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

* Format
-rw-r--r--package.json2
-rw-r--r--src/screens/Search/Explore.tsx129
-rw-r--r--src/screens/Search/modules/ExploreSuggestedAccounts.tsx2
-rw-r--r--src/state/cache/profile-shadow.ts2
-rw-r--r--src/state/queries/trending/useGetSuggestedUsersQuery.ts71
-rw-r--r--yarn.lock8
6 files changed, 108 insertions, 106 deletions
diff --git a/package.json b/package.json
index 6f73a5581..2ff7557d1 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
     "icons:optimize": "svgo -f ./assets/icons"
   },
   "dependencies": {
-    "@atproto/api": "^0.14.19",
+    "@atproto/api": "^0.14.20",
     "@bitdrift/react-native": "^0.6.8",
     "@braintree/sanitize-url": "^6.0.2",
     "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
index 088bc5724..8c41a507a 100644
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
 import {logger} from '#/logger'
 import {type MetricEvents} from '#/logger/metrics'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useActorSearchPaginated} from '#/state/queries/actor-search'
 import {
   type FeedPreviewItem,
   useFeedPreviews,
@@ -23,8 +22,8 @@ import {
 import {useGetPopularFeedsQuery} from '#/state/queries/feed'
 import {Nux, useNux} from '#/state/queries/nuxs'
 import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
 import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
+import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
 import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
 import {useProgressGuide} from '#/state/shell/progress-guide'
 import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
@@ -57,7 +56,6 @@ import * as ModuleHeader from './components/ModuleHeader'
 import {
   SuggestedAccountsTabBar,
   SuggestedProfileCard,
-  useLoadEnoughProfiles,
 } from './modules/ExploreSuggestedAccounts'
 
 function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
@@ -144,7 +142,7 @@ type ExploreScreenItems =
   | {
       type: 'profile'
       key: string
-      profile: AppBskyActorDefs.ProfileView
+      profile: AppBskyActorDefs.ProfileViewBasic
       recId?: number
     }
   | {
@@ -203,33 +201,13 @@ export function Explore({
   const gate = useGate()
   const guide = useProgressGuide('follow-10')
   const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
+  // TODO always get at least 10 back
   const {
-    data: suggestedProfiles,
-    hasNextPage: hasNextSuggestedProfilesPage,
-    isLoading: isLoadingSuggestedProfiles,
-    isFetchingNextPage: isFetchingNextSuggestedProfilesPage,
-    error: suggestedProfilesError,
-    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,
+    data: suggestedUsers,
+    isLoading: suggestedUsersIsLoading,
+    error: suggestedUsersError,
+  } = useGetSuggestedUsersQuery({
+    category: selectedInterest,
   })
   const {
     data: feeds,
@@ -243,39 +221,6 @@ export function Explore({
   const showInterestsNux =
     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 {
     data: suggestedSPs,
     isLoading: isLoadingSuggestedSPs,
@@ -358,55 +303,42 @@ export function Explore({
       },
     })
 
-    if (!canShowSuggestedProfiles) {
+    if (suggestedUsersIsLoading) {
       i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
-    } else if (profilesError) {
+    } else if (suggestedUsersError) {
       i.push({
         type: 'error',
-        key: 'profilesError',
+        key: 'suggestedUsersError',
         message: _(msg`Failed to load suggested follows`),
-        error: cleanError(profilesError),
+        error: cleanError(suggestedUsersError),
       })
     } else {
-      if (profiles !== undefined) {
-        if (profiles.pages.length > 0 && moderationOpts) {
+      if (suggestedUsers !== undefined) {
+        if (suggestedUsers.actors.length > 0 && moderationOpts) {
           // Currently the responses contain duplicate items.
           // Needs to be fixed on backend, but let's dedupe to be safe.
           let seen = new Set()
           const profileItems: ExploreScreenItems[] = []
-          for (const page of profiles.pages) {
-            for (const actor of page.actors) {
-              if (!seen.has(actor.did) && !actor.viewer?.following) {
-                seen.add(actor.did)
-                profileItems.push({
-                  type: 'profile',
-                  key: actor.did,
-                  profile: actor,
-                  recId: page.recId,
-                })
-              }
+          for (const actor of suggestedUsers.actors) {
+            if (!seen.has(actor.did) && !actor.viewer?.following) {
+              seen.add(actor.did)
+              profileItems.push({
+                type: 'profile',
+                key: actor.did,
+                profile: actor,
+              })
             }
           }
 
           if (profileItems.length === 0) {
-            if (!hasNextProfilesPage) {
-              // no items! remove the header
-              i.pop()
-            }
+            // no items! remove the header
+            i.pop()
           } else {
             i.push(...profileItems)
           }
-          if (hasNextProfilesPage) {
-            i.push({
-              type: 'loadMore',
-              key: 'loadMoreProfiles',
-              message: _(msg`Load more suggested accounts`),
-              isLoadingMore: isLoadingMoreProfiles,
-              onLoadMore: onLoadMoreProfiles,
-            })
-          }
         } else {
-          console.log('no pages')
+          // no items! remove the header
+          i.pop()
         }
       } else {
         i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
@@ -414,14 +346,11 @@ export function Explore({
     }
     return i
   }, [
-    profiles,
     _,
-    canShowSuggestedProfiles,
-    hasNextProfilesPage,
-    isLoadingMoreProfiles,
     moderationOpts,
-    onLoadMoreProfiles,
-    profilesError,
+    suggestedUsers,
+    suggestedUsersIsLoading,
+    suggestedUsersError,
   ])
   const suggestedFeedsModule = useMemo(() => {
     const i: ExploreScreenItems[] = []
diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
index 070d75910..71210823e 100644
--- a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
+++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
@@ -83,7 +83,7 @@ export function SuggestedAccountsTabBar({
         }}
         hasSearchText={false}
         interestsDisplayNames={{
-          all: _(msg`All`),
+          all: _(msg`For You`),
           ...interestsDisplayNames,
         }}
         TabComponent={Tab}
diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts
index 84ebc565c..82ee44388 100644
--- a/src/state/cache/profile-shadow.ts
+++ b/src/state/cache/profile-shadow.ts
@@ -19,6 +19,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 findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
 import type * as bsky from '#/types/bsky'
 import {castAsShadow, type Shadow} from './types'
 
@@ -149,6 +150,7 @@ function* findProfilesInCache(
   yield* findAllProfilesInProfileQueryData(queryClient, did)
   yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
   yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
+  yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
   yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
   yield* findAllProfilesInActorSearchQueryData(queryClient, did)
   yield* findAllProfilesInListConvosQueryData(queryClient, did)
diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
new file mode 100644
index 000000000..eb97ad666
--- /dev/null
+++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
@@ -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
+      }
+    }
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index 0541080f6..88b7c8388 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -80,10 +80,10 @@
     tlds "^1.234.0"
     zod "^3.23.8"
 
-"@atproto/api@^0.14.19":
-  version "0.14.19"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.19.tgz#fef8994e2b14e69a9e3a0aef043c7fcb34d6bf8c"
-  integrity sha512-YYTqM0K0qk2TP7PguktPzlAQGLTL1bEGz6PgY5kqKJNX4o1318kJYB22DzjJYqV2NUCq0JQ9Lb0oskLvTisEOg==
+"@atproto/api@^0.14.20":
+  version "0.14.20"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.20.tgz#904c85a91748f3203fd929415cb8fb3bc78d35d3"
+  integrity sha512-Daip22+u9N+EVPk9PsEEVrTfjIqGczXnAT7o2EHGd0JsOzMbp3a6wmW1beKqYDzPf+Dc36/39JeUYYqhB3fKjg==
   dependencies:
     "@atproto/common-web" "^0.4.1"
     "@atproto/lexicon" "^0.4.10"