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) {