This commit is contained in:
Eric Bailey
2025-01-16 10:56:20 -06:00
parent eac4a67533
commit 6d242c3891
3 changed files with 260 additions and 74 deletions
+131
View File
@@ -0,0 +1,131 @@
import {Pressable,View} from 'react-native'
import {Image} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient'
import {AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from '#/lib/strings/handles'
import {FeedPostSliceItem} from '#/state/queries/post-feed'
import {formatCount} from '#/view/com/util/numeric/format'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {BLUE_HUE} from '#/alf/util/colorGeneration'
import {select} from '#/alf/util/themeSelector'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import {Text} from '#/components/Typography'
export function VideoPostCard({post}: {post: FeedPostSliceItem}) {
const t = useTheme()
const {_, i18n} = useLingui()
const embed = post.post.embed
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
if (!AppBskyEmbedVideo.isView(embed)) {
// TODO unavailable?
return null
}
const {thumbnail} = embed
const black = select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dark: t.atoms.bg_contrast_25.backgroundColor,
dim: `hsl(${BLUE_HUE}, 28%, 6%)`,
})
return (
<Pressable
accessibilityHint={_(msg`View video`)} // TODO
accessibilityLabel={_(msg`View video`)}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}>
<View
style={[
a.justify_center,
a.rounded_sm,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<Image
source={{uri: thumbnail}}
style={[a.w_full, a.h_full]}
accessibilityIgnoresInvertColors
/>
<View style={[a.absolute, a.inset_0]}>
<View
style={[
a.absolute,
a.inset_0,
a.transition_opacity,
{
backgroundColor: black,
opacity: hovered ? 0 : 0.2,
},
]}
/>
<View
style={[
a.absolute,
a.inset_0,
a.pt_3xl,
{
top: 'auto',
},
]}>
<LinearGradient
colors={[black, 'rgba(22, 24, 35, 0)']}
locations={[0.02, 1]}
start={{x: 0, y: 1}}
end={{x: 0, y: 0}}
style={[a.absolute, a.inset_0]}
/>
<View style={[a.relative, a.z_10, a.p_md, a.flex_row, a.gap_md]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Heart size="md" fill="white" />
<Text style={[a.text_md, a.font_bold]}>
{formatCount(i18n, post.post.likeCount || 0)}
</Text>
</View>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Repost size="md" fill="white" />
<Text style={[a.text_md, a.font_bold]}>
{formatCount(i18n, post.post.repostCount || 0)}
</Text>
</View>
</View>
</View>
</View>
</View>
<View style={[a.pt_sm, a.flex_row, a.gap_sm, a.align_center]}>
<PreviewableUserAvatar
type="user"
size={24}
profile={post.post.author}
/>
<Text
style={[
a.flex_1,
a.text_sm,
a.font_bold,
a.leading_tight,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(post.post.author.handle, '@')}
</Text>
</View>
</Pressable>
)
}
@@ -0,0 +1,20 @@
import {View} from 'react-native'
import {FeedPostSliceItem} from '#/state/queries/post-feed'
import {atoms as a, useGutters} from '#/alf'
import {VideoPostCard} from '#/components/VideoPostCard'
export function PostFeedVideoGridRow({posts}: {posts: FeedPostSliceItem[]}) {
const gutters = useGutters(['base', 'base', 0, 'base'])
return (
<View style={[gutters]}>
<View style={[a.flex_row, a.gap_lg]}>
{posts.map(post => (
<View key={post._reactKey} style={[a.flex_1]}>
<VideoPostCard post={post} />
</View>
))}
</View>
</View>
)
}
+109 -74
View File
@@ -33,6 +33,7 @@ import {
FeedDescriptor,
FeedParams,
FeedPostSlice,
FeedPostSliceItem,
pollLatest,
RQKEY,
usePostFeedQuery,
@@ -48,6 +49,7 @@ import {
SuggestedFollows,
VideoModeEntranceInterstitial,
} from '#/components/FeedInterstitials'
import {PostFeedVideoGridRow} from '#/components/feeds/PostFeedVideoGridRow'
import {TrendingInterstitial} from '#/components/interstitials/Trending'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedShutdownMsg} from './FeedShutdownMsg'
@@ -77,7 +79,7 @@ type FeedRow =
key: string
}
| {
type: 'slice'
type: 'slice' // TODO can we remove?
key: string
slice: FeedPostSlice
}
@@ -88,6 +90,11 @@ type FeedRow =
indexInSlice: number
showReplyTo: boolean
}
| {
type: 'videoGridRow'
key: string
posts: FeedPostSliceItem[]
}
| {
type: 'sliceViewFullThread'
key: string
@@ -175,7 +182,7 @@ let PostFeed = ({
const checkForNewRef = React.useRef<(() => void) | null>(null)
const lastFetchRef = React.useRef<number>(Date.now())
const [feedType, feedUri, feedTab] = feed.split('|')
const {gtTablet} = useBreakpoints()
const {gtMobile, gtTablet} = useBreakpoints()
const opts = React.useMemo(
() => ({enabled, ignoreFilterFor}),
@@ -318,88 +325,113 @@ let PostFeed = ({
} else if (data) {
let sliceIndex = -1
for (const page of data?.pages) {
for (const slice of page.slices) {
sliceIndex++
if (feedKind === 'thevids') {
if (sliceIndex === -1) {
arr.push({
type: 'videoModeEntrance',
key: 'videoModeEntrance',
})
}
if (hasSession) {
if (feedKind === 'discover') {
if (sliceIndex === 0) {
if (showProgressIntersitial) {
arr.push({
type: 'interstitialProgressGuide',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
if (!gtTablet && !trendingDisabled) {
arr.push({
type: 'interstitialTrending',
key: 'interstitial2-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (sliceIndex === 30) {
arr.push({
type: 'interstitialFollows',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (feedKind === 'profile') {
if (sliceIndex === 5) {
arr.push({
type: 'interstitialFollows',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (feedKind === 'thevids') {
if (sliceIndex === 0) {
arr.push({
type: 'videoModeEntrance',
key:
'videoModeEntrance-' + sliceIndex + '-' + lastFetchedAt,
})
}
const rows: FeedPostSliceItem[][] = []
for (let i = 0; i < page.slices.length; i++) {
const slice = page.slices[i]
const root = slice.items.at(0)
if (!root) continue
const cols = gtMobile ? 3 : 2
if (i % cols === 0) {
rows.push([root])
} else {
rows[rows.length - 1].push(root)
}
}
if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2
const last = slice.items.length - 1
for (const row of rows) {
sliceIndex++
arr.push({
type: 'sliceItem',
key: slice.items[0]._reactKey,
slice: slice,
indexInSlice: 0,
showReplyTo: false,
type: 'videoGridRow',
key: row.map(r => r._reactKey).join('-'),
posts: row,
})
arr.push({
type: 'sliceViewFullThread',
key: slice._reactKey + '-viewFullThread',
uri: slice.items[0].uri,
})
arr.push({
type: 'sliceItem',
key: slice.items[beforeLast]._reactKey,
slice: slice,
indexInSlice: beforeLast,
showReplyTo:
slice.items[beforeLast].parentAuthor?.did !==
slice.items[beforeLast].post.author.did,
})
arr.push({
type: 'sliceItem',
key: slice.items[last]._reactKey,
slice: slice,
indexInSlice: last,
showReplyTo: false,
})
} else {
for (let i = 0; i < slice.items.length; i++) {
}
} else {
for (const slice of page.slices) {
sliceIndex++
if (hasSession) {
if (feedKind === 'discover') {
if (sliceIndex === 0) {
if (showProgressIntersitial) {
arr.push({
type: 'interstitialProgressGuide',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
if (!gtTablet && !trendingDisabled) {
arr.push({
type: 'interstitialTrending',
key:
'interstitial2-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (sliceIndex === 30) {
arr.push({
type: 'interstitialFollows',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
} else if (feedKind === 'profile') {
if (sliceIndex === 5) {
arr.push({
type: 'interstitialFollows',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
}
}
if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2
const last = slice.items.length - 1
arr.push({
type: 'sliceItem',
key: slice.items[i]._reactKey,
key: slice.items[0]._reactKey,
slice: slice,
indexInSlice: i,
showReplyTo: i === 0,
indexInSlice: 0,
showReplyTo: false,
})
arr.push({
type: 'sliceViewFullThread',
key: slice._reactKey + '-viewFullThread',
uri: slice.items[0].uri,
})
arr.push({
type: 'sliceItem',
key: slice.items[beforeLast]._reactKey,
slice: slice,
indexInSlice: beforeLast,
showReplyTo:
slice.items[beforeLast].parentAuthor?.did !==
slice.items[beforeLast].post.author.did,
})
arr.push({
type: 'sliceItem',
key: slice.items[last]._reactKey,
slice: slice,
indexInSlice: last,
showReplyTo: false,
})
} else {
for (let i = 0; i < slice.items.length; i++) {
arr.push({
type: 'sliceItem',
key: slice.items[i]._reactKey,
slice: slice,
indexInSlice: i,
showReplyTo: i === 0,
})
}
}
}
}
@@ -432,6 +464,7 @@ let PostFeed = ({
showProgressIntersitial,
trendingDisabled,
gtTablet,
gtMobile,
])
// events
@@ -556,6 +589,8 @@ let PostFeed = ({
)
} else if (row.type === 'sliceViewFullThread') {
return <ViewFullThread uri={row.uri} />
} else if (row.type === 'videoGridRow') {
return <PostFeedVideoGridRow posts={row.posts} />
} else {
return null
}