diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index aa525e6495..ea68ea01e6 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -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' diff --git a/src/components/TrendingTopics.tsx b/src/components/TrendingTopics.tsx index 8d35e126d4..4b045c6e3e 100644 --- a/src/components/TrendingTopics.tsx +++ b/src/components/TrendingTopics.tsx @@ -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 ( - - {hasIcon && topic.type === 'starter-pack' && ( - - )} - - {/* - - {topic.type === 'tag' ? ( - - ) : topic.type === 'topic' ? ( - - ) : topic.type === 'feed' ? ( - - ) : ( - - )} - - */} - - - {topic.displayName} - - - ) -} - -export function TrendingTopicSkeleton({ - size = 'large', - index = 0, -}: { - size?: 'large' | 'small' - index?: number -}) { - const t = useTheme() - const isSmall = size === 'small' - return ( - - ) -} export function TrendingTopicLink({ topic: raw, children, ...rest }: { - topic: TrendingTopic + topic: AppBskyUnspeccedDefs.TrendView } & Omit) { 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 diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 98e8f77b2a..94c4b37dd6 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -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() { {' '} - ) : !trending?.topics ? null : ( + ) : !trending?.trends ? null : ( <> - {trending.topics.map(topic => ( + {trending.trends.map(topic => ( { ax.metric('trendingTopic:click', { context: 'interstitial', + recId: trending.recId, }) }}> diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index eaa68d9254..514e892895 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -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() diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 95f58645ad..fc6f99bd55 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -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 } - case 'recommendations': { - return - } case 'profile': { return ( : 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 : ( - <> - - - - - - Recommended - - - {!allFeeds ? ( - - - Content from across the network we think you might like. - - - ) : ( - - Feeds we think you might like. - - )} - - - - - - {isLoading ? ( - Array(RECOMMENDATIONS_COUNT) - .fill(0) - .map((_, i) => ) - ) : !trending?.suggested ? null : ( - <> - {trending.suggested.map(topic => ( - { - ax.metric('recommendedTopic:click', {context: 'explore'}) - }}> - {({hovered}) => ( - - )} - - ))} - - )} - - - - ) -} - -function isAllFeeds(topics: AppBskyUnspeccedDefs.TrendingTopic[]) { - return topics.every(topic => { - const segments = topic.link.split('/').slice(1) - return segments[0] === 'profile' && segments[2] === 'feed' - }) -} diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx index 6ae7132f18..5a3f4ab7f4 100644 --- a/src/screens/Search/modules/ExploreTrendingTopics.tsx +++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx @@ -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, + }) }} /> ))} diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts index c670802aa7..21fa738f28 100644 --- a/src/state/queries/trending/useGetTrendsQuery.ts +++ b/src/state/queries/trending/useGetTrendsQuery.ts @@ -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(trends: T[]): T[] { + const seen = new Set() + 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], diff --git a/src/state/queries/trending/useTrendingTopics.ts b/src/state/queries/trending/useTrendingTopics.ts deleted file mode 100644 index 81b25e5f94..0000000000 --- a/src/state/queries/trending/useTrendingTopics.ts +++ /dev/null @@ -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() - 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({ - 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], - ), - }) -} diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index f337edb86a..aa7bf4026c 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -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() { /> )) - ) : !trending?.topics ? null : ( + ) : !trending?.trends ? null : ( <> - {trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => ( + {trending.trends.slice(0, TRENDING_LIMIT).map((topic, i) => ( { - ax.metric('trendingTopic:click', {context: 'sidebar'}) + ax.metric('trendingTopic:click', { + context: 'sidebar', + recId: trending.recId, + }) }}> {({hovered}) => (