diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 7c18cb72b3..4b75354e17 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -27,21 +27,25 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' -type DisplayVariant = 'default' | 'compact' +function getBlackColor(t: ReturnType) { + return select(t.name, { + light: t.palette.black, + dark: t.atoms.bg_contrast_25.backgroundColor, + dim: `hsl(${BLUE_HUE}, 28%, 6%)`, + }) +} export function VideoPostCard({ post, sourceContext, moderation, - variant = 'default', onInteract, }: { post: AppBskyFeedDefs.PostView sourceContext: VideoFeedSourceContext moderation: ModerationDecision - variant?: DisplayVariant /** - * Callback for metrics and stuff + * Callback for metrics etc */ onInteract: () => void }) { @@ -60,16 +64,11 @@ 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 const {thumbnail} = embed - const black = select(t.name, { - light: t.palette.black, - dark: t.atoms.bg_contrast_25.backgroundColor, - dim: `hsl(${BLUE_HUE}, 28%, 6%)`, - }) + const black = getBlackColor(t) return ( )} - {!isCompact && repostCount > 0 && ( + {repostCount > 0 && ( @@ -195,13 +194,17 @@ export function VideoPostCard({ - {!isCompact && text && ( + {text && ( {text} )} - + + + + @@ -245,52 +240,48 @@ export function VideoPostCardPlaceholder({ ]}> - + ) } export function VideoPostCardTextPlaceholder({ author, - variant, }: { author?: AppBskyActorDefs.ProfileViewBasic - variant?: DisplayVariant }) { const t = useTheme() - const isCompact = variant === 'compact' return ( - {!isCompact && ( - <> - - - - )} + + {author ? ( - + + + + - - + ]} + /> )} ) } + +export function CompactVideoPostCard({ + post, + sourceContext, + moderation, + onInteract, +}: { + post: AppBskyFeedDefs.PostView + sourceContext: VideoFeedSourceContext + moderation: ModerationDecision + /** + * Callback for metrics etc + */ + onInteract: () => void +}) { + const t = useTheme() + const {_, i18n} = useLingui() + const embed = post.embed + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + + /** + * Filtering should be done at a higher level, such as `PostFeed` or + * `PostFeedVideoGridRow`, but we need to protect here as well. + */ + if (!AppBskyEmbedVideo.isView(embed)) return null + + const likeCount = post?.likeCount ?? 0 + const {thumbnail} = embed + const black = getBlackColor(t) + + return ( + { + onInteract() + }} + onPressIn={onPressIn} + onPressOut={onPressOut} + style={[ + a.flex_col, + { + alignItems: undefined, + justifyContent: undefined, + }, + ]}> + + + + + + + + + + + {_(msg`Hidden`)} + + + + + + + + + + + + + + + + + + + + + + + {likeCount > 0 && ( + + + + {formatCount(i18n, likeCount)} + + + )} + + + + + + + + ) +} + +export function CompactVideoPostCardPlaceholder() { + const t = useTheme() + const black = getBlackColor(t) + + return ( + + + + + + ) +} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index ee62da41c0..3aac27a0f8 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ScrollView,View} from 'react-native' +import {ScrollView, View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -7,19 +7,16 @@ 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 {atoms as a, useGutters, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import { - VideoPostCard, - VideoPostCardPlaceholder, + CompactVideoPostCard, + CompactVideoPostCardPlaceholder, } from '#/components/VideoPostCard' const CARD_WIDTH = 100 @@ -30,33 +27,9 @@ export function TrendingVideos() { const gutters = useGutters([0, 'base']) const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`) const {setTrendingVideoDisabled} = useTrendingSettingsApi() + const trendingPrompt = Prompt.usePromptControl() - 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(() => { + const onConfirmHide = React.useCallback(() => { setTrendingVideoDisabled(true) logEvent('trendingVideos:hide', {context: 'interstitial'}) }, [setTrendingVideoDisabled]) @@ -69,7 +42,7 @@ export function TrendingVideos() { + onPress={() => trendingPrompt.open()}> @@ -107,7 +80,6 @@ export function TrendingVideos() { style={[ a.flex_row, a.gap_sm, - a.pb_sm, { paddingLeft: gutters.paddingLeft, paddingRight: gutters.paddingRight, @@ -118,7 +90,7 @@ export function TrendingVideos() { .fill(0) .map((_, i) => ( - + )) ) : error || !data ? ( @@ -133,8 +105,7 @@ export function TrendingVideos() { .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) .map(item => ( - - {!isSavedAlready && ( - - - - - - Pin to your home screen for easy access - - - - - )} + ) } diff --git a/src/screens/Search/components/ExploreTrendingVideos.tsx b/src/screens/Search/components/ExploreTrendingVideos.tsx new file mode 100644 index 0000000000..cdd914201e --- /dev/null +++ b/src/screens/Search/components/ExploreTrendingVideos.tsx @@ -0,0 +1,175 @@ +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 {isWeb} from '#/platform/detection' +import {useSavedFeeds} from '#/state/queries/feed' +import {usePostFeedQuery} from '#/state/queries/post-feed' +import {useAddSavedFeedsMutation} from '#/state/queries/preferences' +import {atoms as a, tokens,useGutters, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {GradientFill} from '#/components/GradientFill' +import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin' +import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import {Text} from '#/components/Typography' +import { + CompactVideoPostCard, + CompactVideoPostCardPlaceholder, +} from '#/components/VideoPostCard' + +const CARD_WIDTH = 100 + +export function ExploreTrendingVideos() { + const t = useTheme() + const {_} = useLingui() + const gutters = useGutters([0, 'base']) + const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`) + + 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], + ) + + if (error) { + return null + } + + return ( + + + + + + + Trending Videos + + + + + BETA + + + + + A new way to experience video on Bluesky + + + + + + + {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 the trending videos feed to your home screen for easy access + + + + + )} + + ) +} diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index e27435c35c..ecb02a7b4c 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' +import {isNative,isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -26,6 +26,7 @@ import { import {UserAvatar} from '#/view/com/util/UserAvatar' import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations' import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics' +import {ExploreTrendingVideos} from '#/screens/Search/components/ExploreTrendingVideos' import {atoms as a, useTheme, ViewStyleProp} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' @@ -246,6 +247,10 @@ type ExploreScreenItems = type: 'trendingTopics' key: string } + | { + type: 'trendingVideos' + key: string + } | { type: 'recommendations' key: string @@ -343,6 +348,13 @@ export function Explore() { key: `trending-topics`, }) + if (isNative) { + i.push({ + type: 'trendingVideos', + key: `trending-videos`, + }) + } + i.push({ type: 'recommendations', key: `recommendations`, @@ -514,6 +526,9 @@ export function Explore() { case 'trendingTopics': { return } + case 'trendingVideos': { + return + } case 'recommendations': { return }