From e83f962b82183764e7b5c8bf12cfe9fa97d0e6e4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 15:27:52 -0600 Subject: [PATCH] Add Discover interstitial, settings, includes pin for now --- src/components/VideoPostCard.tsx | 79 +++++--- .../interstitials/TrendingVideos.tsx | 184 ++++++++++++++++++ src/lib/statsig/events.ts | 9 + .../Settings/ContentAndMediaSettings.tsx | 26 ++- src/state/persisted/schema.ts | 2 + src/state/preferences/trending.tsx | 20 +- src/view/com/posts/PostFeed.tsx | 17 +- 7 files changed, 304 insertions(+), 33 deletions(-) create mode 100644 src/components/interstitials/TrendingVideos.tsx diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 5b27887b73..7c18cb72b3 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -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({ )} - {repostCount > 0 && ( + {!isCompact && repostCount > 0 && ( @@ -182,7 +195,7 @@ export function VideoPostCard({ - {text && ( + {!isCompact && 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() { ]}> - + ) } export function VideoPostCardTextPlaceholder({ author, + variant, }: { author?: AppBskyActorDefs.ProfileViewBasic + variant?: DisplayVariant }) { const t = useTheme() + const isCompact = variant === 'compact' return ( - - + {!isCompact && ( + <> + + + + )} {author ? ( @@ -298,8 +322,9 @@ export function VideoPostCardTextPlaceholder({ height: 12, width: '75%', }, - ]} - /> + ]}> + + )} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx new file mode 100644 index 0000000000..ee62da41c0 --- /dev/null +++ b/src/components/interstitials/TrendingVideos.tsx @@ -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 ( + + + + + + Trending Videos + + + + + + + + {isLoading ? ( + Array(10) + .fill(0) + .map((_, i) => ( + + + + )) + ) : error || !data ? ( + + Whoops! Trending videos failed to load. + + ) : ( + data.pages + .flatMap(page => page.slices) + .map(slice => slice.items[0]) + .filter(Boolean) + .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) + .map(item => ( + + { + logEvent('trendingVideo:click', {context: 'interstitial'}) + }} + /> + + )) + )} + + + + {!isSavedAlready && ( + + + + + + Pin to your home screen for easy access + + + + + )} + + ) +} diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index f914ef01f9..ca1f683e98 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -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': {} diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index 4a9354bb84..415801370d 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -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 ( @@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) { + { + const hide = Boolean(!value) + if (hide) { + logEvent('trendingVideos:hide', {context: 'settings'}) + } else { + logEvent('trendingVideos:show', {context: 'settings'}) + } + setTrendingVideoDisabled(hide) + }}> + + + + Enable trending videos + + + + )} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 0a9e5b2c07..f840081f3d 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -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 @@ -172,6 +173,7 @@ export const defaults: Schema = { hasCheckedForStarterPack: false, subtitlesEnabled: true, trendingDisabled: false, + trendingVideoDisabled: false, } export function tryParse(rawData: string): Schema | undefined { diff --git a/src/state/preferences/trending.tsx b/src/state/preferences/trending.tsx index bf5d8f13cc..87ec687712 100644 --- a/src/state/preferences/trending.tsx +++ b/src/state/preferences/trending.tsx @@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted' type StateContext = { trendingDisabled: Exclude + trendingVideoDisabled: Exclude< + persisted.Schema['trendingVideoDisabled'], + undefined + > } type ApiContext = { setTrendingDisabled( hidden: Exclude, ): void + setTrendingVideoDisabled( + hidden: Exclude, + ): void } const StateContext = React.createContext({ trendingDisabled: Boolean(persisted.defaults.trendingDisabled), + trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled), }) const ApiContext = React.createContext({ setTrendingDisabled() {}, + setTrendingVideoDisabled() {}, }) function usePersistedBooleanValue(key: T) { @@ -43,14 +52,19 @@ function usePersistedBooleanValue(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 ( diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 84edfcf630..c9ed8a08da 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -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 } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'interstitialTrendingVideos') { + return } else if (row.type === 'sliceItem') { const slice = row.slice if (slice.isFallbackMarker) {