Pass recId with trending topic click metrics (#11232)

This commit is contained in:
DS Boyce
2026-07-22 12:51:49 -07:00
committed by GitHub
parent f62e54ec82
commit 0bd6a99613
10 changed files with 83 additions and 360 deletions
+6 -8
View File
@@ -510,7 +510,7 @@ export type Events = {
| 'ProgressGuide'
location: 'Card' | 'Profile' | 'FollowAll'
recSource?: 'Search'
recId?: number | string
recId?: string
position: number
suggestedDid: string
category: string | null
@@ -523,7 +523,7 @@ export type Events = {
| 'ProfileHeader'
| 'Onboarding'
| 'SeeMoreSuggestedUsers'
recId?: number | string
recId?: string
position: number
suggestedDid: string
category: string | null
@@ -538,7 +538,7 @@ export type Events = {
| 'SeeMoreSuggestedUsers'
| 'ProgressGuide'
recSource?: 'Search'
recId?: number | string
recId?: string
position: number
suggestedDid: string
category: string | null
@@ -550,11 +550,11 @@ export type Events = {
| 'ProfileInterstitial'
| 'ProfileHeader'
| 'Onboarding'
recId?: number | string
recId?: string
}
'suggestedUser:dismiss': {
logContext: 'DiscoverInterstitial' | 'ProfileInterstitial' | 'ProfileHeader'
recId?: number | string
recId?: string
position: number
suggestedDid: string
}
@@ -743,9 +743,7 @@ export type Events = {
}
'trendingTopic:click': {
context: 'sidebar' | 'interstitial' | 'explore'
}
'recommendedTopic:click': {
context: 'explore'
recId?: string
}
'trendingVideos:show': {
context: 'settings'
+6 -127
View File
@@ -1,143 +1,20 @@
import {useMemo} from 'react'
import {View} from 'react-native'
import {type AtUri} from '@atproto/api'
import {type AppBskyUnspeccedDefs, type AtUri} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
// import {makeProfileLink} from '#/lib/routes/links'
// import {feedUriToHref} from '#/lib/strings/url-helpers'
// import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
// import {CloseQuote_Filled_Stroke2_Corner0_Rounded as Quote} from '#/components/icons/Quote'
// import {UserAvatar} from '#/view/com/util/UserAvatar'
import {type TrendingTopic} from '#/state/queries/trending/useTrendingTopics'
import {atoms as a, native, useTheme, type ViewStyleProp} from '#/alf'
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
import {native} from '#/alf'
import {Link as InternalLink, type LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
export function TrendingTopic({
topic: raw,
size,
style,
hovered,
}: {
topic: TrendingTopic
size?: 'large' | 'small'
hovered?: boolean
} & ViewStyleProp) {
const topic = useTopic(raw)
const isSmall = size === 'small'
const hasIcon = topic.type === 'starter-pack' && !isSmall
const iconSize = 20
return (
<View
style={[
a.flex_row,
a.align_center,
isSmall
? [
{
paddingVertical: 2,
paddingHorizontal: 4,
},
]
: [a.py_xs, a.px_sm],
hasIcon && {gap: 6},
style,
]}>
{hasIcon && topic.type === 'starter-pack' && (
<StarterPackIcon
gradient="sky"
width={iconSize}
style={{marginLeft: -3, marginVertical: -1}}
/>
)}
{/*
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
a.overflow_hidden,
{
width: iconSize,
height: iconSize,
},
]}>
{topic.type === 'tag' ? (
<Hashtag width={iconSize} />
) : topic.type === 'topic' ? (
<Quote width={iconSize - 2} />
) : topic.type === 'feed' ? (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
) : (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
)}
</View>
*/}
<Text
style={[
a.font_semi_bold,
a.leading_tight,
isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}],
hovered && {textDecorationLine: 'underline'},
]}
numberOfLines={1}>
{topic.displayName}
</Text>
</View>
)
}
export function TrendingTopicSkeleton({
size = 'large',
index = 0,
}: {
size?: 'large' | 'small'
index?: number
}) {
const t = useTheme()
const isSmall = size === 'small'
return (
<View
style={[
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg_contrast_25,
isSmall
? {
width: index % 2 === 0 ? 75 : 90,
height: 27,
}
: {
width: index % 2 === 0 ? 90 : 110,
height: 36,
},
]}
/>
)
}
export function TrendingTopicLink({
topic: raw,
children,
...rest
}: {
topic: TrendingTopic
topic: AppBskyUnspeccedDefs.TrendView
} & Omit<LinkProps, 'to' | 'label'>) {
const topic = useTopic(raw)
@@ -168,7 +45,9 @@ type ParsedTrendingTopic =
uri: AtUri
}
export function useTopic(raw: TrendingTopic): ParsedTrendingTopic {
export function useTopic(
raw: AppBskyUnspeccedDefs.TrendView,
): ParsedTrendingTopic {
const {_} = useLingui()
return useMemo(() => {
const {topic: displayName, link} = raw
+15 -5
View File
@@ -7,7 +7,7 @@ import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
import {useTrendingConfig} from '#/state/service-config'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
@@ -20,6 +20,8 @@ import {TrendingTopicLink} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
const TRENDING_LIMIT = 14
export function TrendingInterstitial() {
const {enabled} = useTrendingConfig()
const {trendingDisabled} = useTrendingSettings()
@@ -33,8 +35,15 @@ export function Inner() {
const gutters = useGutters([0, 'base', 0, 'base'])
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingDisabled} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
const {
data: trending,
error,
isLoading,
} = useGetTrendsQuery({
limit: TRENDING_LIMIT,
refetchOnWindowFocus: true,
})
const noTopics = !isLoading && !error && !trending?.trends?.length
const onConfirmHide = useCallback(() => {
ax.metric('trendingTopics:hide', {context: 'interstitial'})
@@ -88,15 +97,16 @@ export function Inner() {
{' '}
</Text>
</View>
) : !trending?.topics ? null : (
) : !trending?.trends ? null : (
<>
{trending.topics.map(topic => (
{trending.trends.map(topic => (
<TrendingTopicLink
key={topic.link}
topic={topic}
onPress={() => {
ax.metric('trendingTopic:click', {
context: 'interstitial',
recId: trending.recId,
})
}}>
<View style={[a.py_lg]}>
@@ -370,7 +370,7 @@ function SuggestedProfileCard({
category: string | null
onSeen: (did: string, position: number) => void
recSource?: 'Search'
recId?: number | string
recId?: string
}) {
const t = useTheme()
const ax = useAnalytics()
-8
View File
@@ -48,7 +48,6 @@ import {
StarterPackCardSkeleton,
} from '#/screens/Search/components/StarterPackCard'
import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCard'
import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
import {atoms as a, native, platform, useTheme} from '#/alf'
@@ -154,10 +153,6 @@ type ExploreScreenItems =
type: 'trendingVideos'
key: string
}
| {
type: 'recommendations'
key: string
}
| {
type: 'profile'
key: string
@@ -807,9 +802,6 @@ export function Explore({
case 'trendingVideos': {
return <ExploreTrendingVideos />
}
case 'recommendations': {
return <ExploreRecommendations />
}
case 'profile': {
return (
<SuggestedProfileCard
@@ -1,120 +0,0 @@
import {View} from 'react-native'
import {type AppBskyUnspeccedDefs} from '@atproto/api'
import {Trans} from '@lingui/react/macro'
import {
DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/service-config'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
// Note: This module is not currently used and may be removed in the future.
export function ExploreRecommendations() {
const {enabled} = useTrendingConfig()
return enabled ? <Inner /> : null
}
function Inner() {
const t = useTheme()
const ax = useAnalytics()
const gutters = useGutters([0, 'compact'])
const {data: trending, error, isLoading} = useTrendingTopics()
const noRecs = !isLoading && !error && !trending?.suggested?.length
const allFeeds = trending?.suggested && isAllFeeds(trending.suggested)
return error || noRecs ? null : (
<>
<View
style={[
a.flex_row,
IS_WEB
? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
: [a.p_lg, a.pt_2xl, a.gap_md],
a.border_b,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_sm]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Hashtag_Stroke2_Corner0_Rounded
size="lg"
fill={t.palette.primary_500}
style={{marginLeft: -2}}
/>
<Text style={[a.text_2xl, a.font_bold, t.atoms.text]}>
<Trans>Recommended</Trans>
</Text>
</View>
{!allFeeds ? (
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>
Content from across the network we think you might like.
</Trans>
</Text>
) : (
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>Feeds we think you might like.</Trans>
</Text>
)}
</View>
</View>
<View style={[a.pt_md, a.pb_lg]}>
<View
style={[
a.flex_row,
a.justify_start,
a.flex_wrap,
{rowGap: 8, columnGap: 6},
gutters,
]}>
{isLoading ? (
Array(RECOMMENDATIONS_COUNT)
.fill(0)
.map((_, i) => <TrendingTopicSkeleton key={i} index={i} />)
) : !trending?.suggested ? null : (
<>
{trending.suggested.map(topic => (
<TrendingTopicLink
key={topic.link}
topic={topic}
onPress={() => {
ax.metric('recommendedTopic:click', {context: 'explore'})
}}>
{({hovered}) => (
<TrendingTopic
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
</View>
</>
)
}
function isAllFeeds(topics: AppBskyUnspeccedDefs.TrendingTopic[]) {
return topics.every(topic => {
const segments = topic.link.split('/').slice(1)
return segments[0] === 'profile' && segments[2] === 'feed'
})
}
@@ -45,7 +45,10 @@ function Inner() {
trend={trend}
rank={index + 1}
onPress={() => {
ax.metric('trendingTopic:click', {context: 'explore'})
ax.metric('trendingTopic:click', {
context: 'explore',
recId: trending.recId,
})
}}
/>
))}
+36 -10
View File
@@ -6,6 +6,7 @@ import {
aggregateUserInterests,
createBskyTopicsHeader,
} from '#/lib/api/feed/utils'
import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
@@ -13,24 +14,43 @@ import {useAgent} from '#/state/session'
export const DEFAULT_LIMIT = 5
export const createGetTrendsQueryKey = () => ['trends']
type QueryProps = {
limit?: number
refetchOnWindowFocus?: boolean
}
export function useGetTrendsQuery() {
function dedupe<T extends {link: string}>(trends: T[]): T[] {
const seen = new Set<string>()
return trends.filter(trend => {
if (seen.has(trend.link)) return false
seen.add(trend.link)
return true
})
}
export const createGetTrendsQueryKey = (props: QueryProps = {}) => [
'trends',
props.limit ?? DEFAULT_LIMIT,
]
export function useGetTrendsQuery(props: QueryProps = {}) {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
const limit = props.limit ?? DEFAULT_LIMIT
const mutedWords = useMemo(() => {
return preferences?.moderationPrefs?.mutedWords || []
}, [preferences?.moderationPrefs])
return useQuery({
enabled: !!preferences,
refetchOnWindowFocus: props.refetchOnWindowFocus,
staleTime: STALE.MINUTES.THREE,
queryKey: createGetTrendsQueryKey(),
queryKey: createGetTrendsQueryKey({limit}),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getTrends(
{
limit: DEFAULT_LIMIT,
limit,
},
{
headers: {
@@ -39,17 +59,23 @@ export function useGetTrendsQuery() {
},
},
)
if (!data.recIdStr) {
logger.debug('useGetTrendsQuery response missing recIdStr')
}
return data
},
select: useCallback(
(data: AppBskyUnspeccedGetTrends.OutputSchema) => {
return {
trends: (data.trends ?? []).filter(t => {
return !hasMutedWord({
mutedWords,
text: t.topic + ' ' + t.displayName + ' ' + t.category,
})
}),
recId: data.recIdStr,
trends: dedupe(
(data.trends ?? []).filter(t => {
return !hasMutedWord({
mutedWords,
text: `${t.topic} ${t.displayName} ${t.category}`,
})
}),
),
}
},
[mutedWords],
@@ -1,74 +0,0 @@
import {useCallback, useMemo} from 'react'
import {type AppBskyUnspeccedDefs, hasMutedWord} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
export type TrendingTopic = AppBskyUnspeccedDefs.TrendingTopic
type Response = {
topics: TrendingTopic[]
suggested: TrendingTopic[]
}
export const DEFAULT_LIMIT = 14
function dedup(topics: TrendingTopic[]): TrendingTopic[] {
const seen = new Set<string>()
return topics.filter(t => {
if (seen.has(t.link)) return false
seen.add(t.link)
return true
})
}
export const trendingTopicsQueryKey = ['trending-topics']
export function useTrendingTopics() {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
const mutedWords = useMemo(
() => preferences?.moderationPrefs?.mutedWords ?? [],
[preferences?.moderationPrefs?.mutedWords],
)
return useQuery<Response>({
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.THREE,
queryKey: trendingTopicsQueryKey,
async queryFn() {
const {data} = await agent.app.bsky.unspecced.getTrendingTopics({
limit: DEFAULT_LIMIT,
})
return {
topics: data.topics ?? [],
suggested: data.suggested ?? [],
}
},
select: useCallback(
(data: Response) => {
return {
topics: dedup(
data.topics.filter(t => {
return !hasMutedWord({
mutedWords,
text: `${t.topic} ${t.displayName ?? ''} ${t.description ?? ''}`,
})
}),
),
suggested: dedup(
data.suggested.filter(t => {
return !hasMutedWord({
mutedWords,
text: `${t.topic} ${t.displayName ?? ''} ${t.description ?? ''}`,
})
}),
),
}
},
[mutedWords],
),
})
}
@@ -5,7 +5,7 @@ import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
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'
@@ -30,8 +30,14 @@ function Inner() {
const ax = useAnalytics()
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingDisabled} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
const {
data: trending,
error,
isLoading,
} = useGetTrendsQuery({
refetchOnWindowFocus: true,
})
const noTopics = !isLoading && !error && !trending?.trends?.length
const onConfirmHide = () => {
ax.metric('trendingTopics:hide', {context: 'sidebar'})
@@ -82,14 +88,17 @@ function Inner() {
/>
</View>
))
) : !trending?.topics ? null : (
) : !trending?.trends ? null : (
<>
{trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => (
{trending.trends.slice(0, TRENDING_LIMIT).map((topic, i) => (
<TrendingTopicLink
key={topic.link}
topic={topic}
onPress={() => {
ax.metric('trendingTopic:click', {context: 'sidebar'})
ax.metric('trendingTopic:click', {
context: 'sidebar',
recId: trending.recId,
})
}}>
{({hovered}) => (
<View style={[a.flex_1, a.flex_row, a.gap_xs]}>