about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/components/SubtleHover.tsx34
-rw-r--r--src/screens/Search/Explore.tsx147
-rw-r--r--src/screens/Search/components/ModuleHeader.tsx10
-rw-r--r--src/screens/Search/components/StarterPackCard.tsx140
-rw-r--r--src/screens/Search/modules/ExploreInterestsCard.tsx8
-rw-r--r--src/screens/Search/modules/ExploreSuggestedAccounts.tsx93
-rw-r--r--src/screens/Search/modules/ExploreTrendingTopics.tsx25
-rw-r--r--src/state/queries/explore-feed-previews.tsx66
-rw-r--r--src/state/queries/trending/useGetSuggestedFeedsQuery.ts3
-rw-r--r--src/state/queries/trending/useGetSuggestedUsersQuery.ts4
-rw-r--r--src/state/queries/trending/useGetTrendsQuery.ts1
-rw-r--r--src/state/queries/useSuggestedStarterPacksQuery.ts3
12 files changed, 344 insertions, 190 deletions
diff --git a/src/components/SubtleHover.tsx b/src/components/SubtleHover.tsx
new file mode 100644
index 000000000..bb5911baa
--- /dev/null
+++ b/src/components/SubtleHover.tsx
@@ -0,0 +1,34 @@
+import {View} from 'react-native'
+
+import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
+
+export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
+  const t = useTheme()
+
+  let opacity: number
+  switch (t.name) {
+    case 'dark':
+      opacity = 0.4
+      break
+    case 'dim':
+      opacity = 0.45
+      break
+    case 'light':
+      opacity = 0.5
+      break
+  }
+
+  return (
+    <View
+      style={[
+        a.absolute,
+        a.inset_0,
+        a.pointer_events_none,
+        a.transition_opacity,
+        t.atoms.bg_contrast_25,
+        style,
+        {opacity: hover ? opacity : 0},
+      ]}
+    />
+  )
+}
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
index 61ec36785..e29b85f76 100644
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -7,6 +7,7 @@ import {
 } from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
 
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {useGate} from '#/lib/statsig/statsig'
@@ -22,9 +23,19 @@ import {
 import {useGetPopularFeedsQuery} from '#/state/queries/feed'
 import {Nux, useNux} from '#/state/queries/nuxs'
 import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
-import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
-import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
+import {
+  createGetSuggestedFeedsQueryKey,
+  useGetSuggestedFeedsQuery,
+} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
+import {
+  getSuggestedUsersQueryKeyRoot,
+  useGetSuggestedUsersQuery,
+} from '#/state/queries/trending/useGetSuggestedUsersQuery'
+import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
+import {
+  createSuggestedStarterPacksQueryKey,
+  useSuggestedStarterPacksQuery,
+} from '#/state/queries/useSuggestedStarterPacksQuery'
 import {useProgressGuide} from '#/state/shell/progress-guide'
 import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
 import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
@@ -41,6 +52,7 @@ import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommenda
 import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
 import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
 import {atoms as a, native, platform, useTheme, web} from '#/alf'
+import {Admonition} from '#/components/Admonition'
 import {Button} from '#/components/Button'
 import * as FeedCard from '#/components/FeedCard'
 import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
@@ -49,9 +61,11 @@ import {type Props as IcoProps} from '#/components/icons/common'
 import {type Props as SVGIconProps} from '#/components/icons/common'
 import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
 import {StarterPack} from '#/components/icons/StarterPack'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
 import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
 import {Loader} from '#/components/Loader'
 import * as ProfileCard from '#/components/ProfileCard'
+import {SubtleHover} from '#/components/SubtleHover'
 import {Text} from '#/components/Typography'
 import * as ModuleHeader from './components/ModuleHeader'
 import {
@@ -69,33 +83,26 @@ function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
       onPress={item.onLoadMore}
       style={[a.relative, a.w_full]}>
       {({hovered, pressed}) => (
-        <View
-          style={[
-            a.flex_1,
-            a.flex_row,
-            a.align_center,
-            a.justify_center,
-            a.px_lg,
-            a.py_md,
-            a.gap_sm,
-            (hovered || pressed) && t.atoms.bg_contrast_25,
-          ]}>
-          <Text
+        <>
+          <SubtleHover hover={hovered || pressed} />
+          <View
             style={[
-              a.leading_snug,
-              hovered ? t.atoms.text : t.atoms.text_contrast_medium,
+              a.flex_1,
+              a.flex_row,
+              a.align_center,
+              a.justify_center,
+              a.px_lg,
+              a.py_md,
+              a.gap_sm,
             ]}>
-            {item.message}
-          </Text>
-          {item.isLoadingMore ? (
-            <Loader size="sm" />
-          ) : (
-            <ChevronDownIcon
-              size="sm"
-              style={hovered ? t.atoms.text : t.atoms.text_contrast_medium}
-            />
-          )}
-        </View>
+            <Text style={[a.leading_snug]}>{item.message}</Text>
+            {item.isLoadingMore ? (
+              <Loader size="sm" />
+            ) : (
+              <ChevronDownIcon size="sm" style={t.atoms.text_contrast_medium} />
+            )}
+          </View>
+        </>
       )}
     </Button>
   )
@@ -112,6 +119,7 @@ type ExploreScreenItems =
       title: string
       icon: React.ComponentType<SVGIconProps>
       iconSize?: IcoProps['size']
+      bottomBorder?: boolean
       searchButton?: {
         label: string
         metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
@@ -148,6 +156,10 @@ type ExploreScreenItems =
       recId?: number
     }
   | {
+      type: 'profileEmpty'
+      key: 'profileEmpty'
+    }
+  | {
       type: 'feed'
       key: string
       feed: AppBskyFeedDefs.GeneratorView
@@ -203,11 +215,12 @@ 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
+  // TODO always get at least 10 back TODO still
   const {
     data: suggestedUsers,
     isLoading: suggestedUsersIsLoading,
     error: suggestedUsersError,
+    isRefetching: suggestedUsersIsRefetching,
   } = useGetSuggestedUsersQuery({
     category: selectedInterest,
   })
@@ -227,6 +240,7 @@ export function Explore({
     data: suggestedSPs,
     isLoading: isLoadingSuggestedSPs,
     error: suggestedSPsError,
+    isRefetching: isRefetchingSuggestedSPs,
   } = useSuggestedStarterPacksQuery()
 
   const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
@@ -262,6 +276,27 @@ export function Explore({
     },
   } = useFeedPreviews(suggestedFeeds?.feeds ?? [])
 
+  const qc = useQueryClient()
+  const [isPTR, setIsPTR] = useState(false)
+  const onPTR = useCallback(async () => {
+    setIsPTR(true)
+    await Promise.all([
+      await qc.resetQueries({
+        queryKey: createGetTrendsQueryKey(),
+      }),
+      await qc.resetQueries({
+        queryKey: createSuggestedStarterPacksQueryKey(),
+      }),
+      await qc.resetQueries({
+        queryKey: [getSuggestedUsersQueryKeyRoot],
+      }),
+      await qc.resetQueries({
+        queryKey: createGetSuggestedFeedsQueryKey(),
+      }),
+    ])
+    setIsPTR(false)
+  }, [qc, setIsPTR])
+
   const onLoadMoreFeedPreviews = useCallback(async () => {
     if (
       isPendingFeedPreviews ||
@@ -305,7 +340,7 @@ export function Explore({
       },
     })
 
-    if (suggestedUsersIsLoading) {
+    if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
       i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
     } else if (suggestedUsersError) {
       i.push({
@@ -333,14 +368,18 @@ export function Explore({
           }
 
           if (profileItems.length === 0) {
-            // no items! remove the header
-            i.pop()
+            i.push({
+              type: 'profileEmpty',
+              key: 'profileEmpty',
+            })
           } else {
             i.push(...profileItems)
           }
         } else {
-          // no items! remove the header
-          i.pop()
+          i.push({
+            type: 'profileEmpty',
+            key: 'profileEmpty',
+          })
         }
       } else {
         i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
@@ -352,6 +391,7 @@ export function Explore({
     moderationOpts,
     suggestedUsers,
     suggestedUsersIsLoading,
+    suggestedUsersIsRefetching,
     suggestedUsersError,
   ])
   const suggestedFeedsModule = useMemo(() => {
@@ -466,7 +506,7 @@ export function Explore({
       iconSize: 'xl',
     })
 
-    if (isLoadingSuggestedSPs) {
+    if (isLoadingSuggestedSPs || isRefetchingSuggestedSPs) {
       Array.from({length: 3}).forEach((__, index) =>
         i.push({
           type: 'starterPackSkeleton',
@@ -486,7 +526,13 @@ export function Explore({
       })
     }
     return i
-  }, [suggestedSPs, _, isLoadingSuggestedSPs, suggestedSPsError])
+  }, [
+    suggestedSPs,
+    _,
+    isLoadingSuggestedSPs,
+    suggestedSPsError,
+    isRefetchingSuggestedSPs,
+  ])
   const feedPreviewsModule = useMemo(() => {
     const i: ExploreScreenItems[] = []
     i.push(...feedPreviewSlices)
@@ -520,6 +566,13 @@ export function Explore({
     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)
@@ -533,6 +586,7 @@ export function Explore({
 
     return i
   }, [
+    _,
     topBorder,
     isNewUser,
     suggestedFollowsModule,
@@ -564,7 +618,7 @@ export function Explore({
           )
         case 'header': {
           return (
-            <ModuleHeader.Container>
+            <ModuleHeader.Container bottomBorder={item.bottomBorder}>
               <ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
               <ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
               {item.searchButton && (
@@ -623,6 +677,15 @@ export function Explore({
             />
           )
         }
+        case 'profileEmpty': {
+          return (
+            <View style={[a.px_lg, a.pb_lg]}>
+              <Admonition>
+                <Trans>No results for "{selectedInterest}".</Trans>
+              </Admonition>
+            </View>
+          )
+        }
         case 'feed': {
           return (
             <View
@@ -738,7 +801,13 @@ export function Explore({
           return (
             <ModuleHeader.Container
               headerHeight={headerHeight}
-              style={[a.pt_xs, a.border_b, t.atoms.border_contrast_low]}>
+              style={[
+                a.pt_xs,
+                t.atoms.border_contrast_low,
+                native(a.border_b),
+              ]}>
+              {/* Very non-scientific way to avoid small gap on scroll */}
+              <View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
               <ModuleHeader.FeedLink feed={item.feed}>
                 <ModuleHeader.FeedAvatar feed={item.feed} />
                 <View style={[a.flex_1, a.gap_xs]}>
@@ -876,6 +945,8 @@ export function Explore({
       windowSize={9}
       maxToRenderPerBatch={platform({ios: 5, default: 1})}
       updateCellsBatchingPeriod={40}
+      refreshing={isPTR}
+      onRefresh={onPTR}
     />
   )
 }
diff --git a/src/screens/Search/components/ModuleHeader.tsx b/src/screens/Search/components/ModuleHeader.tsx
index c6411d1c0..9c208d2b2 100644
--- a/src/screens/Search/components/ModuleHeader.tsx
+++ b/src/screens/Search/components/ModuleHeader.tsx
@@ -18,7 +18,12 @@ export function Container({
   style,
   children,
   headerHeight,
-}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
+  bottomBorder,
+}: {
+  children: React.ReactNode
+  headerHeight?: number
+  bottomBorder?: boolean
+} & ViewStyleProp) {
   const t = useTheme()
   return (
     <View
@@ -31,10 +36,9 @@ export function Container({
         a.gap_sm,
         t.atoms.bg,
         headerHeight && web({position: 'sticky', top: headerHeight}),
+        bottomBorder && [a.border_b, t.atoms.border_contrast_low],
         style,
       ]}>
-      {/* Very non-scientific way to avoid small gap on scroll */}
-      <View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
       {children}
     </View>
   )
diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx
index 1b9f94828..fcb0ef068 100644
--- a/src/screens/Search/components/StarterPackCard.tsx
+++ b/src/screens/Search/components/StarterPackCard.tsx
@@ -19,6 +19,7 @@ import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
 import {Link} from '#/components/Link'
 import {MediaInsetBorder} from '#/components/MediaInsetBorder'
 import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
+import {SubtleHover} from '#/components/SubtleHover'
 import {Text} from '#/components/Typography'
 import * as bsky from '#/types/bsky'
 
@@ -48,75 +49,80 @@ export function StarterPackCard({
     .map(item => item.subject)
 
   return (
-    <View
-      style={[
-        a.w_full,
-        a.p_lg,
-        a.gap_md,
-        a.border,
-        a.rounded_sm,
-        a.overflow_hidden,
-        t.atoms.border_contrast_low,
-      ]}>
-      <View aria-hidden style={[a.absolute, a.inset_0, a.z_40]}>
-        <Link
-          to={link.to}
-          label={link.label}
-          style={[a.absolute, a.inset_0]}
-          onHoverIn={link.precache}
-          onPress={link.precache}>
-          <View />
-        </Link>
-      </View>
+    <Link
+      to={link.to}
+      label={link.label}
+      onHoverIn={link.precache}
+      onPress={link.precache}>
+      {s => (
+        <>
+          <SubtleHover hover={s.hovered || s.pressed} />
 
-      <AvatarStack
-        profiles={profiles ?? []}
-        numPending={profileCount}
-        total={view.list?.listItemCount}
-      />
+          <View
+            style={[
+              a.w_full,
+              a.p_lg,
+              a.gap_md,
+              a.border,
+              a.rounded_sm,
+              a.overflow_hidden,
+              t.atoms.border_contrast_low,
+            ]}>
+            <AvatarStack
+              profiles={profiles ?? []}
+              numPending={profileCount}
+              total={view.list?.listItemCount}
+            />
 
-      <View
-        style={[
-          a.w_full,
-          a.flex_row,
-          a.align_start,
-          a.gap_lg,
-          web({
-            position: 'static',
-            zIndex: 'unset',
-          }),
-        ]}>
-        <View style={[a.flex_1]}>
-          <Text
-            emoji
-            style={[a.text_md, a.font_bold, a.leading_snug]}
-            numberOfLines={1}>
-            {view.record.name}
-          </Text>
-          <Text
-            emoji
-            style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
-            numberOfLines={1}>
-            {view.creator?.did === currentAccount?.did
-              ? _(msg`By you`)
-              : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
-          </Text>
-        </View>
-        <Link
-          to={link.to}
-          label={link.label}
-          onHoverIn={link.precache}
-          onPress={link.precache}
-          variant="solid"
-          color="secondary"
-          size="small"
-          style={[a.z_50]}>
-          <ButtonText>
-            <Trans>Open pack</Trans>
-          </ButtonText>
-        </Link>
-      </View>
-    </View>
+            <View
+              style={[
+                a.w_full,
+                a.flex_row,
+                a.align_start,
+                a.gap_lg,
+                web({
+                  position: 'static',
+                  zIndex: 'unset',
+                }),
+              ]}>
+              <View style={[a.flex_1]}>
+                <Text
+                  emoji
+                  style={[a.text_md, a.font_bold, a.leading_snug]}
+                  numberOfLines={1}>
+                  {view.record.name}
+                </Text>
+                <Text
+                  emoji
+                  style={[
+                    a.text_sm,
+                    a.leading_snug,
+                    t.atoms.text_contrast_medium,
+                  ]}
+                  numberOfLines={1}>
+                  {view.creator?.did === currentAccount?.did
+                    ? _(msg`By you`)
+                    : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
+                </Text>
+              </View>
+              <Link
+                to={link.to}
+                label={link.label}
+                onHoverIn={link.precache}
+                onPress={link.precache}
+                variant="solid"
+                color="secondary"
+                size="small"
+                style={[a.z_50]}>
+                <ButtonText>
+                  <Trans>Open pack</Trans>
+                </ButtonText>
+              </Link>
+            </View>
+          </View>
+        </>
+      )}
+    </Link>
   )
 }
 
diff --git a/src/screens/Search/modules/ExploreInterestsCard.tsx b/src/screens/Search/modules/ExploreInterestsCard.tsx
index 00a15111a..00014ffc6 100644
--- a/src/screens/Search/modules/ExploreInterestsCard.tsx
+++ b/src/screens/Search/modules/ExploreInterestsCard.tsx
@@ -40,14 +40,14 @@ export function ExploreInterestsCard() {
     <>
       <Prompt.Basic
         control={trendingPrompt}
-        title={_(msg`Your interests`)}
+        title={_(msg`Dismiss interests`)}
         description={_(
-          msg`You can adjust your interests at any time from your "Content and media" settings.`,
+          msg`You can adjust your interests at any time from "Content and media" settings.`,
         )}
         confirmButtonCta={_(
           msg({
-            message: `Copy that!`,
-            comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`,
+            message: `OK`,
+            comment: `Confirm button text.`,
           }),
         )}
         onConfirm={onConfirmClose}
diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
index 6d36ef1a7..8d66dfbc1 100644
--- a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
+++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
@@ -17,6 +17,7 @@ import {atoms as a} from '#/alf'
 import {Button} from '#/components/Button'
 import * as ProfileCard from '#/components/ProfileCard'
 import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
+import {SubtleHover} from '#/components/SubtleHover'
 import {Text} from '#/components/Typography'
 import type * as bsky from '#/types/bsky'
 
@@ -133,10 +134,7 @@ let Tab = ({
               a.py_sm,
               a.border,
               active || hovered || pressed || focused
-                ? [
-                    t.atoms.bg_contrast_25,
-                    {borderColor: t.atoms.bg_contrast_25.backgroundColor},
-                  ]
+                ? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
                 : [t.atoms.bg, t.atoms.border_contrast_low],
             ]}>
             <Text
@@ -186,47 +184,52 @@ let SuggestedProfileCard = ({
           {statsig: true},
         )
       }}>
-      <View
-        style={[
-          a.w_full,
-          a.py_lg,
-          a.px_lg,
-          a.border_t,
-          t.atoms.border_contrast_low,
-          a.flex_1,
-        ]}>
-        <ProfileCard.Outer>
-          <ProfileCard.Header>
-            <ProfileCard.Avatar
-              profile={profile}
-              moderationOpts={moderationOpts}
-            />
-            <ProfileCard.NameAndHandle
-              profile={profile}
-              moderationOpts={moderationOpts}
-            />
-            <ProfileCard.FollowButton
-              profile={profile}
-              moderationOpts={moderationOpts}
-              withIcon={false}
-              logContext="ExploreSuggestedAccounts"
-              onFollow={() => {
-                logger.metric(
-                  'suggestedUser:follow',
-                  {
-                    logContext: 'Explore',
-                    location: 'Card',
-                    recId,
-                    position,
-                  },
-                  {statsig: true},
-                )
-              }}
-            />
-          </ProfileCard.Header>
-          <ProfileCard.Description profile={profile} numberOfLines={2} />
-        </ProfileCard.Outer>
-      </View>
+      {s => (
+        <>
+          <SubtleHover hover={s.hovered || s.pressed} />
+          <View
+            style={[
+              a.flex_1,
+              a.w_full,
+              a.py_lg,
+              a.px_lg,
+              a.border_t,
+              t.atoms.border_contrast_low,
+            ]}>
+            <ProfileCard.Outer>
+              <ProfileCard.Header>
+                <ProfileCard.Avatar
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+                <ProfileCard.NameAndHandle
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+                <ProfileCard.FollowButton
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                  withIcon={false}
+                  logContext="ExploreSuggestedAccounts"
+                  onFollow={() => {
+                    logger.metric(
+                      'suggestedUser:follow',
+                      {
+                        logContext: 'Explore',
+                        location: 'Card',
+                        recId,
+                        position,
+                      },
+                      {statsig: true},
+                    )
+                  }}
+                />
+              </ProfileCard.Header>
+              <ProfileCard.Description profile={profile} numberOfLines={2} />
+            </ProfileCard.Outer>
+          </View>
+        </>
+      )}
     </ProfileCard.Link>
   )
 }
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
index 75ca19351..167f6d193 100644
--- a/src/screens/Search/modules/ExploreTrendingTopics.tsx
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -17,6 +17,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
 import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
 import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
 import {Link} from '#/components/Link'
+import {SubtleHover} from '#/components/SubtleHover'
 import {Text} from '#/components/Typography'
 
 const TOPIC_COUNT = 5
@@ -28,10 +29,10 @@ export function ExploreTrendingTopics() {
 }
 
 function Inner() {
-  const {data: trending, error, isLoading} = useGetTrendsQuery()
+  const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
   const noTopics = !isLoading && !error && !trending?.trends?.length
 
-  return isLoading ? (
+  return isLoading || isRefetching ? (
     Array.from({length: TOPIC_COUNT}).map((__, i) => (
       <TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
     ))
@@ -92,25 +93,23 @@ export function TrendRow({
       PressableComponent={Pressable}>
       {({hovered, pressed}) => (
         <>
-          <View
-            style={[
-              gutters,
-              a.w_full,
-              a.py_lg,
-              a.flex_row,
-              a.gap_2xs,
-              (hovered || pressed) && t.atoms.bg_contrast_25,
-            ]}>
+          <SubtleHover hover={hovered || pressed} />
+          <View style={[gutters, a.w_full, a.py_lg, a.flex_row, a.gap_2xs]}>
             <View style={[a.flex_1, a.gap_xs]}>
               <View style={[a.flex_row]}>
                 <Text
-                  style={[a.text_md, a.font_bold, a.leading_snug, {width: 20}]}>
+                  style={[
+                    a.text_md,
+                    a.font_bold,
+                    a.leading_tight,
+                    {width: 20},
+                  ]}>
                   <Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
                     {rank}.
                   </Trans>
                 </Text>
                 <Text
-                  style={[a.text_md, a.font_bold, a.leading_snug]}
+                  style={[a.text_md, a.font_bold, a.leading_tight]}
                   numberOfLines={1}>
                   {trend.displayName}
                 </Text>
diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx
index fcf9194db..2aee8b6b3 100644
--- a/src/state/queries/explore-feed-previews.tsx
+++ b/src/state/queries/explore-feed-previews.tsx
@@ -34,6 +34,41 @@ const RQKEY_ROOT = 'feed-previews'
 const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
 
 const LIMIT = 8 // sliced to 6, overfetch to account for moderation
+const PINNED_POST_URIS: Record<string, boolean> = {
+  // 📰 News
+  'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b':
+    true,
+  // Gardening
+  'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27':
+    true,
+  // Web Development Trending
+  'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22':
+    true,
+  // Anime & Manga EN
+  'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y':
+    true,
+  // 📽️ Film
+  'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g':
+    true,
+  // PopSky
+  'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g':
+    true,
+  // Science
+  'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s':
+    true,
+  // Birds! 🦉
+  'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d':
+    true,
+  // Astronomy
+  'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w':
+    true,
+  // What's Cooking 🍽️
+  'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q':
+    true,
+  // BookSky 💙📚 #booksky
+  'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e':
+    true,
+}
 
 export type FeedPreviewItem =
   | {
@@ -181,19 +216,24 @@ export function useFeedPreviews(
                 feedContext: item.feedContext,
                 reason: item.reason,
                 feedPostUri: item.feedPostUri,
-                items: item.items.slice(0, 6).map((subItem, i) => {
-                  const feedPostSliceItem: FeedPostSliceItem = {
-                    _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
-                    uri: subItem.post.uri,
-                    post: subItem.post,
-                    record: subItem.record,
-                    moderation: moderations[i],
-                    parentAuthor: subItem.parentAuthor,
-                    isParentBlocked: subItem.isParentBlocked,
-                    isParentNotFound: subItem.isParentNotFound,
-                  }
-                  return feedPostSliceItem
-                }),
+                items: item.items
+                  .slice(0, 6)
+                  .filter(subItem => {
+                    return !PINNED_POST_URIS[subItem.post.uri]
+                  })
+                  .map((subItem, i) => {
+                    const feedPostSliceItem: FeedPostSliceItem = {
+                      _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
+                      uri: subItem.post.uri,
+                      post: subItem.post,
+                      record: subItem.record,
+                      moderation: moderations[i],
+                      parentAuthor: subItem.parentAuthor,
+                      isParentBlocked: subItem.isParentBlocked,
+                      isParentNotFound: subItem.isParentNotFound,
+                    }
+                    return feedPostSliceItem
+                  }),
               }
               if (slice.isIncompleteThread && slice.items.length >= 3) {
                 const beforeLast = slice.items.length - 2
diff --git a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
index 55b633af0..eef71f1ca 100644
--- a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
@@ -20,8 +20,7 @@ export function useGetSuggestedFeedsQuery() {
 
   return useQuery({
     enabled: !!preferences,
-    refetchOnWindowFocus: true,
-    staleTime: STALE.MINUTES.ONE,
+    staleTime: STALE.MINUTES.THREE,
     queryKey: createGetSuggestedFeedsQueryKey(),
     queryFn: async () => {
       const contentLangs = getContentLanguages().join(',')
diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
index eb97ad666..c8c3f0089 100644
--- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
@@ -27,14 +27,14 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
 
   return useQuery({
     enabled: !!preferences,
-    refetchOnWindowFocus: true,
-    staleTime: STALE.MINUTES.ONE,
+    staleTime: STALE.MINUTES.THREE,
     queryKey: createGetSuggestedUsersQueryKey(props),
     queryFn: async () => {
       const contentLangs = getContentLanguages().join(',')
       const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
         {
           category: props.category ?? undefined,
+          limit: 10,
         },
         {
           headers: {
diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts
index 02386a505..94a5b0cba 100644
--- a/src/state/queries/trending/useGetTrendsQuery.ts
+++ b/src/state/queries/trending/useGetTrendsQuery.ts
@@ -25,7 +25,6 @@ export function useGetTrendsQuery() {
 
   return useQuery({
     enabled: !!preferences,
-    refetchOnWindowFocus: true,
     staleTime: STALE.MINUTES.THREE,
     queryKey: createGetTrendsQueryKey(),
     queryFn: async () => {
diff --git a/src/state/queries/useSuggestedStarterPacksQuery.ts b/src/state/queries/useSuggestedStarterPacksQuery.ts
index cda3c28ab..3ec030ac0 100644
--- a/src/state/queries/useSuggestedStarterPacksQuery.ts
+++ b/src/state/queries/useSuggestedStarterPacksQuery.ts
@@ -20,8 +20,7 @@ export function useSuggestedStarterPacksQuery() {
 
   return useQuery({
     enabled: !!preferences,
-    refetchOnWindowFocus: true,
-    staleTime: STALE.MINUTES.ONE,
+    staleTime: STALE.MINUTES.THREE,
     queryKey: createSuggestedStarterPacksQueryKey(),
     async queryFn() {
       const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(