Add Discover interstitial, settings, includes pin for now

This commit is contained in:
Eric Bailey
2025-01-17 15:27:52 -06:00
parent 2f3ac892c5
commit b221829152
7 changed files with 304 additions and 33 deletions
+52 -27
View File
@@ -27,14 +27,23 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
type DisplayVariant = 'default' | 'compact'
export function VideoPostCard({
post,
sourceContext,
moderation,
variant = 'default',
onInteract,
}: {
post: AppBskyFeedDefs.PostView
sourceContext: VideoFeedSourceContext
moderation: ModerationDecision
variant?: DisplayVariant
/**
* Callback for metrics and stuff
*/
onInteract: () => void
}) {
const t = useTheme()
const {_, i18n} = useLingui()
@@ -51,6 +60,7 @@ export function VideoPostCard({
*/
if (!AppBskyEmbedVideo.isView(embed)) return null
const isCompact = variant === 'compact'
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
const likeCount = post?.likeCount ?? 0
const repostCount = post?.repostCount ?? 0
@@ -71,6 +81,9 @@ export function VideoPostCard({
initialPostUri: post.uri,
},
}}
onPress={() => {
onInteract()
}}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={[
@@ -169,7 +182,7 @@ export function VideoPostCard({
</Text>
</View>
)}
{repostCount > 0 && (
{!isCompact && repostCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Repost size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
@@ -182,7 +195,7 @@ export function VideoPostCard({
</View>
</View>
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
{text && (
{!isCompact && text && (
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji>
{text}
</Text>
@@ -207,7 +220,11 @@ export function VideoPostCard({
)
}
export function VideoPostCardPlaceholder() {
export function VideoPostCardPlaceholder({
variant = 'default',
}: {
variant?: DisplayVariant
}) {
const t = useTheme()
const black = select(t.name, {
light: t.palette.black,
@@ -228,42 +245,49 @@ export function VideoPostCardPlaceholder() {
]}>
<MediaInsetBorder />
</View>
<VideoPostCardTextPlaceholder />
<VideoPostCardTextPlaceholder variant={variant} />
</View>
)
}
export function VideoPostCardTextPlaceholder({
author,
variant,
}: {
author?: AppBskyActorDefs.ProfileViewBasic
variant?: DisplayVariant
}) {
const t = useTheme()
const isCompact = variant === 'compact'
return (
<View style={[a.flex_1]}>
<View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}>
<View
style={[
a.w_full,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
height: 14,
},
]}
/>
<View
style={[
a.w_full,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
height: 14,
width: '70%',
},
]}
/>
{!isCompact && (
<>
<View
style={[
a.w_full,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
height: 14,
},
]}
/>
<View
style={[
a.w_full,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
height: 14,
width: '70%',
},
]}
/>
</>
)}
{author ? (
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<UserAvatar type="user" size={20} avatar={author.avatar} />
@@ -298,8 +322,9 @@ export function VideoPostCardTextPlaceholder({
height: 12,
width: '75%',
},
]}
/>
]}>
<MediaInsetBorder />
</View>
</View>
)}
</View>
@@ -0,0 +1,184 @@
import React from 'react'
import {ScrollView,View} from 'react-native'
import {AppBskyEmbedVideo} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {VIDEO_FEED_URI} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {useTrendingSettingsApi} from '#/state/preferences/trending'
import {useSavedFeeds} from '#/state/queries/feed'
import {usePostFeedQuery} from '#/state/queries/post-feed'
import {useAddSavedFeedsMutation} from '#/state/queries/preferences'
import {atoms as a, useGutters,useTheme} from '#/alf'
import {Button, ButtonIcon,ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import {Text} from '#/components/Typography'
import {
VideoPostCard,
VideoPostCardPlaceholder,
} from '#/components/VideoPostCard'
const CARD_WIDTH = 100
export function TrendingVideos() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters([0, 'base'])
const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`)
const {setTrendingVideoDisabled} = useTrendingSettingsApi()
const {data: saved} = useSavedFeeds()
const isSavedAlready = React.useMemo(() => {
return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI)
}, [saved])
const {mutateAsync: addSavedFeeds, isPending: isPinPending} =
useAddSavedFeedsMutation()
const pinFeed = React.useCallback(
(e: any) => {
e.preventDefault()
addSavedFeeds([
{
type: 'feed',
value: VIDEO_FEED_URI,
pinned: true,
},
])
// prevent navigation
return false
},
[addSavedFeeds],
)
const hide = React.useCallback(() => {
setTrendingVideoDisabled(true)
logEvent('trendingVideos:hide', {context: 'interstitial'})
}, [setTrendingVideoDisabled])
if (error) {
return null
}
return (
<View
style={[
a.pt_lg,
a.pb_md,
a.border_t,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<View
style={[
gutters,
a.pb_sm,
a.flex_row,
a.align_center,
a.justify_between,
]}>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_xs]}>
<Graph />
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
<Trans>Trending Videos</Trans>
</Text>
</View>
<Button
label={_(msg`Dismiss this section`)}
size="tiny"
variant="ghost"
color="secondary"
shape="round"
onPress={hide}>
<ButtonIcon icon={X} />
</Button>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={(CARD_WIDTH + a.gap_sm.gap) * 2}>
<View
style={[
a.flex_row,
a.gap_sm,
a.pb_sm,
{
paddingLeft: gutters.paddingLeft,
paddingRight: gutters.paddingRight,
},
]}>
{isLoading ? (
Array(10)
.fill(0)
.map((_, i) => (
<View key={i} style={[{width: CARD_WIDTH}]}>
<VideoPostCardPlaceholder variant="compact" />
</View>
))
) : error || !data ? (
<Text>
<Trans>Whoops! Trending videos failed to load.</Trans>
</Text>
) : (
data.pages
.flatMap(page => page.slices)
.map(slice => slice.items[0])
.filter(Boolean)
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
.map(item => (
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
<VideoPostCard
variant="compact"
post={item.post}
moderation={item.moderation}
sourceContext={{
type: 'feedgen',
uri: VIDEO_FEED_URI,
}}
onInteract={() => {
logEvent('trendingVideo:click', {context: 'interstitial'})
}}
/>
</View>
))
)}
</View>
</ScrollView>
{!isSavedAlready && (
<View style={[gutters, a.pt_xs]}>
<Divider />
<View
style={[
a.pt_sm,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_md,
]}>
<Text style={[a.flex_1, a.text_sm, a.leading_snug]}>
<Trans>Pin to your home screen for easy access</Trans>
</Text>
<Button
disabled={isPinPending}
label={_(msg`Pin`)}
size="small"
variant="solid"
color="primary"
onPress={pinFeed}>
<ButtonText>{_(msg`Pin`)}</ButtonText>
<ButtonIcon icon={Pin} position="right" />
</Button>
</View>
</View>
)}
</View>
)
}
+9
View File
@@ -249,6 +249,15 @@ export type LogEvents = {
'recommendedTopic:click': {
context: 'explore'
}
'trendingVideos:show': {
context: 'settings'
}
'trendingVideos:hide': {
context: 'settings' | 'interstitial' | 'explore'
}
'trendingVideo:click': {
context: 'interstitial' | 'explore'
}
'progressGuide:hide': {}
'progressGuide:followDialog:open': {}
@@ -37,8 +37,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser()
const {enabled: trendingEnabled} = useTrendingConfig()
const {trendingDisabled} = useTrendingSettings()
const {setTrendingDisabled} = useTrendingSettingsApi()
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
const {setTrendingDisabled, setTrendingVideoDisabled} =
useTrendingSettingsApi()
return (
<Layout.Screen>
@@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) {
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
<Toggle.Item
name="show_trending_videos"
label={_(msg`Enable trending videos`)}
value={!trendingVideoDisabled}
onChange={value => {
const hide = Boolean(!value)
if (hide) {
logEvent('trendingVideos:hide', {context: 'settings'})
} else {
logEvent('trendingVideos:show', {context: 'settings'})
}
setTrendingVideoDisabled(hide)
}}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={Graph} />
<SettingsList.ItemText>
<Trans>Enable trending videos</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
</>
)}
</SettingsList.Container>
+2
View File
@@ -126,6 +126,7 @@ const schema = z.object({
/** @deprecated */
mutedThreads: z.array(z.string()),
trendingDisabled: z.boolean().optional(),
trendingVideoDisabled: z.boolean().optional(),
})
export type Schema = z.infer<typeof schema>
@@ -172,6 +173,7 @@ export const defaults: Schema = {
hasCheckedForStarterPack: false,
subtitlesEnabled: true,
trendingDisabled: false,
trendingVideoDisabled: false,
}
export function tryParse(rawData: string): Schema | undefined {
+17 -3
View File
@@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted'
type StateContext = {
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
trendingVideoDisabled: Exclude<
persisted.Schema['trendingVideoDisabled'],
undefined
>
}
type ApiContext = {
setTrendingDisabled(
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
): void
setTrendingVideoDisabled(
hidden: Exclude<persisted.Schema['trendingVideoDisabled'], undefined>,
): void
}
const StateContext = React.createContext<StateContext>({
trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled),
})
const ApiContext = React.createContext<ApiContext>({
setTrendingDisabled() {},
setTrendingVideoDisabled() {},
})
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
@@ -43,14 +52,19 @@ function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
export function Provider({children}: React.PropsWithChildren<{}>) {
const [trendingDisabled, setTrendingDisabled] =
usePersistedBooleanValue('trendingDisabled')
const [trendingVideoDisabled, setTrendingVideoDisabled] =
usePersistedBooleanValue('trendingVideoDisabled')
/*
* Context
*/
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled])
const state = React.useMemo(
() => ({trendingDisabled, trendingVideoDisabled}),
[trendingDisabled, trendingVideoDisabled],
)
const api = React.useMemo(
() => ({setTrendingDisabled}),
[setTrendingDisabled],
() => ({setTrendingDisabled, setTrendingVideoDisabled}),
[setTrendingDisabled, setTrendingVideoDisabled],
)
return (
+16 -1
View File
@@ -50,6 +50,7 @@ import {
PostFeedVideoGridRowPlaceholder,
} from '#/components/feeds/PostFeedVideoGridRow'
import {TrendingInterstitial} from '#/components/interstitials/Trending'
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedShutdownMsg} from './FeedShutdownMsg'
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
@@ -116,6 +117,10 @@ type FeedRow =
type: 'interstitialTrending'
key: string
}
| {
type: 'interstitialTrendingVideos'
key: string
}
export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
if (feedRow.type === 'sliceItem') {
@@ -287,7 +292,7 @@ let PostFeed = ({
const showProgressIntersitial =
(followProgressGuide || followAndLikeProgressGuide) && !isDesktop
const {trendingDisabled} = useTrendingSettings()
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
const feedItems: FeedRow[] = React.useMemo(() => {
let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined
@@ -380,6 +385,13 @@ let PostFeed = ({
'interstitial2-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (sliceIndex === 15) {
if (isNative && !trendingVideoDisabled) {
arr.push({
type: 'interstitialTrendingVideos',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (sliceIndex === 30) {
arr.push({
type: 'interstitialFollows',
@@ -475,6 +487,7 @@ let PostFeed = ({
hasSession,
showProgressIntersitial,
trendingDisabled,
trendingVideoDisabled,
gtTablet,
gtMobile,
isVideoFeed,
@@ -566,6 +579,8 @@ let PostFeed = ({
return <ProgressGuide />
} else if (row.type === 'interstitialTrending') {
return <TrendingInterstitial />
} else if (row.type === 'interstitialTrendingVideos') {
return <TrendingVideosInterstitial />
} else if (row.type === 'sliceItem') {
const slice = row.slice
if (slice.isFallbackMarker) {