Add Discover interstitial, settings, includes pin for now

This commit is contained in:
Eric Bailey
2025-01-17 15:27:52 -06:00
parent a6a81bf0a8
commit e83f962b82
7 changed files with 304 additions and 33 deletions
+31 -6
View File
@@ -27,14 +27,23 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Hider from '#/components/moderation/Hider' import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
type DisplayVariant = 'default' | 'compact'
export function VideoPostCard({ export function VideoPostCard({
post, post,
sourceContext, sourceContext,
moderation, moderation,
variant = 'default',
onInteract,
}: { }: {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
sourceContext: VideoFeedSourceContext sourceContext: VideoFeedSourceContext
moderation: ModerationDecision moderation: ModerationDecision
variant?: DisplayVariant
/**
* Callback for metrics and stuff
*/
onInteract: () => void
}) { }) {
const t = useTheme() const t = useTheme()
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
@@ -51,6 +60,7 @@ export function VideoPostCard({
*/ */
if (!AppBskyEmbedVideo.isView(embed)) return null if (!AppBskyEmbedVideo.isView(embed)) return null
const isCompact = variant === 'compact'
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : '' const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
const likeCount = post?.likeCount ?? 0 const likeCount = post?.likeCount ?? 0
const repostCount = post?.repostCount ?? 0 const repostCount = post?.repostCount ?? 0
@@ -71,6 +81,9 @@ export function VideoPostCard({
initialPostUri: post.uri, initialPostUri: post.uri,
}, },
}} }}
onPress={() => {
onInteract()
}}
onPressIn={onPressIn} onPressIn={onPressIn}
onPressOut={onPressOut} onPressOut={onPressOut}
style={[ style={[
@@ -169,7 +182,7 @@ export function VideoPostCard({
</Text> </Text>
</View> </View>
)} )}
{repostCount > 0 && ( {!isCompact && repostCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}> <View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Repost size="sm" fill="white" /> <Repost size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}> <Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
@@ -182,7 +195,7 @@ export function VideoPostCard({
</View> </View>
</View> </View>
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}> <View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
{text && ( {!isCompact && text && (
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji> <Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji>
{text} {text}
</Text> </Text>
@@ -207,7 +220,11 @@ export function VideoPostCard({
) )
} }
export function VideoPostCardPlaceholder() { export function VideoPostCardPlaceholder({
variant = 'default',
}: {
variant?: DisplayVariant
}) {
const t = useTheme() const t = useTheme()
const black = select(t.name, { const black = select(t.name, {
light: t.palette.black, light: t.palette.black,
@@ -228,21 +245,26 @@ export function VideoPostCardPlaceholder() {
]}> ]}>
<MediaInsetBorder /> <MediaInsetBorder />
</View> </View>
<VideoPostCardTextPlaceholder /> <VideoPostCardTextPlaceholder variant={variant} />
</View> </View>
) )
} }
export function VideoPostCardTextPlaceholder({ export function VideoPostCardTextPlaceholder({
author, author,
variant,
}: { }: {
author?: AppBskyActorDefs.ProfileViewBasic author?: AppBskyActorDefs.ProfileViewBasic
variant?: DisplayVariant
}) { }) {
const t = useTheme() const t = useTheme()
const isCompact = variant === 'compact'
return ( return (
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}> <View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}>
{!isCompact && (
<>
<View <View
style={[ style={[
a.w_full, a.w_full,
@@ -264,6 +286,8 @@ export function VideoPostCardTextPlaceholder({
}, },
]} ]}
/> />
</>
)}
{author ? ( {author ? (
<View style={[a.flex_row, a.gap_xs, a.align_center]}> <View style={[a.flex_row, a.gap_xs, a.align_center]}>
<UserAvatar type="user" size={20} avatar={author.avatar} /> <UserAvatar type="user" size={20} avatar={author.avatar} />
@@ -298,8 +322,9 @@ export function VideoPostCardTextPlaceholder({
height: 12, height: 12,
width: '75%', width: '75%',
}, },
]} ]}>
/> <MediaInsetBorder />
</View>
</View> </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': { 'recommendedTopic:click': {
context: 'explore' context: 'explore'
} }
'trendingVideos:show': {
context: 'settings'
}
'trendingVideos:hide': {
context: 'settings' | 'interstitial' | 'explore'
}
'trendingVideo:click': {
context: 'interstitial' | 'explore'
}
'progressGuide:hide': {} 'progressGuide:hide': {}
'progressGuide:followDialog:open': {} 'progressGuide:followDialog:open': {}
@@ -37,8 +37,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const inAppBrowserPref = useInAppBrowser() const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser()
const {enabled: trendingEnabled} = useTrendingConfig() const {enabled: trendingEnabled} = useTrendingConfig()
const {trendingDisabled} = useTrendingSettings() const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
const {setTrendingDisabled} = useTrendingSettingsApi() const {setTrendingDisabled, setTrendingVideoDisabled} =
useTrendingSettingsApi()
return ( return (
<Layout.Screen> <Layout.Screen>
@@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) {
<Toggle.Platform /> <Toggle.Platform />
</SettingsList.Item> </SettingsList.Item>
</Toggle.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> </SettingsList.Container>
+2
View File
@@ -126,6 +126,7 @@ const schema = z.object({
/** @deprecated */ /** @deprecated */
mutedThreads: z.array(z.string()), mutedThreads: z.array(z.string()),
trendingDisabled: z.boolean().optional(), trendingDisabled: z.boolean().optional(),
trendingVideoDisabled: z.boolean().optional(),
}) })
export type Schema = z.infer<typeof schema> export type Schema = z.infer<typeof schema>
@@ -172,6 +173,7 @@ export const defaults: Schema = {
hasCheckedForStarterPack: false, hasCheckedForStarterPack: false,
subtitlesEnabled: true, subtitlesEnabled: true,
trendingDisabled: false, trendingDisabled: false,
trendingVideoDisabled: false,
} }
export function tryParse(rawData: string): Schema | undefined { export function tryParse(rawData: string): Schema | undefined {
+17 -3
View File
@@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted'
type StateContext = { type StateContext = {
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined> trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
trendingVideoDisabled: Exclude<
persisted.Schema['trendingVideoDisabled'],
undefined
>
} }
type ApiContext = { type ApiContext = {
setTrendingDisabled( setTrendingDisabled(
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>, hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
): void ): void
setTrendingVideoDisabled(
hidden: Exclude<persisted.Schema['trendingVideoDisabled'], undefined>,
): void
} }
const StateContext = React.createContext<StateContext>({ const StateContext = React.createContext<StateContext>({
trendingDisabled: Boolean(persisted.defaults.trendingDisabled), trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled),
}) })
const ApiContext = React.createContext<ApiContext>({ const ApiContext = React.createContext<ApiContext>({
setTrendingDisabled() {}, setTrendingDisabled() {},
setTrendingVideoDisabled() {},
}) })
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) { 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<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [trendingDisabled, setTrendingDisabled] = const [trendingDisabled, setTrendingDisabled] =
usePersistedBooleanValue('trendingDisabled') usePersistedBooleanValue('trendingDisabled')
const [trendingVideoDisabled, setTrendingVideoDisabled] =
usePersistedBooleanValue('trendingVideoDisabled')
/* /*
* Context * Context
*/ */
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled]) const state = React.useMemo(
() => ({trendingDisabled, trendingVideoDisabled}),
[trendingDisabled, trendingVideoDisabled],
)
const api = React.useMemo( const api = React.useMemo(
() => ({setTrendingDisabled}), () => ({setTrendingDisabled, setTrendingVideoDisabled}),
[setTrendingDisabled], [setTrendingDisabled, setTrendingVideoDisabled],
) )
return ( return (
+16 -1
View File
@@ -50,6 +50,7 @@ import {
PostFeedVideoGridRowPlaceholder, PostFeedVideoGridRowPlaceholder,
} from '#/components/feeds/PostFeedVideoGridRow' } from '#/components/feeds/PostFeedVideoGridRow'
import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingInterstitial} from '#/components/interstitials/Trending'
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedShutdownMsg} from './FeedShutdownMsg' import {FeedShutdownMsg} from './FeedShutdownMsg'
import {PostFeedErrorMessage} from './PostFeedErrorMessage' import {PostFeedErrorMessage} from './PostFeedErrorMessage'
@@ -116,6 +117,10 @@ type FeedRow =
type: 'interstitialTrending' type: 'interstitialTrending'
key: string key: string
} }
| {
type: 'interstitialTrendingVideos'
key: string
}
export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null { export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
if (feedRow.type === 'sliceItem') { if (feedRow.type === 'sliceItem') {
@@ -287,7 +292,7 @@ let PostFeed = ({
const showProgressIntersitial = const showProgressIntersitial =
(followProgressGuide || followAndLikeProgressGuide) && !isDesktop (followProgressGuide || followAndLikeProgressGuide) && !isDesktop
const {trendingDisabled} = useTrendingSettings() const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
const feedItems: FeedRow[] = React.useMemo(() => { const feedItems: FeedRow[] = React.useMemo(() => {
let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined
@@ -380,6 +385,13 @@ let PostFeed = ({
'interstitial2-' + sliceIndex + '-' + lastFetchedAt, 'interstitial2-' + sliceIndex + '-' + lastFetchedAt,
}) })
} }
} else if (sliceIndex === 15) {
if (isNative && !trendingVideoDisabled) {
arr.push({
type: 'interstitialTrendingVideos',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (sliceIndex === 30) { } else if (sliceIndex === 30) {
arr.push({ arr.push({
type: 'interstitialFollows', type: 'interstitialFollows',
@@ -475,6 +487,7 @@ let PostFeed = ({
hasSession, hasSession,
showProgressIntersitial, showProgressIntersitial,
trendingDisabled, trendingDisabled,
trendingVideoDisabled,
gtTablet, gtTablet,
gtMobile, gtMobile,
isVideoFeed, isVideoFeed,
@@ -566,6 +579,8 @@ let PostFeed = ({
return <ProgressGuide /> return <ProgressGuide />
} else if (row.type === 'interstitialTrending') { } else if (row.type === 'interstitialTrending') {
return <TrendingInterstitial /> return <TrendingInterstitial />
} else if (row.type === 'interstitialTrendingVideos') {
return <TrendingVideosInterstitial />
} else if (row.type === 'sliceItem') { } else if (row.type === 'sliceItem') {
const slice = row.slice const slice = row.slice
if (slice.isFallbackMarker) { if (slice.isFallbackMarker) {