diff options
author | Hailey <me@haileyok.com> | 2025-01-19 17:17:41 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-19 17:17:41 -0800 |
commit | 34582edf3ea17789684100172d6dd496220482b0 (patch) | |
tree | ca29a927bf015107a60a867d8266274c8a78de49 /src/screens/VideoFeed/components | |
parent | cb020655504dd0d39f8e91fd517f14dc4a82c307 (diff) | |
download | voidsky-34582edf3ea17789684100172d6dd496220482b0.tar.zst |
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 commit 9e3f2f43745eed9c71cb985e48135b7363d91aa9) * 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: commit 3d1be4c0f19789dd3c5a3572ec1acd744a2edb80 Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 01:08:05 2025 +0000 extend animation commit c9f199413b018efcbd9d8d2a58dd05eb41e7acb7 Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 01:01:24 2025 +0000 fix gap commit 22e520795f50efda176f21a5e967cb27d0cdd907 Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:50:16 2025 +0000 thinner bar, format time commit c32427f21405294ed3567545629a2964c4af59fe Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:47:57 2025 +0000 fix 2 in 3 screens commit cbf84c08d64ca0a08ba9070ef5db918f89aa4296 Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:45:46 2025 +0000 rm unneeded var commit 7e0e100177bb1cd0e64c0841bb7685c7f1eb857f Author: 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>
Diffstat (limited to 'src/screens/VideoFeed/components')
-rw-r--r-- | src/screens/VideoFeed/components/Header.tsx | 180 | ||||
-rw-r--r-- | src/screens/VideoFeed/components/Scrubber.tsx | 265 |
2 files changed, 445 insertions, 0 deletions
diff --git a/src/screens/VideoFeed/components/Header.tsx b/src/screens/VideoFeed/components/Header.tsx new file mode 100644 index 000000000..66c932119 --- /dev/null +++ b/src/screens/VideoFeed/components/Header.tsx @@ -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> + ) +} diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx new file mode 100644 index 000000000..ef3190526 --- /dev/null +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -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) +} |