From a5f0e8839a29c68c05485ff938d90d45d4acbc4c Mon Sep 17 00:00:00 2001
From: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Date: Mon, 27 Jul 2026 16:34:15 -0700
Subject: [PATCH] Update trending topics UI (#11304)
---
oxlint-suppressions.json | 5 -
src/components/FeedInterstitials.tsx | 69 ++++---
src/components/ProfileCard.tsx | 2 +-
src/components/VideoPostCard.tsx | 43 ++--
.../interstitials/FeedTrendingTopics.tsx | 185 ++++++++++--------
.../interstitials/TrendingVideos.tsx | 55 +++---
.../Search/components/ModuleHeader.tsx | 27 +++
.../Search/modules/ExploreTrendingTopics.tsx | 80 +++++---
src/view/com/posts/PostFeed.tsx | 10 +-
.../shell/desktop/SidebarTrendingTopics.tsx | 8 +-
10 files changed, 281 insertions(+), 203 deletions(-)
diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json
index 0a3da7641f..996b647227 100644
--- a/oxlint-suppressions.json
+++ b/oxlint-suppressions.json
@@ -596,11 +596,6 @@
"count": 1
}
},
- "src/components/interstitials/TrendingVideos.tsx": {
- "typescript/no-floating-promises": {
- "count": 1
- }
- },
"src/components/verification/VerificationCreatePrompt.tsx": {
"typescript/no-misused-promises": {
"count": 1
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 7f8820f36f..fec0447b61 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -23,6 +23,7 @@ import {
atoms as a,
native,
useBreakpoints,
+ useGutters,
useTheme,
type ViewStyleProp,
web,
@@ -60,10 +61,10 @@ function CardOuter({
a.flex_1,
a.w_full,
a.p_md,
- a.rounded_lg,
+ a.rounded_xl,
a.border,
t.atoms.bg,
- t.atoms.shadow_sm,
+ t.atoms.shadow_md,
t.atoms.border_contrast_low,
!gtMobile && {
width: MOBILE_CARD_WIDTH,
@@ -204,6 +205,7 @@ export function ProfileGrid({
const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const {gtMobile} = useBreakpoints()
+ const gutters = useGutters([0, 'base'])
const followDialogControl = useDialogControl()
const isLoading = isSuggestionsLoading || !moderationOpts
@@ -414,7 +416,7 @@ export function ProfileGrid({
moderationOpts={moderationOpts}
logContext="FeedInterstitial"
withIcon={false}
- style={[a.rounded_sm]}
+ style={[a.rounded_full]}
onFollow={() => {
ax.metric('suggestedUser:follow', {
logContext,
@@ -458,40 +460,43 @@ export function ProfileGrid({
pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
-
- Suggested for you
-
-
+
+
+ Suggested for you
+
+
+
@@ -540,7 +545,7 @@ function SeeMoreSuggestedProfilesCard({onPress}: {onPress: () => void}) {
a.justify_center,
a.gap_sm,
a.p_md,
- a.rounded_lg,
+ a.rounded_xl,
{width: FINAL_CARD_WIDTH},
]}>
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index a177a33620..7b4d75164d 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -601,7 +601,7 @@ export function FollowButtonPlaceholder({style}: ViewStyleProp) {
return (
void
}) {
const t = useTheme()
- const {_, i18n} = useLingui()
+ const {t: l, i18n} = useLingui()
const embed = post.embed
const {
state: pressed,
@@ -118,8 +117,8 @@ export function VideoPostCard({
return (
-
- {_(msg`Hidden`)}
-
+ {l`Hidden`}
@@ -367,7 +364,7 @@ export function CompactVideoPostCard({
onInteract?: () => void
}) {
const t = useTheme()
- const {_, i18n} = useLingui()
+ const {t: l, i18n} = useLingui()
const embed = post.embed
const {
state: pressed,
@@ -398,7 +395,7 @@ export function CompactVideoPostCard({
return (
-
- {_(msg`Hidden`)}
-
+ {l`Hidden`}
@@ -469,7 +465,7 @@ export function CompactVideoPostCard({
-
+
+ style={[
+ a.relative,
+ a.rounded_full,
+ {
+ width: 24,
+ height: 24,
+ },
+ ]}>
+
+ <>
-
-
-
- Trending
-
-
-
- {({hovered, pressed}) => (
-
- See more
+
+
+
+
+ Trending
- )}
-
+
+
+
+ {({hovered, pressed}) => (
+
+ See more
+
+ )}
+
+
+
+
+
+
+ {isLoading || isRefetching
+ ? Array.from({length: TOPIC_COUNT}).map((_, i) => (
+
+ ))
+ : trending?.trends?.map((trend, index) => (
+ {
+ ax.metric('trendingTopic:click', {
+ context: 'interstitial',
+ })
+ }}
+ />
+ ))}
+
+
-
- {isLoading || isRefetching
- ? Array.from({length: TOPIC_COUNT}).map((_, i) => (
-
- ))
- : trending?.trends?.map((trend, index) => (
- {
- ax.metric('trendingTopic:click', {context: 'interstitial'})
- }}
- />
- ))}
-
-
+ {
+ ax.metric('trendingTopics:hide', {context: 'interstitial'})
+ setTrendingDisabled(true)
+ }}
+ />
+ >
)
}
@@ -156,12 +182,7 @@ function TrendRow({
label={l`Browse topic ${trend.displayName}`}
to={trend.link}
onPress={onPress}
- style={[
- rank < TOPIC_COUNT && a.border_b,
- {
- borderColor: t.palette.primary_100,
- },
- ]}
+ style={[rank < TOPIC_COUNT && a.border_b, t.atoms.border_contrast_low]}
PressableComponent={Pressable}>
{({hovered, pressed}) => (
<>
diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx
index 7f3a891436..d3047001a4 100644
--- a/src/components/interstitials/TrendingVideos.tsx
+++ b/src/components/interstitials/TrendingVideos.tsx
@@ -1,9 +1,7 @@
import {useCallback, useEffect, useMemo} from 'react'
import {ScrollView, View} from 'react-native'
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
-import {msg} from '@lingui/core/macro'
-import {useLingui} from '@lingui/react'
-import {Trans} from '@lingui/react/macro'
+import {Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URI} from '#/lib/constants'
@@ -14,7 +12,7 @@ import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
-import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
import {Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
@@ -35,7 +33,7 @@ const FEED_PARAMS: {
export function TrendingVideos() {
const t = useTheme()
- const {_} = useLingui()
+ const {t: l} = useLingui()
const ax = useAnalytics()
const gutters = useGutters([0, 'base'])
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
@@ -48,7 +46,7 @@ export function TrendingVideos() {
.getQueryCache()
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
if (query && query.getObserversCount() <= 1) {
- query.fetch()
+ void query.fetch()
}
}
}, [queryClient])
@@ -83,20 +81,22 @@ export function TrendingVideos() {
a.align_center,
a.justify_between,
]}>
-
- Trending Videos
-
-
+
+
+ Trending videos
+
+
+
-
-
@@ -186,7 +185,7 @@ function VideoCards({
function ViewMoreCard() {
const t = useTheme()
- const {_} = useLingui()
+ const {t: l} = useLingui()
const href = useMemo(() => {
const urip = new AtUri(VIDEO_FEED_URI)
@@ -197,16 +196,16 @@ function ViewMoreCard() {
{({pressed}) => (
+ label={l`View more trending videos`}>
diff --git a/src/screens/Search/components/ModuleHeader.tsx b/src/screens/Search/components/ModuleHeader.tsx
index cf6840dbe0..ce40af98b8 100644
--- a/src/screens/Search/components/ModuleHeader.tsx
+++ b/src/screens/Search/components/ModuleHeader.tsx
@@ -9,6 +9,7 @@ import {atoms as a, native, useTheme, type ViewStyleProp} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
import {sizes as iconSizes} from '#/components/icons/common'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
import {Link} from '#/components/Link'
import {Text, type TextProps} from '#/components/Typography'
@@ -151,6 +152,32 @@ export function SearchButton({
)
}
+export function EllipsisButton({
+ label,
+ onPress,
+}: {
+ label: string
+ onPress?: () => void
+}) {
+ return (
+
+ )
+}
+
export function PinButton({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
return (
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
index 3f17d01e04..f9425e37d4 100644
--- a/src/screens/Search/modules/ExploreTrendingTopics.tsx
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -9,7 +9,10 @@ import {
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useTrendingSettings} from '#/state/preferences/trending'
+import {
+ useTrendingSettings,
+ useTrendingSettingsApi,
+} from '#/state/preferences/trending'
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
import {useTrendingConfig} from '#/state/service-config'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
@@ -18,6 +21,7 @@ import {atoms as a, useGutters, useTheme, type ViewStyleProp} from '#/alf'
import {AvatarStack} from '#/components/AvatarStack'
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
import {Link} from '#/components/Link'
+import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
@@ -36,6 +40,9 @@ export function ExploreTrendingTopics() {
function Inner() {
const ax = useAnalytics()
+ const {t: l} = useLingui()
+ const trendingPrompt = Prompt.usePromptControl()
+ const {setTrendingDisabled} = useTrendingSettingsApi()
const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
const noTopics = !isLoading && !error && !trending?.trends?.length
const showLoading = isLoading || isRefetching
@@ -43,31 +50,48 @@ function Inner() {
if (!showLoading && (error || !trending?.trends || noTopics)) return null
return (
-
-
-
-
- Trending
-
-
- {showLoading
- ? Array.from({length: TOPIC_COUNT}).map((__, i) => (
-
- ))
- : trending?.trends.map((trend, index) => (
- {
- ax.metric('trendingTopic:click', {
- context: 'explore',
- recId: trending.recId,
- })
- }}
- />
- ))}
-
+ <>
+
+
+
+
+ Trending
+
+ trendingPrompt.open()}
+ />
+
+ {showLoading
+ ? Array.from({length: TOPIC_COUNT}).map((__, i) => (
+
+ ))
+ : trending?.trends.map((trend, index) => (
+ {
+ ax.metric('trendingTopic:click', {
+ context: 'explore',
+ recId: trending.recId,
+ })
+ }}
+ />
+ ))}
+
+
+ {
+ ax.metric('trendingTopics:hide', {context: 'explore:trending'})
+ setTrendingDisabled(true)
+ }}
+ />
+ >
)
}
@@ -112,7 +136,7 @@ export function TrendRow({
{trend.displayName}
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index f67e6a26a3..0100856307 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -211,6 +211,10 @@ export type PostFeedRef = {
// const REFRESH_AFTER = STALE.HOURS.ONE
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
+const TRENDING_TOPICS_INDEX = 5
+const TRENDING_VIDEO_INDEX = 30
+const SUGGESTED_FOR_YOU_INDEX = 15
+
let PostFeed = ({
feed,
description,
@@ -570,19 +574,19 @@ let PostFeed = ({
key: 'composerPrompt-' + sliceIndex,
})
}
- } else if (sliceIndex === 1) {
+ } else if (sliceIndex === TRENDING_TOPICS_INDEX) {
arr.push({
type: 'interstitialFeedTrendingTopics',
key: 'interstitialFeedTrendingTopics-' + sliceIndex,
})
- } else if (sliceIndex === 15) {
+ } else if (sliceIndex === TRENDING_VIDEO_INDEX) {
if (areVideoFeedsEnabled && !trendingVideoDisabled) {
arr.push({
type: 'interstitialTrendingVideos',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
- } else if (sliceIndex === 30) {
+ } else if (sliceIndex === SUGGESTED_FOR_YOU_INDEX) {
arr.push({
type: 'interstitialFollows',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx
index aa7bf4026c..8ad9a0d2cc 100644
--- a/src/view/shell/desktop/SidebarTrendingTopics.tsx
+++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx
@@ -9,7 +9,7 @@ import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
import {useTrendingConfig} from '#/state/service-config'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
-import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
import * as Prompt from '#/components/Prompt'
import {TrendingTopicLink} from '#/components/TrendingTopics'
@@ -50,7 +50,7 @@ function Inner() {
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}>
-
+
Trending