Merge branch 'pnbx/fix-consume' of https://github.com/bluesky-social/social-app into pnbx/fix-consume

This commit is contained in:
Samuel Newman
2025-01-17 18:00:06 +00:00
3 changed files with 224 additions and 123 deletions
+178 -91
View File
@@ -1,30 +1,40 @@
import {View} from 'react-native'
import {Image} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient'
import {AppBskyEmbedVideo, AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyEmbedVideo,
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationDecision,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from '#/lib/strings/handles'
import {formatCount} from '#/view/com/util/numeric/format'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
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 {EyeSlash_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/EyeSlash'
import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
export function VideoPostCard({
post,
sourceContext,
moderation,
}: {
post: AppBskyFeedDefs.PostView
sourceContext: VideoFeedSourceContext
moderation: ModerationDecision
}) {
const t = useTheme()
const {_, i18n} = useLingui()
@@ -70,82 +80,129 @@ export function VideoPostCard({
justifyContent: undefined,
},
]}>
<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]}>
<Hider.Outer modui={moderation.ui('contentList')}>
<Hider.Mask>
<View
style={[
a.absolute,
a.inset_0,
a.pt_2xl,
a.justify_center,
a.rounded_md,
a.overflow_hidden,
{
top: 'auto',
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<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, {opacity: 0.9}]}
<Image
source={{uri: thumbnail}}
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
accessibilityIgnoresInvertColors
blurRadius={40}
/>
<View style={[a.relative, a.z_10, a.p_md, 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>
)}
{repostCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Repost size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, repostCount)}
</Text>
</View>
)}
<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>
</View>
</View>
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
{text && (
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2}>
{text}
</Text>
)}
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<PreviewableUserAvatar type="user" size={20} profile={post.author} />
<Text
<VideoPostCardTextPlaceholder author={post.author} />
</Hider.Mask>
<Hider.Content>
<View
style={[
a.flex_1,
a.text_sm,
a.leading_tight,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</Text>
</View>
</View>
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.pt_2xl,
{
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, {opacity: 0.9}]}
/>
<View
style={[a.relative, a.z_10, a.p_md, 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>
)}
{repostCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Repost size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, repostCount)}
</Text>
</View>
)}
</View>
</View>
</View>
</View>
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
{text && (
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2}>
{text}
</Text>
)}
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<UserAvatar type="user" size={20} avatar={post.author.avatar} />
<Text
style={[
a.flex_1,
a.text_sm,
a.leading_tight,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</Text>
</View>
</View>
</Hider.Content>
</Hider.Outer>
</Link>
)
}
@@ -171,6 +228,20 @@ export function VideoPostCardPlaceholder() {
]}>
<MediaInsetBorder />
</View>
<VideoPostCardTextPlaceholder />
</View>
)
}
export function VideoPostCardTextPlaceholder({
author,
}: {
author?: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
return (
<View style={[a.flex_1]}>
<View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}>
<View
style={[
@@ -193,28 +264,44 @@ export function VideoPostCardPlaceholder() {
},
]}
/>
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_50,
{
width: 20,
height: 20,
},
]}
/>
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{
height: 12,
width: '75%',
},
]}
/>
</View>
{author ? (
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<UserAvatar type="user" size={20} avatar={author.avatar} />
<Text
style={[
a.flex_1,
a.text_sm,
a.leading_tight,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(author.handle, '@')}
</Text>
</View>
) : (
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_50,
{
width: 20,
height: 20,
},
]}
/>
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{
height: 12,
width: '75%',
},
]}
/>
</View>
)}
</View>
</View>
)
+10 -3
View File
@@ -19,7 +19,10 @@ export function PostFeedVideoGridRow({
const gutters = useGutters(['base', 'base', 0, 'base'])
const posts = slices
.filter(slice => AppBskyEmbedVideo.isView(slice.post.embed))
.map(slice => slice.post)
.map(slice => ({
post: slice.post,
moderation: slice.moderation,
}))
/**
* This should not happen because we should be filtering out posts without
@@ -31,8 +34,12 @@ export function PostFeedVideoGridRow({
<View style={[gutters]}>
<View style={[a.flex_row, a.gap_sm]}>
{posts.map(post => (
<View key={post.uri} style={[a.flex_1]}>
<VideoPostCard post={post} sourceContext={sourceContext} />
<View key={post.post.uri} style={[a.flex_1]}>
<VideoPostCard
post={post.post}
sourceContext={sourceContext}
moderation={post.moderation}
/>
</View>
))}
</View>
+36 -29
View File
@@ -323,37 +323,44 @@ let PostFeed = ({
})
} else if (data) {
let sliceIndex = -1
for (const page of data?.pages) {
if (isVideoFeed && isNative) {
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
// TODO test this
if (!AppBskyEmbedVideo.isView(root.post.embed)) {
i--
continue
}
const cols = gtMobile ? 3 : 2
if (i % cols === 0) {
rows.push([root])
} else {
rows[rows.length - 1].push(root)
}
}
for (const row of rows) {
sliceIndex++
arr.push({
type: 'videoGridRow',
key: row.map(r => r._reactKey).join('-'),
slices: row,
sourceFeedUri: feedUri,
})
if (isVideoFeed && isNative) {
const rows: FeedPostSliceItem[][] = []
let slices: {slice: FeedPostSlice; index: number}[] = []
for (const page of data.pages) {
for (const slice of page.slices) {
slices.push({slice, index: sliceIndex++})
}
} else {
}
for (let i = 0; i < slices.length; i++) {
const slice = slices[i]
const root = slice.slice.items.at(0)
if (!root) continue
// TODO test this
if (!AppBskyEmbedVideo.isView(root.post.embed)) {
i--
continue
}
const cols = gtMobile ? 3 : 2
if (i % cols === 0) {
rows.push([root])
} else {
rows[rows.length - 1].push(root)
}
}
for (const row of rows) {
sliceIndex++
arr.push({
type: 'videoGridRow',
key: row.map(r => r._reactKey).join('-'),
slices: row,
sourceFeedUri: feedUri,
})
}
} else {
for (const page of data?.pages) {
for (const slice of page.slices) {
sliceIndex++