34582edf3e
* tweaks to constants (#7478) * add did * use correct did * typo * tweak * Prevent Drawer gesture conflicting with Suggestions scroll (#7468) * Extract BlockDrawerGeesture * Block drawer when scrolling interstitials (cherry picked from commit9e3f2f4374) * yolo interstitial * yolo mode * right swipe * fix nav gesture * vibe controls * collapsible post text * rm blurview, cover for tall videos * smarter video source handling * use thumbnails, improve perf significantly * better android loading * improve aspect ratio * optimize source changes * rm spinner on ios * whoops, remove debug `false` * FIX WRONG VIDEOS SHOWING UP * don't spring on way down * release video players when leaving screen * remove jank animation * Add grid * improve contract, fix double tap * Filter out posts without videos * Only do grid on native * Pipe through feedSourceUri and link to feed * Handle passed through params * Partial revert, just filter posts to start at index * Clean up cards, remove entry interstitial * Tweak handle * Change constant name * Rename some things * Make types legit * Clean up more naming * Add placeholder for grid view * Handle web, set up new organization * Begin work on Header * Replace types * Squashed commit of the following: commit3d1be4c0f1Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 01:08:05 2025 +0000 extend animation commitc9f199413bAuthor: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 01:01:24 2025 +0000 fix gap commit22e520795fAuthor: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:50:16 2025 +0000 thinner bar, format time commitc32427f214Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:47:57 2025 +0000 fix 2 in 3 screens commitcbf84c08d6Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:45:46 2025 +0000 rm unneeded var commit7e0e100177Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:41:18 2025 +0000 scrubberrrrr * use white with opacity rather than gray * Simultaneous gesture * cleanup attempt * fix jank * link to profile on press * fix jitter fr this time * mostly fix android flicker * Maybe fix row generation * Add content hider to video card * emoji in post text * reduce update rate * fix type error * Fix grid layout trailing single item * Add Discover interstitial, settings, includes pin for now * Explore interstitial, handle dimissal, pinning, compact card * Only use grid placeholder on native * Update events * Add feature gate * android nav bar fixes + lower update speed * fix interval + decel rate on interstitials * attempt to fix broken scrub on android (not working) * follow button * Part out the interstitials for perf, add view more * Remove prod web route * Wrap interstitials with BlockDrawerGesture * Bring video cropping in line with images (#7462) * Mimic image cropping for videos on web * Same on native * Rename variables for clarity * Fix Android scrubbing * Add FeedFeedbackProvider * Remove swipe gesture * fix light status bar behaviour * bump * feedback * Copy pasta to new location * Copy pasta part deux * Filter only videos * Make whole text clickable to expand (cherry picked from commit 4cf31120779f4e06eb4c296b3d4b53814d432b07) * move scrubber to own file * end card * add icon to end card * add min view time to viewability config * play haptic on like * tweak feedback * tweak feedback again * Moderation (cherry picked from commit 6b6b471cfb363031284b3e7a1f6e0ade3ac4ae47) * remove bad check * fix feedback for new video grid * change prop name to items as well * Simplify logic * Fix mod footer * Give scrubber more space on android * Add subtle track behind scrubber, adjust opacity * wire in feed context again... * Add better a11y desc to card * Fix key issue * Update a11y copy * Fix scrubber height * improve scrubber animation * Make follow button more obvious * Make header back button more clear * Disable interactions with actual video el * keep content away from the bottom safe area * fix blur * fix moderation issue * improve contrast on mod screen * Make moderation static per item * Memoize rows * Optimizations * Take video moderation into account * Only blur titles for list blur * Change copy * Bump blur radius * animate text in both directions * Rm unused field * Filter by root early * Refactor for clarity * add compose prompt to scrubber * rm log * tweak gradient * Bump SDK, use contentMode to power video feed * Ensure ProfileFeed view also supports video feed * improve scrubber on android * rm border from footer * Update prod video feed did * Separate caches * Add lil hover to View More * Fix undefined logic, remove header for interstitial * Ungate * Fix stuckness * remove extra useless map * Fix effect cleanup * Send seen without cleanup * Simplify react stuff * Earlier early return to avoid loading flash * remove scrubber placeholder * Remove opacity hack * Render useEvent conditionally * Fix Android flash --------- Co-authored-by: dan <dan.abramov@gmail.com> Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Eric Bailey <git@esb.lol>
541 lines
15 KiB
TypeScript
541 lines
15 KiB
TypeScript
import {useMemo} from 'react'
|
|
import {View} from 'react-native'
|
|
import {Image} from 'expo-image'
|
|
import {LinearGradient} from 'expo-linear-gradient'
|
|
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 {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'
|
|
|
|
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({
|
|
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()
|
|
|
|
const listModUi = moderation.ui('contentList')
|
|
|
|
const mergedModui = useMemo(() => {
|
|
const modui = moderation.ui('contentList')
|
|
const mediaModui = moderation.ui('contentMedia')
|
|
modui.alerts = [...modui.alerts, ...mediaModui.alerts]
|
|
modui.blurs = [...modui.blurs, ...mediaModui.blurs]
|
|
modui.filters = [...modui.filters, ...mediaModui.filters]
|
|
modui.informs = [...modui.informs, ...mediaModui.informs]
|
|
return modui
|
|
}, [moderation])
|
|
|
|
/**
|
|
* 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 author = post.author
|
|
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
|
|
const likeCount = post?.likeCount ?? 0
|
|
const repostCount = post?.repostCount ?? 0
|
|
const {thumbnail} = embed
|
|
const black = getBlackColor(t)
|
|
|
|
const textAndAuthor = (
|
|
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
|
|
{text && (
|
|
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji>
|
|
{text}
|
|
</Text>
|
|
)}
|
|
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
|
|
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
|
<UserAvatar type="user" size={20} avatar={post.author.avatar} />
|
|
<MediaInsetBorder />
|
|
</View>
|
|
<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>
|
|
)
|
|
|
|
return (
|
|
<Link
|
|
accessibilityHint={_(msg`Tap to view video in immersive mode.`)}
|
|
label={_(msg`Video from ${author.handle}: ${text}`)}
|
|
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={mergedModui}>
|
|
<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={100}
|
|
/>
|
|
<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>
|
|
{listModUi.blur ? (
|
|
<VideoPostCardTextPlaceholder author={post.author} />
|
|
) : (
|
|
textAndAuthor
|
|
)}
|
|
</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.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_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>
|
|
{textAndAuthor}
|
|
</Hider.Content>
|
|
</Hider.Outer>
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
export function VideoPostCardPlaceholder() {
|
|
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>
|
|
<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={[
|
|
a.w_full,
|
|
a.rounded_xs,
|
|
t.atoms.bg_contrast_50,
|
|
{
|
|
height: 14,
|
|
},
|
|
]}
|
|
/>
|
|
<View
|
|
style={[
|
|
a.w_full,
|
|
a.rounded_xs,
|
|
t.atoms.bg_contrast_50,
|
|
{
|
|
height: 14,
|
|
width: '70%',
|
|
},
|
|
]}
|
|
/>
|
|
{author ? (
|
|
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
|
|
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
|
<UserAvatar type="user" size={20} avatar={author.avatar} />
|
|
<MediaInsetBorder />
|
|
</View>
|
|
<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>
|
|
)
|
|
}
|
|
|
|
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()
|
|
|
|
const mergedModui = useMemo(() => {
|
|
const modui = moderation.ui('contentList')
|
|
const mediaModui = moderation.ui('contentMedia')
|
|
modui.alerts = [...modui.alerts, ...mediaModui.alerts]
|
|
modui.blurs = [...modui.blurs, ...mediaModui.blurs]
|
|
modui.filters = [...modui.filters, ...mediaModui.filters]
|
|
modui.informs = [...modui.informs, ...mediaModui.informs]
|
|
return modui
|
|
}, [moderation])
|
|
|
|
/**
|
|
* 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={mergedModui}>
|
|
<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={100}
|
|
/>
|
|
<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>
|
|
</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>
|
|
)
|
|
}
|