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