Add grid
This commit is contained in:
@@ -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
@@ -33,6 +33,7 @@ import {
|
|||||||
FeedDescriptor,
|
FeedDescriptor,
|
||||||
FeedParams,
|
FeedParams,
|
||||||
FeedPostSlice,
|
FeedPostSlice,
|
||||||
|
FeedPostSliceItem,
|
||||||
pollLatest,
|
pollLatest,
|
||||||
RQKEY,
|
RQKEY,
|
||||||
usePostFeedQuery,
|
usePostFeedQuery,
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
SuggestedFollows,
|
SuggestedFollows,
|
||||||
VideoModeEntranceInterstitial,
|
VideoModeEntranceInterstitial,
|
||||||
} from '#/components/FeedInterstitials'
|
} from '#/components/FeedInterstitials'
|
||||||
|
import {PostFeedVideoGridRow} from '#/components/feeds/PostFeedVideoGridRow'
|
||||||
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||||
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
||||||
@@ -77,7 +79,7 @@ type FeedRow =
|
|||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'slice'
|
type: 'slice' // TODO can we remove?
|
||||||
key: string
|
key: string
|
||||||
slice: FeedPostSlice
|
slice: FeedPostSlice
|
||||||
}
|
}
|
||||||
@@ -88,6 +90,11 @@ type FeedRow =
|
|||||||
indexInSlice: number
|
indexInSlice: number
|
||||||
showReplyTo: boolean
|
showReplyTo: boolean
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'videoGridRow'
|
||||||
|
key: string
|
||||||
|
posts: FeedPostSliceItem[]
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'sliceViewFullThread'
|
type: 'sliceViewFullThread'
|
||||||
key: string
|
key: string
|
||||||
@@ -175,7 +182,7 @@ let PostFeed = ({
|
|||||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||||
const lastFetchRef = React.useRef<number>(Date.now())
|
const lastFetchRef = React.useRef<number>(Date.now())
|
||||||
const [feedType, feedUri, feedTab] = feed.split('|')
|
const [feedType, feedUri, feedTab] = feed.split('|')
|
||||||
const {gtTablet} = useBreakpoints()
|
const {gtMobile, gtTablet} = useBreakpoints()
|
||||||
|
|
||||||
const opts = React.useMemo(
|
const opts = React.useMemo(
|
||||||
() => ({enabled, ignoreFilterFor}),
|
() => ({enabled, ignoreFilterFor}),
|
||||||
@@ -318,88 +325,113 @@ let PostFeed = ({
|
|||||||
} else if (data) {
|
} else if (data) {
|
||||||
let sliceIndex = -1
|
let sliceIndex = -1
|
||||||
for (const page of data?.pages) {
|
for (const page of data?.pages) {
|
||||||
for (const slice of page.slices) {
|
if (feedKind === 'thevids') {
|
||||||
sliceIndex++
|
if (sliceIndex === -1) {
|
||||||
|
arr.push({
|
||||||
|
type: 'videoModeEntrance',
|
||||||
|
key: 'videoModeEntrance',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (hasSession) {
|
const rows: FeedPostSliceItem[][] = []
|
||||||
if (feedKind === 'discover') {
|
for (let i = 0; i < page.slices.length; i++) {
|
||||||
if (sliceIndex === 0) {
|
const slice = page.slices[i]
|
||||||
if (showProgressIntersitial) {
|
const root = slice.items.at(0)
|
||||||
arr.push({
|
if (!root) continue
|
||||||
type: 'interstitialProgressGuide',
|
const cols = gtMobile ? 3 : 2
|
||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
if (i % cols === 0) {
|
||||||
})
|
rows.push([root])
|
||||||
}
|
} else {
|
||||||
if (!gtTablet && !trendingDisabled) {
|
rows[rows.length - 1].push(root)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slice.isIncompleteThread && slice.items.length >= 3) {
|
for (const row of rows) {
|
||||||
const beforeLast = slice.items.length - 2
|
sliceIndex++
|
||||||
const last = slice.items.length - 1
|
|
||||||
arr.push({
|
arr.push({
|
||||||
type: 'sliceItem',
|
type: 'videoGridRow',
|
||||||
key: slice.items[0]._reactKey,
|
key: row.map(r => r._reactKey).join('-'),
|
||||||
slice: slice,
|
posts: row,
|
||||||
indexInSlice: 0,
|
|
||||||
showReplyTo: false,
|
|
||||||
})
|
})
|
||||||
arr.push({
|
}
|
||||||
type: 'sliceViewFullThread',
|
} else {
|
||||||
key: slice._reactKey + '-viewFullThread',
|
for (const slice of page.slices) {
|
||||||
uri: slice.items[0].uri,
|
sliceIndex++
|
||||||
})
|
|
||||||
arr.push({
|
if (hasSession) {
|
||||||
type: 'sliceItem',
|
if (feedKind === 'discover') {
|
||||||
key: slice.items[beforeLast]._reactKey,
|
if (sliceIndex === 0) {
|
||||||
slice: slice,
|
if (showProgressIntersitial) {
|
||||||
indexInSlice: beforeLast,
|
arr.push({
|
||||||
showReplyTo:
|
type: 'interstitialProgressGuide',
|
||||||
slice.items[beforeLast].parentAuthor?.did !==
|
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
slice.items[beforeLast].post.author.did,
|
})
|
||||||
})
|
}
|
||||||
arr.push({
|
if (!gtTablet && !trendingDisabled) {
|
||||||
type: 'sliceItem',
|
arr.push({
|
||||||
key: slice.items[last]._reactKey,
|
type: 'interstitialTrending',
|
||||||
slice: slice,
|
key:
|
||||||
indexInSlice: last,
|
'interstitial2-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
showReplyTo: false,
|
})
|
||||||
})
|
}
|
||||||
} else {
|
} else if (sliceIndex === 30) {
|
||||||
for (let i = 0; i < slice.items.length; i++) {
|
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({
|
arr.push({
|
||||||
type: 'sliceItem',
|
type: 'sliceItem',
|
||||||
key: slice.items[i]._reactKey,
|
key: slice.items[0]._reactKey,
|
||||||
slice: slice,
|
slice: slice,
|
||||||
indexInSlice: i,
|
indexInSlice: 0,
|
||||||
showReplyTo: i === 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,
|
showProgressIntersitial,
|
||||||
trendingDisabled,
|
trendingDisabled,
|
||||||
gtTablet,
|
gtTablet,
|
||||||
|
gtMobile,
|
||||||
])
|
])
|
||||||
|
|
||||||
// events
|
// events
|
||||||
@@ -556,6 +589,8 @@ let PostFeed = ({
|
|||||||
)
|
)
|
||||||
} else if (row.type === 'sliceViewFullThread') {
|
} else if (row.type === 'sliceViewFullThread') {
|
||||||
return <ViewFullThread uri={row.uri} />
|
return <ViewFullThread uri={row.uri} />
|
||||||
|
} else if (row.type === 'videoGridRow') {
|
||||||
|
return <PostFeedVideoGridRow posts={row.posts} />
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user