Create new trending feeds interstitial (#11207)
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import {useMemo} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
|
||||
import {useTrendingConfig} from '#/state/service-config'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {
|
||||
atoms as a,
|
||||
useGutters,
|
||||
useLayoutBreakpoints,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {alpha} from '#/alf/utils'
|
||||
import {AvatarStack} from '#/components/AvatarStack'
|
||||
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'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
|
||||
const TOPIC_COUNT = 3
|
||||
|
||||
export function FeedTrendingTopicsInterstitial() {
|
||||
const {enabled} = useTrendingConfig()
|
||||
const {trendingDisabled} = useTrendingSettings()
|
||||
const {rightNavVisible} = useLayoutBreakpoints()
|
||||
|
||||
return enabled && !trendingDisabled && !rightNavVisible ? <Inner /> : null
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const gutters = useGutters([0, 'base'])
|
||||
const ax = useAnalytics()
|
||||
const {
|
||||
data: trending,
|
||||
error,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
} = useGetTrendsQuery({limit: TOPIC_COUNT})
|
||||
const noTopics = !isLoading && !error && !trending?.trends?.length
|
||||
|
||||
const shadowColor = alpha(t.palette.primary_100, 0.5)
|
||||
|
||||
const gradient = {
|
||||
values: [
|
||||
[0, t.atoms.bg.backgroundColor],
|
||||
[0.1, t.palette.primary_25],
|
||||
[0.9, t.palette.primary_25],
|
||||
[1, t.atoms.bg.backgroundColor],
|
||||
],
|
||||
hover_value: t.palette.white,
|
||||
}
|
||||
|
||||
if (error || noTopics) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
gutters,
|
||||
a.pt_lg,
|
||||
a.pb_xl,
|
||||
a.gap_sm,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={gradient.values.map(c => c[1]) as [string, string, ...string[]]}
|
||||
locations={
|
||||
gradient.values.map(c => c[0]) as [number, number, ...number[]]
|
||||
}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.px_xs,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_xs]}>
|
||||
<TrendingIcon width={18} />
|
||||
<Text
|
||||
style={[a.text_md, a.font_medium, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
<Trans>Trending</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Link label={l`See more trending topics`} to="/search">
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_high,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
<Trans>See more</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_10,
|
||||
a.border,
|
||||
a.rounded_xl,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderColor: t.palette.primary_100,
|
||||
boxShadow: `0 0 16px 0 ${shadowColor}`,
|
||||
elevation: 8,
|
||||
shadowColor: shadowColor,
|
||||
shadowOffset: {width: 0, height: 0},
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 16,
|
||||
},
|
||||
]}>
|
||||
{isLoading || isRefetching
|
||||
? Array.from({length: TOPIC_COUNT}).map((_, i) => (
|
||||
<TrendingTopicRowSkeleton key={i} rank={i + 1} />
|
||||
))
|
||||
: trending?.trends?.map((trend, index) => (
|
||||
<TrendRow
|
||||
key={trend.link}
|
||||
trend={trend}
|
||||
rank={index + 1}
|
||||
onPress={() => {
|
||||
ax.metric('trendingTopic:click', {context: 'interstitial'})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function TrendRow({
|
||||
trend,
|
||||
rank,
|
||||
onPress,
|
||||
}: ViewStyleProp & {
|
||||
trend: AppBskyUnspeccedDefs.TrendView
|
||||
rank: number
|
||||
children?: React.ReactNode
|
||||
onPress?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l, i18n} = useLingui()
|
||||
|
||||
const actors = useModerateTrendingActors(trend.actors)
|
||||
|
||||
return (
|
||||
<Link
|
||||
testID={trend.link}
|
||||
label={l`Browse topic ${trend.displayName}`}
|
||||
to={trend.link}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
rank < TOPIC_COUNT && a.border_b,
|
||||
{
|
||||
borderColor: t.palette.primary_100,
|
||||
},
|
||||
]}
|
||||
PressableComponent={Pressable}>
|
||||
{({hovered, pressed}) => (
|
||||
<>
|
||||
<SubtleHover hover={hovered || pressed} native />
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.flex_row,
|
||||
{
|
||||
gap: 6,
|
||||
padding: 14,
|
||||
paddingLeft: 16,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_low,
|
||||
{
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
]}>
|
||||
<Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
|
||||
{rank}.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_1, a.gap_xs]}>
|
||||
<Text style={[a.text_md, a.font_medium]} numberOfLines={1}>
|
||||
{trend.displayName}
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
{actors.length > 0 ? (
|
||||
<AvatarStack size={24} profiles={actors} />
|
||||
) : null}
|
||||
<Text
|
||||
style={[a.text_sm, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{trend.postCount >= 1000 ? (
|
||||
<Trans comment="Over 1,000 posts">1K+ posts</Trans>
|
||||
) : (
|
||||
<Trans comment="'{postCount} {posts}', e.g., '1.2K posts'">
|
||||
{formatCount(i18n, trend.postCount)}{' '}
|
||||
{plural(trend.postCount, {one: 'post', other: 'posts'})}
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
function TrendingTopicRowSkeleton({rank}: {rank: number}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.px_lg,
|
||||
a.py_lg,
|
||||
a.flex_row,
|
||||
rank < TOPIC_COUNT && a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
{
|
||||
gap: 6,
|
||||
},
|
||||
]}>
|
||||
<LoadingPlaceholder width={17} height={17} style={[a.rounded_full]} />
|
||||
<View style={[a.flex_1, a.gap_xs]}>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
<LoadingPlaceholder width={70} height={17} />
|
||||
<LoadingPlaceholder width={40} height={17} />
|
||||
<LoadingPlaceholder width={60} height={17} />
|
||||
</View>
|
||||
<LoadingPlaceholder width={24} height={24} style={[a.rounded_full]} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function useModerateTrendingActors(
|
||||
actors: AppBskyUnspeccedDefs.TrendView['actors'],
|
||||
) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!moderationOpts) return []
|
||||
|
||||
return actors
|
||||
.filter(actor => {
|
||||
const decision = moderateProfile(actor, moderationOpts)
|
||||
return !decision.ui('avatar').filter && !decision.ui('avatar').blur
|
||||
})
|
||||
.slice(0, 3)
|
||||
}, [actors, moderationOpts])
|
||||
}
|
||||
@@ -3,11 +3,9 @@ import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||
import {atoms as a, useLayoutBreakpoints} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times'
|
||||
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide'
|
||||
import {useUserPreferencedLiveEvents} from '#/features/liveEvents/context'
|
||||
@@ -17,16 +15,10 @@ import {type LiveEventFeed} from '#/features/liveEvents/types'
|
||||
export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||
const events = useUserPreferencedLiveEvents()
|
||||
const {rightNavVisible} = useLayoutBreakpoints()
|
||||
const {trendingDisabled} = useTrendingSettings()
|
||||
|
||||
if (!events.feeds.length) {
|
||||
if (!rightNavVisible && !trendingDisabled) {
|
||||
// only show trending on mobile when live event banner is not shown
|
||||
return <TrendingInterstitial />
|
||||
} else {
|
||||
// no feed, no trending
|
||||
return null
|
||||
}
|
||||
// no feed
|
||||
return null
|
||||
}
|
||||
|
||||
// On desktop, we show in the sidebar
|
||||
|
||||
@@ -28,10 +28,8 @@ function dedupe<T extends {link: string}>(trends: T[]): T[] {
|
||||
})
|
||||
}
|
||||
|
||||
export const createGetTrendsQueryKey = (props: QueryProps = {}) => [
|
||||
'trends',
|
||||
props.limit ?? DEFAULT_LIMIT,
|
||||
]
|
||||
export const createGetTrendsQueryKey = (limit?: number) =>
|
||||
limit === undefined ? ['trends'] : ['trends', {limit}]
|
||||
|
||||
export function useGetTrendsQuery(props: QueryProps = {}) {
|
||||
const agent = useAgent()
|
||||
@@ -45,7 +43,7 @@ export function useGetTrendsQuery(props: QueryProps = {}) {
|
||||
enabled: !!preferences,
|
||||
refetchOnWindowFocus: props.refetchOnWindowFocus,
|
||||
staleTime: STALE.MINUTES.THREE,
|
||||
queryKey: createGetTrendsQueryKey({limit}),
|
||||
queryKey: createGetTrendsQueryKey(limit),
|
||||
queryFn: async () => {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const {data} = await agent.app.bsky.unspecced.getTrends(
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
PostFeedVideoGridRow,
|
||||
PostFeedVideoGridRowPlaceholder,
|
||||
} from '#/components/feeds/PostFeedVideoGridRow'
|
||||
import {FeedTrendingTopicsInterstitial} from '#/components/interstitials/FeedTrendingTopics'
|
||||
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
|
||||
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
||||
@@ -140,6 +141,10 @@ type FeedRow =
|
||||
type: 'interstitialTrending'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'interstitialFeedTrendingTopics'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'interstitialTrendingVideos'
|
||||
key: string
|
||||
@@ -540,6 +545,11 @@ let PostFeed = ({
|
||||
key: 'composerPrompt-' + sliceIndex,
|
||||
})
|
||||
}
|
||||
} else if (sliceIndex === 1) {
|
||||
arr.push({
|
||||
type: 'interstitialFeedTrendingTopics',
|
||||
key: 'interstitialFeedTrendingTopics-' + sliceIndex,
|
||||
})
|
||||
} else if (sliceIndex === 15) {
|
||||
if (areVideoFeedsEnabled && !trendingVideoDisabled) {
|
||||
arr.push({
|
||||
@@ -791,6 +801,8 @@ let PostFeed = ({
|
||||
return <AgeAssuranceDismissibleFeedBanner />
|
||||
} else if (row.type === 'interstitialTrending') {
|
||||
return <TrendingInterstitial />
|
||||
} else if (row.type === 'interstitialFeedTrendingTopics') {
|
||||
return <FeedTrendingTopicsInterstitial />
|
||||
} else if (row.type === 'liveEventFeedsAndTrendingBanner') {
|
||||
return <DiscoverFeedLiveEventFeedsAndTrendingBanner />
|
||||
} else if (row.type === 'composerPrompt') {
|
||||
|
||||
Reference in New Issue
Block a user