about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json2
-rw-r--r--src/screens/Search/Explore.tsx106
-rw-r--r--src/screens/Search/modules/ExploreSuggestedAccounts.tsx20
-rw-r--r--src/screens/Search/util/useSuggestedUsers.ts56
-rw-r--r--src/state/queries/actor-search.ts2
-rw-r--r--src/state/queries/explore-feed-previews.tsx3
-rw-r--r--src/state/queries/feed.ts22
-rw-r--r--src/state/queries/trending/useGetSuggestedFeedsQuery.ts4
-rw-r--r--src/state/queries/trending/useGetSuggestedUsersQuery.ts8
-rw-r--r--src/state/queries/useSuggestedStarterPacksQuery.ts4
-rw-r--r--yarn.lock8
11 files changed, 173 insertions, 62 deletions
diff --git a/package.json b/package.json
index cfe79d32b..9b874c3a9 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
     "icons:optimize": "svgo -f ./assets/icons"
   },
   "dependencies": {
-    "@atproto/api": "^0.14.20",
+    "@atproto/api": "^0.14.21",
     "@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 1236005a3..8050d7f73 100644
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -8,13 +8,16 @@ import {
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
+import * as bcp47Match from 'bcp-47-match'
 
 import {useGate} from '#/lib/statsig/statsig'
 import {cleanError} from '#/lib/strings/errors'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {logger} from '#/logger'
 import {type MetricEvents} from '#/logger/metrics'
+import {useLanguagePrefs} from '#/state/preferences/languages'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {RQKEY_ROOT_PAGINATED as useActorSearchPaginatedQueryKeyRoot} from '#/state/queries/actor-search'
 import {
   type FeedPreviewItem,
   useFeedPreviews,
@@ -26,10 +29,7 @@ import {
   createGetSuggestedFeedsQueryKey,
   useGetSuggestedFeedsQuery,
 } from '#/state/queries/trending/useGetSuggestedFeedsQuery'
-import {
-  getSuggestedUsersQueryKeyRoot,
-  useGetSuggestedUsersQuery,
-} from '#/state/queries/trending/useGetSuggestedUsersQuery'
+import {getSuggestedUsersQueryKeyRoot} from '#/state/queries/trending/useGetSuggestedUsersQuery'
 import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
 import {
   createSuggestedStarterPacksQueryKey,
@@ -43,6 +43,10 @@ import {List} from '#/view/com/util/List'
 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
 import {
+  popularInterests,
+  useInterestsDisplayNames,
+} from '#/screens/Onboarding/state'
+import {
   StarterPackCard,
   StarterPackCardSkeleton,
 } from '#/screens/Search/components/StarterPackCard'
@@ -50,6 +54,7 @@ import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCar
 import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
 import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
 import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
+import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
 import {atoms as a, native, platform, useTheme} from '#/alf'
 import {Admonition} from '#/components/Admonition'
 import {Button} from '#/components/Button'
@@ -64,6 +69,7 @@ import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Tre
 import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
 import {Loader} from '#/components/Loader'
 import * as ProfileCard from '#/components/ProfileCard'
+import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
 import {SubtleHover} from '#/components/SubtleHover'
 import {Text} from '#/components/Typography'
 import * as ModuleHeader from './components/ModuleHeader'
@@ -135,6 +141,7 @@ type ExploreScreenItems =
         metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
         tab: 'user' | 'profile' | 'feed'
       }
+      hideDefaultTab?: boolean
     }
   | {
       type: 'trendingTopics'
@@ -151,7 +158,7 @@ type ExploreScreenItems =
   | {
       type: 'profile'
       key: string
-      profile: AppBskyActorDefs.ProfileViewBasic
+      profile: AppBskyActorDefs.ProfileView
       recId?: number
     }
   | {
@@ -212,14 +219,31 @@ export function Explore({
   const gate = useGate()
   const guide = useProgressGuide('follow-10')
   const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
+
+  /*
+   * Begin special language handling
+   */
+  const {contentLanguages} = useLanguagePrefs()
+  const useFullExperience = useMemo(() => {
+    if (contentLanguages.length === 0) return true
+    return bcp47Match.basicFilter('en', contentLanguages).length > 0
+  }, [contentLanguages])
+  const personalizedInterests = preferences?.interests?.tags
+  const interestsDisplayNames = useInterestsDisplayNames()
+  const interests = Object.keys(interestsDisplayNames)
+    .sort(boostInterests(popularInterests))
+    .sort(boostInterests(personalizedInterests))
   const {
     data: suggestedUsers,
     isLoading: suggestedUsersIsLoading,
     error: suggestedUsersError,
     isRefetching: suggestedUsersIsRefetching,
-  } = useGetSuggestedUsersQuery({
-    category: selectedInterest,
+  } = useSuggestedUsers({
+    category: selectedInterest || (useFullExperience ? null : interests[0]),
+    search: !useFullExperience,
   })
+  /* End special language handling */
+
   const {
     data: feeds,
     hasNextPage: hasNextFeedsPage,
@@ -227,7 +251,7 @@ export function Explore({
     isFetchingNextPage: isFetchingNextFeedsPage,
     error: feedsError,
     fetchNextPage: fetchNextFeedsPage,
-  } = useGetPopularFeedsQuery({limit: 10})
+  } = useGetPopularFeedsQuery({limit: 10, enabled: useFullExperience})
   const interestsNux = useNux(Nux.ExploreInterestsCard)
   const showInterestsNux =
     interestsNux.status === 'ready' && !interestsNux.nux?.completed
@@ -237,7 +261,7 @@ export function Explore({
     isLoading: isLoadingSuggestedSPs,
     error: suggestedSPsError,
     isRefetching: isRefetchingSuggestedSPs,
-  } = useSuggestedStarterPacksQuery()
+  } = useSuggestedStarterPacksQuery({enabled: useFullExperience})
 
   const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
   const [hasPressedLoadMoreFeeds, setHasPressedLoadMoreFeeds] = useState(false)
@@ -260,7 +284,9 @@ export function Explore({
     hasPressedLoadMoreFeeds,
   ])
 
-  const {data: suggestedFeeds} = useGetSuggestedFeedsQuery()
+  const {data: suggestedFeeds} = useGetSuggestedFeedsQuery({
+    enabled: useFullExperience,
+  })
   const {
     data: feedPreviewSlices,
     query: {
@@ -270,7 +296,7 @@ export function Explore({
       hasNextPage: hasNextPageFeedPreviews,
       error: feedPreviewSlicesError,
     },
-  } = useFeedPreviews(suggestedFeeds?.feeds ?? [])
+  } = useFeedPreviews(suggestedFeeds?.feeds ?? [], useFullExperience)
 
   const qc = useQueryClient()
   const [isPTR, setIsPTR] = useState(false)
@@ -287,6 +313,9 @@ export function Explore({
         queryKey: [getSuggestedUsersQueryKeyRoot],
       }),
       await qc.resetQueries({
+        queryKey: [useActorSearchPaginatedQueryKeyRoot],
+      }),
+      await qc.resetQueries({
         queryKey: createGetSuggestedFeedsQueryKey(),
       }),
     ])
@@ -334,6 +363,7 @@ export function Explore({
         metricsTag: 'suggestedAccounts',
         tab: 'user',
       },
+      hideDefaultTab: !useFullExperience,
     })
 
     if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
@@ -353,6 +383,7 @@ export function Explore({
           let seen = new Set()
           const profileItems: ExploreScreenItems[] = []
           for (const actor of suggestedUsers.actors) {
+            // checking for following still necessary if search data is used
             if (!seen.has(actor.did) && !actor.viewer?.following) {
               seen.add(actor.did)
               profileItems.push({
@@ -369,7 +400,7 @@ export function Explore({
               key: 'profileEmpty',
             })
           } else {
-            if (selectedInterest === null) {
+            if (selectedInterest === null && useFullExperience) {
               // First "For You" tab, only show 5 to keep screen short
               i.push(...profileItems.slice(0, 5))
             } else {
@@ -395,6 +426,7 @@ export function Explore({
     suggestedUsersIsRefetching,
     suggestedUsersError,
     selectedInterest,
+    useFullExperience,
   ])
   const suggestedFeedsModule = useMemo(() => {
     const i: ExploreScreenItems[] = []
@@ -565,26 +597,31 @@ export function Explore({
 
     i.push(topBorder)
     i.push(...interestsNuxModule)
-    if (isNewUser) {
-      i.push(...suggestedFollowsModule)
-      i.push(...suggestedStarterPacksModule)
-      i.push({
-        type: 'header',
-        key: 'trending-topics-header',
-        title: _(msg`Trending topics`),
-        icon: Graph,
-        bottomBorder: true,
-      })
-      i.push(trendingTopicsModule)
+
+    if (useFullExperience) {
+      if (isNewUser) {
+        i.push(...suggestedFollowsModule)
+        i.push(...suggestedStarterPacksModule)
+        i.push({
+          type: 'header',
+          key: 'trending-topics-header',
+          title: _(msg`Trending topics`),
+          icon: Graph,
+          bottomBorder: true,
+        })
+        i.push(trendingTopicsModule)
+      } else {
+        i.push(trendingTopicsModule)
+        i.push(...suggestedFollowsModule)
+        i.push(...suggestedStarterPacksModule)
+      }
+      if (gate('explore_show_suggested_feeds')) {
+        i.push(...suggestedFeedsModule)
+      }
+      i.push(...feedPreviewsModule)
     } else {
-      i.push(trendingTopicsModule)
       i.push(...suggestedFollowsModule)
-      i.push(...suggestedStarterPacksModule)
     }
-    if (gate('explore_show_suggested_feeds')) {
-      i.push(...suggestedFeedsModule)
-    }
-    i.push(...feedPreviewsModule)
 
     return i
   }, [
@@ -598,6 +635,7 @@ export function Explore({
     feedPreviewsModule,
     interestsNuxModule,
     gate,
+    useFullExperience,
   ])
 
   const renderItem = useCallback(
@@ -641,6 +679,7 @@ export function Explore({
               <SuggestedAccountsTabBar
                 selectedInterest={selectedInterest}
                 onSelectInterest={setSelectedInterest}
+                hideDefaultTab={item.hideDefaultTab}
               />
             </View>
           )
@@ -672,7 +711,13 @@ export function Explore({
           return (
             <View style={[a.px_lg, a.pb_lg]}>
               <Admonition>
-                <Trans>No results for "{selectedInterest}".</Trans>
+                {selectedInterest ? (
+                  <Trans>
+                    No results for "{interestsDisplayNames[selectedInterest]}".
+                  </Trans>
+                ) : (
+                  <Trans>No results.</Trans>
+                )}
               </Admonition>
             </View>
           )
@@ -876,6 +921,7 @@ export function Explore({
       focusSearchInput,
       moderationOpts,
       selectedInterest,
+      interestsDisplayNames,
       _,
       fetchNextPageFeedPreviews,
     ],
diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
index 8d66dfbc1..f91877143 100644
--- a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
+++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
@@ -58,9 +58,11 @@ export function useLoadEnoughProfiles({
 export function SuggestedAccountsTabBar({
   selectedInterest,
   onSelectInterest,
+  hideDefaultTab,
 }: {
   selectedInterest: string | null
   onSelectInterest: (interest: string | null) => void
+  hideDefaultTab?: boolean
 }) {
   const {_} = useLingui()
   const interestsDisplayNames = useInterestsDisplayNames()
@@ -72,8 +74,10 @@ export function SuggestedAccountsTabBar({
   return (
     <BlockDrawerGesture>
       <Tabs
-        interests={['all', ...interests]}
-        selectedInterest={selectedInterest || 'all'}
+        interests={hideDefaultTab ? interests : ['all', ...interests]}
+        selectedInterest={
+          selectedInterest || (hideDefaultTab ? interests[0] : 'all')
+        }
         onSelectTab={tab => {
           logger.metric(
             'explore:suggestedAccounts:tabPressed',
@@ -83,10 +87,14 @@ export function SuggestedAccountsTabBar({
           onSelectInterest(tab === 'all' ? null : tab)
         }}
         hasSearchText={false}
-        interestsDisplayNames={{
-          all: _(msg`For You`),
-          ...interestsDisplayNames,
-        }}
+        interestsDisplayNames={
+          hideDefaultTab
+            ? interestsDisplayNames
+            : {
+                all: _(msg`For You`),
+                ...interestsDisplayNames,
+              }
+        }
         TabComponent={Tab}
         contentContainerStyle={[
           {
diff --git a/src/screens/Search/util/useSuggestedUsers.ts b/src/screens/Search/util/useSuggestedUsers.ts
new file mode 100644
index 000000000..aa29dad8c
--- /dev/null
+++ b/src/screens/Search/util/useSuggestedUsers.ts
@@ -0,0 +1,56 @@
+import {useMemo} from 'react'
+
+import {useActorSearchPaginated} from '#/state/queries/actor-search'
+import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
+import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
+
+/**
+ * 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 useSuggestedUsers({
+  category = null,
+  search = false,
+}: {
+  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
+}) {
+  const interestsDisplayNames = useInterestsDisplayNames()
+  const curated = useGetSuggestedUsersQuery({
+    enabled: !search,
+    category,
+  })
+  const searched = useActorSearchPaginated({
+    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) ?? [],
+            }
+          : undefined,
+        isLoading: searched.isLoading,
+        error: searched.error,
+        isRefetching: searched.isRefetching,
+      }
+    } else {
+      return {
+        data: curated.data,
+        isLoading: curated.isLoading,
+        error: curated.error,
+        isRefetching: curated.isRefetching,
+      }
+    }
+  }, [curated, searched, search])
+}
diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts
index 0b5de2303..5347ca0a1 100644
--- a/src/state/queries/actor-search.ts
+++ b/src/state/queries/actor-search.ts
@@ -17,7 +17,7 @@ import {useAgent} from '#/state/session'
 const RQKEY_ROOT = 'actor-search'
 export const RQKEY = (query: string) => [RQKEY_ROOT, query]
 
-const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
+export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
 export const RQKEY_PAGINATED = (query: string, limit?: number) => [
   RQKEY_ROOT_PAGINATED,
   query,
diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx
index 2aee8b6b3..4cd7336c0 100644
--- a/src/state/queries/explore-feed-previews.tsx
+++ b/src/state/queries/explore-feed-previews.tsx
@@ -120,6 +120,7 @@ export type FeedPreviewItem =
 
 export function useFeedPreviews(
   feedsMaybeWithDuplicates: AppBskyFeedDefs.GeneratorView[],
+  isEnabled: boolean = true,
 ) {
   const feeds = useMemo(
     () =>
@@ -135,7 +136,7 @@ export function useFeedPreviews(
   const {data: preferences} = usePreferencesQuery()
   const userInterests = aggregateUserInterests(preferences)
   const moderationOpts = useModerationOpts()
-  const enabled = feeds.length > 0
+  const enabled = feeds.length > 0 && isEnabled
 
   const query = useInfiniteQuery({
     enabled,
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index 5571c0949..89023e513 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -1,18 +1,18 @@
 import {useCallback, useEffect, useMemo, useRef} from 'react'
 import {
-  AppBskyActorDefs,
-  AppBskyFeedDefs,
-  AppBskyGraphDefs,
-  AppBskyUnspeccedGetPopularFeedGenerators,
+  type AppBskyActorDefs,
+  type AppBskyFeedDefs,
+  type AppBskyGraphDefs,
+  type AppBskyUnspeccedGetPopularFeedGenerators,
   AtUri,
   moderateFeedGenerator,
   RichText,
 } from '@atproto/api'
 import {
-  InfiniteData,
+  type InfiniteData,
   keepPreviousData,
-  QueryClient,
-  QueryKey,
+  type QueryClient,
+  type QueryKey,
   useInfiniteQuery,
   useMutation,
   useQuery,
@@ -28,7 +28,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useAgent, useSession} from '#/state/session'
 import {router} from '#/routes'
 import {useModerationOpts} from '../preferences/moderation-opts'
-import {FeedDescriptor} from './post-feed'
+import {type FeedDescriptor} from './post-feed'
 import {precacheResolvedUri} from './resolve-uri'
 
 export type FeedSourceFeedInfo = {
@@ -203,12 +203,12 @@ export const KNOWN_AUTHED_ONLY_FEEDS = [
   'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why
 ]
 
-type GetPopularFeedsOptions = {limit?: number}
+type GetPopularFeedsOptions = {limit?: number; enabled?: boolean}
 
 export function createGetPopularFeedsQueryKey(
   options?: GetPopularFeedsOptions,
 ) {
-  return ['getPopularFeeds', options]
+  return ['getPopularFeeds', options?.limit]
 }
 
 export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
@@ -237,7 +237,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
     QueryKey,
     string | undefined
   >({
-    enabled: Boolean(moderationOpts),
+    enabled: Boolean(moderationOpts) && options?.enabled !== false,
     queryKey: createGetPopularFeedsQueryKey(options),
     queryFn: async ({pageParam}) => {
       const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
diff --git a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
index eef71f1ca..6eef80942 100644
--- a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
@@ -13,13 +13,13 @@ export const DEFAULT_LIMIT = 5
 
 export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']
 
-export function useGetSuggestedFeedsQuery() {
+export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
   const agent = useAgent()
   const {data: preferences} = usePreferencesQuery()
   const savedFeeds = preferences?.savedFeeds
 
   return useQuery({
-    enabled: !!preferences,
+    enabled: !!preferences && enabled !== false,
     staleTime: STALE.MINUTES.THREE,
     queryKey: createGetSuggestedFeedsQueryKey(),
     queryFn: async () => {
diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
index c8c3f0089..695e53f52 100644
--- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
@@ -13,12 +13,12 @@ import {STALE} from '#/state/queries'
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useAgent} from '#/state/session'
 
-export type QueryProps = {category?: string | null}
+export type QueryProps = {category?: string | null; enabled?: boolean}
 
 export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
 export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
   getSuggestedUsersQueryKeyRoot,
-  ...Object.values(props),
+  props.category,
 ]
 
 export function useGetSuggestedUsersQuery(props: QueryProps) {
@@ -26,7 +26,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
   const {data: preferences} = usePreferencesQuery()
 
   return useQuery({
-    enabled: !!preferences,
+    enabled: !!preferences && props.enabled,
     staleTime: STALE.MINUTES.THREE,
     queryKey: createGetSuggestedUsersQueryKey(props),
     queryFn: async () => {
@@ -52,7 +52,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
 export function* findAllProfilesInQueryData(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
+): Generator<AppBskyActorDefs.ProfileView, void> {
   const responses =
     queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({
       queryKey: [getSuggestedUsersQueryKeyRoot],
diff --git a/src/state/queries/useSuggestedStarterPacksQuery.ts b/src/state/queries/useSuggestedStarterPacksQuery.ts
index 3ec030ac0..cbf61b93a 100644
--- a/src/state/queries/useSuggestedStarterPacksQuery.ts
+++ b/src/state/queries/useSuggestedStarterPacksQuery.ts
@@ -13,13 +13,13 @@ export const createSuggestedStarterPacksQueryKey = () => [
   'suggested-starter-packs',
 ]
 
-export function useSuggestedStarterPacksQuery() {
+export function useSuggestedStarterPacksQuery({enabled}: {enabled?: boolean}) {
   const agent = useAgent()
   const {data: preferences} = usePreferencesQuery()
   const contentLangs = getContentLanguages().join(',')
 
   return useQuery({
-    enabled: !!preferences,
+    enabled: !!preferences && enabled !== false,
     staleTime: STALE.MINUTES.THREE,
     queryKey: createSuggestedStarterPacksQueryKey(),
     async queryFn() {
diff --git a/yarn.lock b/yarn.lock
index ee4a80a24..297a815ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -80,10 +80,10 @@
     tlds "^1.234.0"
     zod "^3.23.8"
 
-"@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==
+"@atproto/api@^0.14.21":
+  version "0.14.21"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.21.tgz#29c189b7dba316945cf7317b9ded49b1b60d3ad9"
+  integrity sha512-hCIcjks/snscH3ZtZFoicQN2hRM5MpWQUvvzyIa265XQ2vSv5BP+gsQVIHWtYKt+gzwq1E7jY4us6c4N7fsLlQ==
   dependencies:
     "@atproto/common-web" "^0.4.1"
     "@atproto/lexicon" "^0.4.10"