* 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 9e3f2f4374)

* 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 3d1be4c0f1
Author: Samuel Newman <mozzius@protonmail.com>
Date:   Fri Jan 17 01:08:05 2025 +0000

    extend animation

commit c9f199413b
Author: Samuel Newman <mozzius@protonmail.com>
Date:   Fri Jan 17 01:01:24 2025 +0000

    fix gap

commit 22e520795f
Author: Samuel Newman <mozzius@protonmail.com>
Date:   Fri Jan 17 00:50:16 2025 +0000

    thinner bar, format time

commit c32427f214
Author: Samuel Newman <mozzius@protonmail.com>
Date:   Fri Jan 17 00:47:57 2025 +0000

    fix 2 in 3 screens

commit cbf84c08d6
Author: Samuel Newman <mozzius@protonmail.com>
Date:   Fri Jan 17 00:45:46 2025 +0000

    rm unneeded var

commit 7e0e100177
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>
This commit is contained in:
Hailey
2025-01-19 17:17:41 -08:00
committed by GitHub
parent cb02065550
commit 34582edf3e
38 changed files with 3165 additions and 126 deletions
+180
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
export function VideoScreen() {
return null
}
+18
View File
@@ -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
}