Explore interstitial, handle dimissal, pinning, compact card
This commit is contained in:
@@ -27,21 +27,25 @@ 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'
|
function getBlackColor(t: ReturnType<typeof useTheme>) {
|
||||||
|
return select(t.name, {
|
||||||
|
light: t.palette.black,
|
||||||
|
dark: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
dim: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function VideoPostCard({
|
export function VideoPostCard({
|
||||||
post,
|
post,
|
||||||
sourceContext,
|
sourceContext,
|
||||||
moderation,
|
moderation,
|
||||||
variant = 'default',
|
|
||||||
onInteract,
|
onInteract,
|
||||||
}: {
|
}: {
|
||||||
post: AppBskyFeedDefs.PostView
|
post: AppBskyFeedDefs.PostView
|
||||||
sourceContext: VideoFeedSourceContext
|
sourceContext: VideoFeedSourceContext
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
variant?: DisplayVariant
|
|
||||||
/**
|
/**
|
||||||
* Callback for metrics and stuff
|
* Callback for metrics etc
|
||||||
*/
|
*/
|
||||||
onInteract: () => void
|
onInteract: () => void
|
||||||
}) {
|
}) {
|
||||||
@@ -60,16 +64,11 @@ 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
|
||||||
const {thumbnail} = embed
|
const {thumbnail} = embed
|
||||||
const black = select(t.name, {
|
const black = getBlackColor(t)
|
||||||
light: t.palette.black,
|
|
||||||
dark: t.atoms.bg_contrast_25.backgroundColor,
|
|
||||||
dim: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -165,7 +164,7 @@ export function VideoPostCard({
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[black, 'rgba(22, 24, 35, 0)']}
|
colors={[black, 'rgba(0, 0, 0, 0)']}
|
||||||
locations={[0.02, 1]}
|
locations={[0.02, 1]}
|
||||||
start={{x: 0, y: 1}}
|
start={{x: 0, y: 1}}
|
||||||
end={{x: 0, y: 0}}
|
end={{x: 0, y: 0}}
|
||||||
@@ -182,7 +181,7 @@ export function VideoPostCard({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{!isCompact && repostCount > 0 && (
|
{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'}]}>
|
||||||
@@ -195,13 +194,17 @@ 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}]}>
|
||||||
{!isCompact && text && (
|
{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>
|
||||||
)}
|
)}
|
||||||
<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={post.author.avatar} />
|
<View
|
||||||
|
style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
||||||
|
<UserAvatar type="user" size={20} avatar={post.author.avatar} />
|
||||||
|
<MediaInsetBorder />
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
@@ -220,17 +223,9 @@ 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 = getBlackColor(t)
|
||||||
light: t.palette.black,
|
|
||||||
dark: t.atoms.bg_contrast_25.backgroundColor,
|
|
||||||
dim: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
@@ -245,52 +240,48 @@ export function VideoPostCardPlaceholder({
|
|||||||
]}>
|
]}>
|
||||||
<MediaInsetBorder />
|
<MediaInsetBorder />
|
||||||
</View>
|
</View>
|
||||||
<VideoPostCardTextPlaceholder variant={variant} />
|
<VideoPostCardTextPlaceholder />
|
||||||
</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
|
||||||
<>
|
style={[
|
||||||
<View
|
a.w_full,
|
||||||
style={[
|
a.rounded_xs,
|
||||||
a.w_full,
|
t.atoms.bg_contrast_50,
|
||||||
a.rounded_xs,
|
{
|
||||||
t.atoms.bg_contrast_50,
|
height: 14,
|
||||||
{
|
},
|
||||||
height: 14,
|
]}
|
||||||
},
|
/>
|
||||||
]}
|
<View
|
||||||
/>
|
style={[
|
||||||
<View
|
a.w_full,
|
||||||
style={[
|
a.rounded_xs,
|
||||||
a.w_full,
|
t.atoms.bg_contrast_50,
|
||||||
a.rounded_xs,
|
{
|
||||||
t.atoms.bg_contrast_50,
|
height: 14,
|
||||||
{
|
width: '70%',
|
||||||
height: 14,
|
},
|
||||||
width: '70%',
|
]}
|
||||||
},
|
/>
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{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} />
|
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
||||||
|
<UserAvatar type="user" size={20} avatar={author.avatar} />
|
||||||
|
<MediaInsetBorder />
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
@@ -322,12 +313,197 @@ export function VideoPostCardTextPlaceholder({
|
|||||||
height: 12,
|
height: 12,
|
||||||
width: '75%',
|
width: '75%',
|
||||||
},
|
},
|
||||||
]}>
|
]}
|
||||||
<MediaInsetBorder />
|
/>
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Link
|
||||||
|
label={_(msg`View video`)}
|
||||||
|
to={{
|
||||||
|
screen: 'VideoFeed',
|
||||||
|
params: {
|
||||||
|
...sourceContext,
|
||||||
|
initialPostUri: post.uri,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
onInteract()
|
||||||
|
}}
|
||||||
|
onPressIn={onPressIn}
|
||||||
|
onPressOut={onPressOut}
|
||||||
|
style={[
|
||||||
|
a.flex_col,
|
||||||
|
{
|
||||||
|
alignItems: undefined,
|
||||||
|
justifyContent: undefined,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Hider.Outer modui={moderation.ui('contentList')}>
|
||||||
|
<Hider.Mask>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
{
|
||||||
|
backgroundColor: black,
|
||||||
|
aspectRatio: 9 / 16,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: thumbnail}}
|
||||||
|
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
blurRadius={40}
|
||||||
|
/>
|
||||||
|
<MediaInsetBorder />
|
||||||
|
<View
|
||||||
|
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{
|
||||||
|
backgroundColor: 'black',
|
||||||
|
opacity: 0.2,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View style={[a.align_center, a.gap_xs]}>
|
||||||
|
<Eye size="lg" fill="white" />
|
||||||
|
<Text style={[a.text_sm, {color: 'white'}]}>
|
||||||
|
{_(msg`Hidden`)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<VideoPostCardTextPlaceholder author={post.author} />
|
||||||
|
</Hider.Mask>
|
||||||
|
<Hider.Content>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
{
|
||||||
|
backgroundColor: black,
|
||||||
|
aspectRatio: 9 / 16,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: thumbnail}}
|
||||||
|
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
<MediaInsetBorder />
|
||||||
|
|
||||||
|
<View style={[a.absolute, a.inset_0]}>
|
||||||
|
<View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}>
|
||||||
|
<View
|
||||||
|
style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
||||||
|
<UserAvatar
|
||||||
|
type="user"
|
||||||
|
size={20}
|
||||||
|
avatar={post.author.avatar}
|
||||||
|
/>
|
||||||
|
<MediaInsetBorder />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.pt_2xl,
|
||||||
|
{
|
||||||
|
top: 'auto',
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[black, 'rgba(0, 0, 0, 0)']}
|
||||||
|
locations={[0.02, 1]}
|
||||||
|
start={{x: 0, y: 1}}
|
||||||
|
end={{x: 0, y: 0}}
|
||||||
|
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
|
||||||
|
{likeCount > 0 && (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<Heart size="sm" fill="white" />
|
||||||
|
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
|
||||||
|
{formatCount(i18n, likeCount)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Hider.Content>
|
||||||
|
</Hider.Outer>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompactVideoPostCardPlaceholder() {
|
||||||
|
const t = useTheme()
|
||||||
|
const black = getBlackColor(t)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
{
|
||||||
|
backgroundColor: black,
|
||||||
|
aspectRatio: 9 / 16,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<MediaInsetBorder />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ScrollView,View} from 'react-native'
|
import {ScrollView, View} from 'react-native'
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -7,19 +7,16 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {VIDEO_FEED_URI} from '#/lib/constants'
|
import {VIDEO_FEED_URI} from '#/lib/constants'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {useTrendingSettingsApi} from '#/state/preferences/trending'
|
import {useTrendingSettingsApi} from '#/state/preferences/trending'
|
||||||
import {useSavedFeeds} from '#/state/queries/feed'
|
|
||||||
import {usePostFeedQuery} from '#/state/queries/post-feed'
|
import {usePostFeedQuery} from '#/state/queries/post-feed'
|
||||||
import {useAddSavedFeedsMutation} from '#/state/queries/preferences'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
import {atoms as a, useGutters,useTheme} from '#/alf'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
|
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {
|
import {
|
||||||
VideoPostCard,
|
CompactVideoPostCard,
|
||||||
VideoPostCardPlaceholder,
|
CompactVideoPostCardPlaceholder,
|
||||||
} from '#/components/VideoPostCard'
|
} from '#/components/VideoPostCard'
|
||||||
|
|
||||||
const CARD_WIDTH = 100
|
const CARD_WIDTH = 100
|
||||||
@@ -30,33 +27,9 @@ export function TrendingVideos() {
|
|||||||
const gutters = useGutters([0, 'base'])
|
const gutters = useGutters([0, 'base'])
|
||||||
const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`)
|
const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`)
|
||||||
const {setTrendingVideoDisabled} = useTrendingSettingsApi()
|
const {setTrendingVideoDisabled} = useTrendingSettingsApi()
|
||||||
|
const trendingPrompt = Prompt.usePromptControl()
|
||||||
|
|
||||||
const {data: saved} = useSavedFeeds()
|
const onConfirmHide = React.useCallback(() => {
|
||||||
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)
|
setTrendingVideoDisabled(true)
|
||||||
logEvent('trendingVideos:hide', {context: 'interstitial'})
|
logEvent('trendingVideos:hide', {context: 'interstitial'})
|
||||||
}, [setTrendingVideoDisabled])
|
}, [setTrendingVideoDisabled])
|
||||||
@@ -69,7 +42,7 @@ export function TrendingVideos() {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.pt_lg,
|
a.pt_lg,
|
||||||
a.pb_md,
|
a.pb_lg,
|
||||||
a.border_t,
|
a.border_t,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
@@ -94,7 +67,7 @@ export function TrendingVideos() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
shape="round"
|
shape="round"
|
||||||
onPress={hide}>
|
onPress={() => trendingPrompt.open()}>
|
||||||
<ButtonIcon icon={X} />
|
<ButtonIcon icon={X} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
@@ -107,7 +80,6 @@ export function TrendingVideos() {
|
|||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
a.pb_sm,
|
|
||||||
{
|
{
|
||||||
paddingLeft: gutters.paddingLeft,
|
paddingLeft: gutters.paddingLeft,
|
||||||
paddingRight: gutters.paddingRight,
|
paddingRight: gutters.paddingRight,
|
||||||
@@ -118,7 +90,7 @@ export function TrendingVideos() {
|
|||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => (
|
.map((_, i) => (
|
||||||
<View key={i} style={[{width: CARD_WIDTH}]}>
|
<View key={i} style={[{width: CARD_WIDTH}]}>
|
||||||
<VideoPostCardPlaceholder variant="compact" />
|
<CompactVideoPostCardPlaceholder />
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
) : error || !data ? (
|
) : error || !data ? (
|
||||||
@@ -133,8 +105,7 @@ export function TrendingVideos() {
|
|||||||
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
|
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
|
||||||
.map(item => (
|
.map(item => (
|
||||||
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
|
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
|
||||||
<VideoPostCard
|
<CompactVideoPostCard
|
||||||
variant="compact"
|
|
||||||
post={item.post}
|
post={item.post}
|
||||||
moderation={item.moderation}
|
moderation={item.moderation}
|
||||||
sourceContext={{
|
sourceContext={{
|
||||||
@@ -151,34 +122,13 @@ export function TrendingVideos() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{!isSavedAlready && (
|
<Prompt.Basic
|
||||||
<View style={[gutters, a.pt_xs]}>
|
control={trendingPrompt}
|
||||||
<Divider />
|
title={_(msg`Hide trending videos?`)}
|
||||||
|
description={_(msg`You can update this later from your settings.`)}
|
||||||
<View
|
confirmButtonCta={_(msg`Hide`)}
|
||||||
style={[
|
onConfirm={onConfirmHide}
|
||||||
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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<View style={[a.pb_xl]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
isWeb
|
||||||
|
? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
|
||||||
|
: [a.p_lg, a.pt_xl, a.gap_md],
|
||||||
|
a.border_b,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1, a.gap_sm]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<Graph
|
||||||
|
size="lg"
|
||||||
|
fill={t.palette.primary_500}
|
||||||
|
style={{marginLeft: -2}}
|
||||||
|
/>
|
||||||
|
<Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
|
||||||
|
<Trans>Trending Videos</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
|
||||||
|
<GradientFill gradient={tokens.gradients.primary} />
|
||||||
|
<Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
|
||||||
|
<Trans>BETA</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||||
|
<Trans>A new way to experience video on Bluesky</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
snapToInterval={(CARD_WIDTH + a.gap_sm.gap) * 2}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pt_lg,
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_sm,
|
||||||
|
{
|
||||||
|
paddingLeft: gutters.paddingLeft,
|
||||||
|
paddingRight: gutters.paddingRight,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{isLoading ? (
|
||||||
|
Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<View key={i} style={[{width: CARD_WIDTH}]}>
|
||||||
|
<CompactVideoPostCardPlaceholder />
|
||||||
|
</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}]}>
|
||||||
|
<CompactVideoPostCard
|
||||||
|
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_lg,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_xl,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.flex_1, a.text_sm, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Pin the trending videos feed to your home screen for easy access
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
disabled={isPinPending}
|
||||||
|
label={_(msg`Pin`)}
|
||||||
|
size="small"
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
onPress={pinFeed}>
|
||||||
|
<ButtonText>{_(msg`Pin`)}</ButtonText>
|
||||||
|
<ButtonIcon icon={Pin} position="right" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isNative,isWeb} from '#/platform/detection'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
|
import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
|
||||||
import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
|
import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
|
||||||
|
import {ExploreTrendingVideos} from '#/screens/Search/components/ExploreTrendingVideos'
|
||||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
@@ -246,6 +247,10 @@ type ExploreScreenItems =
|
|||||||
type: 'trendingTopics'
|
type: 'trendingTopics'
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'trendingVideos'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'recommendations'
|
type: 'recommendations'
|
||||||
key: string
|
key: string
|
||||||
@@ -343,6 +348,13 @@ export function Explore() {
|
|||||||
key: `trending-topics`,
|
key: `trending-topics`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
i.push({
|
||||||
|
type: 'trendingVideos',
|
||||||
|
key: `trending-videos`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
i.push({
|
i.push({
|
||||||
type: 'recommendations',
|
type: 'recommendations',
|
||||||
key: `recommendations`,
|
key: `recommendations`,
|
||||||
@@ -514,6 +526,9 @@ export function Explore() {
|
|||||||
case 'trendingTopics': {
|
case 'trendingTopics': {
|
||||||
return <ExploreTrendingTopics />
|
return <ExploreTrendingTopics />
|
||||||
}
|
}
|
||||||
|
case 'trendingVideos': {
|
||||||
|
return <ExploreTrendingVideos />
|
||||||
|
}
|
||||||
case 'recommendations': {
|
case 'recommendations': {
|
||||||
return <ExploreRecommendations />
|
return <ExploreRecommendations />
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user