yolo (#7499)
* 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>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import {useCallback} from 'react'
|
||||
import {GestureResponderEvent, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {BUTTON_VISUAL_ALIGNMENT_OFFSET} from '#/components/Layout/const'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function HeaderPlaceholder() {
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
width: 36,
|
||||
height: 36,
|
||||
backgroundColor: 'white',
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.flex_1, a.gap_xs]}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_xs,
|
||||
{
|
||||
backgroundColor: 'white',
|
||||
height: 14,
|
||||
width: 80,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_xs,
|
||||
{
|
||||
backgroundColor: 'white',
|
||||
height: 10,
|
||||
width: 140,
|
||||
opacity: 0.6,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Header({
|
||||
sourceContext,
|
||||
}: {
|
||||
sourceContext: VideoFeedSourceContext
|
||||
}) {
|
||||
let content = null
|
||||
switch (sourceContext.type) {
|
||||
case 'feedgen': {
|
||||
content = <FeedHeader sourceContext={sourceContext} />
|
||||
break
|
||||
}
|
||||
case 'author':
|
||||
// TODO
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<BackButton />
|
||||
<Layout.Header.Content align="left">{content}</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeedHeader({
|
||||
sourceContext,
|
||||
}: {
|
||||
sourceContext: Exclude<VideoFeedSourceContext, {type: 'author'}>
|
||||
}) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const {
|
||||
data: info,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFeedSourceInfoQuery({uri: sourceContext.uri})
|
||||
|
||||
if (sourceContext.sourceInterstitial !== undefined) {
|
||||
// For now, don't show the header if coming from an interstitial.
|
||||
return null
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <HeaderPlaceholder />
|
||||
} else if (error || !info) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{info.avatar && <UserAvatar size={36} type="algo" avatar={info.avatar} />}
|
||||
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_heavy,
|
||||
a.leading_tight,
|
||||
gtMobile && a.text_lg,
|
||||
]}
|
||||
numberOfLines={2}>
|
||||
{info.displayName}
|
||||
</Text>
|
||||
<View style={[a.flex_row, {gap: 6}]}>
|
||||
<Text
|
||||
style={[a.flex_shrink, a.text_sm, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(info.creatorHandle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: This customization should be a part of the layout component
|
||||
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressBack = useCallback(
|
||||
(evt: GestureResponderEvent) => {
|
||||
onPress?.(evt)
|
||||
if (evt.defaultPrevented) return
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
},
|
||||
[onPress, navigation],
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Header.Slot>
|
||||
<Button
|
||||
label={_(msg`Go back`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onPressBack}
|
||||
hitSlop={HITSLOP_30}
|
||||
style={[
|
||||
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
||||
a.bg_transparent,
|
||||
style,
|
||||
]}
|
||||
{...props}>
|
||||
<ArrowLeft size="lg" fill="white" />
|
||||
</Button>
|
||||
</Layout.Header.Slot>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
NativeGesture,
|
||||
} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
interpolate,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {
|
||||
useSafeAreaFrame,
|
||||
useSafeAreaInsets,
|
||||
} from 'react-native-safe-area-context'
|
||||
import {useEventListener} from 'expo'
|
||||
import {VideoPlayer} from 'expo-video'
|
||||
|
||||
import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils'
|
||||
import {tokens} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
// magic number that is roughly the min height of the write reply button
|
||||
// we inset the video by this amount
|
||||
export const VIDEO_PLAYER_BOTTOM_INSET = 57
|
||||
|
||||
export function Scrubber({
|
||||
active,
|
||||
player,
|
||||
seekingAnimationSV,
|
||||
scrollGesture,
|
||||
children,
|
||||
}: {
|
||||
active: boolean
|
||||
player?: VideoPlayer
|
||||
seekingAnimationSV: SharedValue<number>
|
||||
scrollGesture: NativeGesture
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const {width: screenWidth} = useSafeAreaFrame()
|
||||
const insets = useSafeAreaInsets()
|
||||
const currentTimeSV = useSharedValue(0)
|
||||
const durationSV = useSharedValue(0)
|
||||
const [currentSeekTime, setCurrentSeekTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
|
||||
const updateTime = (currentTime: number, duration: number) => {
|
||||
'worklet'
|
||||
currentTimeSV.set(currentTime)
|
||||
if (duration !== 0) {
|
||||
durationSV.set(duration)
|
||||
}
|
||||
}
|
||||
|
||||
const isSeekingSV = useSharedValue(false)
|
||||
const seekProgressSV = useSharedValue(0)
|
||||
|
||||
useAnimatedReaction(
|
||||
() => Math.round(seekProgressSV.get()),
|
||||
(progress, prevProgress) => {
|
||||
if (progress !== prevProgress) {
|
||||
runOnJS(setCurrentSeekTime)(progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const seekBy = useCallback(
|
||||
(time: number) => {
|
||||
player?.seekBy(time)
|
||||
|
||||
setTimeout(() => {
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
isSeekingSV.set(false)
|
||||
seekingAnimationSV.set(withTiming(0, {duration: 500}))
|
||||
})()
|
||||
}, 50)
|
||||
},
|
||||
[player, isSeekingSV, seekingAnimationSV],
|
||||
)
|
||||
|
||||
const scrubPanGesture = useMemo(() => {
|
||||
return Gesture.Pan()
|
||||
.blocksExternalGesture(scrollGesture)
|
||||
.activeOffsetX([-10, 10])
|
||||
.failOffsetY([-10, 10])
|
||||
.onStart(() => {
|
||||
'worklet'
|
||||
seekProgressSV.set(currentTimeSV.get())
|
||||
isSeekingSV.set(true)
|
||||
seekingAnimationSV.set(withTiming(1, {duration: 500}))
|
||||
})
|
||||
.onUpdate(evt => {
|
||||
'worklet'
|
||||
const progress = evt.x / screenWidth
|
||||
seekProgressSV.set(
|
||||
clamp(progress * durationSV.get(), 0, durationSV.get()),
|
||||
)
|
||||
})
|
||||
.onEnd(evt => {
|
||||
'worklet'
|
||||
isSeekingSV.get()
|
||||
|
||||
const progress = evt.x / screenWidth
|
||||
const newTime = clamp(progress * durationSV.get(), 0, durationSV.get())
|
||||
|
||||
// optimisically set the progress bar
|
||||
seekProgressSV.set(newTime)
|
||||
|
||||
// it's seek by, so offset by the current time
|
||||
// seekBy sets isSeekingSV back to false, so no need to do that here
|
||||
runOnJS(seekBy)(newTime - currentTimeSV.get())
|
||||
})
|
||||
}, [
|
||||
scrollGesture,
|
||||
seekingAnimationSV,
|
||||
seekBy,
|
||||
screenWidth,
|
||||
currentTimeSV,
|
||||
durationSV,
|
||||
isSeekingSV,
|
||||
seekProgressSV,
|
||||
])
|
||||
|
||||
const timeStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
display: seekingAnimationSV.get() === 0 ? 'none' : 'flex',
|
||||
opacity: seekingAnimationSV.get(),
|
||||
}
|
||||
})
|
||||
|
||||
const barStyle = useAnimatedStyle(() => {
|
||||
const currentTime = isSeekingSV.get()
|
||||
? seekProgressSV.get()
|
||||
: currentTimeSV.get()
|
||||
const progress = currentTime === 0 ? 0 : currentTime / durationSV.get()
|
||||
const isSeeking = seekingAnimationSV.get()
|
||||
return {
|
||||
height: isSeeking * 3 + 1,
|
||||
opacity: interpolate(isSeeking, [0, 1], [0.4, 0.6]),
|
||||
width: `${progress * 100}%`,
|
||||
}
|
||||
})
|
||||
const trackStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
height: seekingAnimationSV.get() * 3 + 1,
|
||||
}
|
||||
})
|
||||
const childrenStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: 1 - seekingAnimationSV.get(),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{player && active && (
|
||||
<PlayerListener
|
||||
player={player}
|
||||
setDuration={setDuration}
|
||||
updateTime={updateTime}
|
||||
/>
|
||||
)}
|
||||
<Animated.View
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: insets.bottom + 80,
|
||||
},
|
||||
timeStyle,
|
||||
]}
|
||||
pointerEvents="none">
|
||||
<Text style={[a.text_center, a.font_bold]}>
|
||||
<Text style={[a.text_5xl, {fontVariant: ['tabular-nums']}]}>
|
||||
{formatTime(currentSeekTime)}
|
||||
</Text>
|
||||
<Text style={[a.text_2xl, {opacity: 0.8}]}>{' / '}</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_5xl,
|
||||
{opacity: 0.8},
|
||||
{fontVariant: ['tabular-nums']},
|
||||
]}>
|
||||
{formatTime(duration)}
|
||||
</Text>
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
||||
<GestureDetector gesture={scrubPanGesture}>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.justify_end,
|
||||
{
|
||||
paddingBottom: insets.bottom,
|
||||
minHeight:
|
||||
// bottom padding
|
||||
insets.bottom +
|
||||
// scrubber height
|
||||
tokens.space.lg +
|
||||
// write reply height
|
||||
VIDEO_PLAYER_BOTTOM_INSET,
|
||||
},
|
||||
a.z_10,
|
||||
]}>
|
||||
<View style={[a.w_full, a.relative]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
a.w_full,
|
||||
{backgroundColor: 'white', opacity: 0.2},
|
||||
trackStyle,
|
||||
]}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
a.absolute,
|
||||
{top: 0, left: 0, backgroundColor: 'white'},
|
||||
barStyle,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<Animated.View
|
||||
style={[{minHeight: VIDEO_PLAYER_BOTTOM_INSET}, childrenStyle]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PlayerListener({
|
||||
player,
|
||||
setDuration,
|
||||
updateTime,
|
||||
}: {
|
||||
player: VideoPlayer
|
||||
setDuration: (duration: number) => void
|
||||
updateTime: (currentTime: number, duration: number) => void
|
||||
}) {
|
||||
useEventListener(player, 'timeUpdate', evt => {
|
||||
const duration = player.duration
|
||||
if (duration !== 0) {
|
||||
setDuration(Math.round(duration))
|
||||
}
|
||||
runOnUI(updateTime)(evt.currentTime, duration)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function clamp(num: number, min: number, max: number) {
|
||||
'worklet'
|
||||
return Math.min(Math.max(num, min), max)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
export function VideoScreen() {
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {AuthorFilter} from '#/state/queries/post-feed'
|
||||
|
||||
/**
|
||||
* Kind of like `FeedDescriptor` but not
|
||||
*/
|
||||
export type VideoFeedSourceContext =
|
||||
| {
|
||||
type: 'feedgen'
|
||||
uri: string
|
||||
sourceInterstitial: 'discover' | 'explore' | 'none'
|
||||
initialPostUri?: string
|
||||
}
|
||||
| {
|
||||
type: 'author'
|
||||
did: string
|
||||
filter: AuthorFilter
|
||||
initialPostUri?: string
|
||||
}
|
||||
Reference in New Issue
Block a user