WIP cleanup

This commit is contained in:
Eric Bailey
2025-01-16 20:51:44 -06:00
parent 70ea56a740
commit 3f92cd4423
+210 -197
View File
@@ -19,7 +19,7 @@ import Animated, {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
import {useEvent, useEventListener} from 'expo' import {useEvent, useEventListener} from 'expo'
import {Image} from 'expo-image' import {Image, ImageStyle} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video'
import { import {
@@ -54,7 +54,7 @@ import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
import {List} from '#/view/com/util/List' import {List} from '#/view/com/util/List'
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {Header} from '#/screens/VideoFeed/Header' import {Header} from '#/screens/VideoFeed/Header'
import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf' import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -350,41 +350,12 @@ function VideoItem({
}) { }) {
const postShadow = usePostShadow(post) const postShadow = usePostShadow(post)
const {height, width} = useWindowDimensions() const {height, width} = useWindowDimensions()
const insets = useSafeAreaInsets()
return ( return (
<View <View
style={{ style={[a.relative, {height, width}]}>
height,
width,
}}>
<SafeAreaView edges={['left', 'right', 'bottom']} style={[a.flex_1]}> <SafeAreaView edges={['left', 'right', 'bottom']} style={[a.flex_1]}>
{player ? ( {postShadow === POST_TOMBSTONE ? (
<VideoItemInner player={player} embed={embed} active={active} />
) : (
embed.thumbnail && (
<Image
accessibilityIgnoresInvertColors
source={{uri: embed.thumbnail}}
style={[
a.flex_1,
a.absolute,
{
top: 0,
left: insets.left,
right: insets.right,
bottom: insets.bottom,
},
]}
contentFit="contain"
/>
)
)}
{postShadow !== POST_TOMBSTONE ? (
player && (
<Overlay player={player} post={postShadow} active={active} />
)
) : (
<View <View
style={[ style={[
a.absolute, a.absolute,
@@ -405,12 +376,21 @@ function VideoItem({
<Trans>Post has been deleted</Trans> <Trans>Post has been deleted</Trans>
</Text> </Text>
</View> </View>
) : (
<>
<VideoItemPlaceholder embed={embed} />
{player && (
<VideoItemInner player={player} embed={embed} active={active} />
)}
<Overlay player={player} post={postShadow} active={active} />
</>
)} )}
</SafeAreaView> </SafeAreaView>
</View> </View>
) )
} }
// TODO maybe unused
function VideoItemInner({ function VideoItemInner({
player, player,
embed, embed,
@@ -420,44 +400,25 @@ function VideoItemInner({
embed: AppBskyEmbedVideo.View embed: AppBskyEmbedVideo.View
active: boolean active: boolean
}) { }) {
const insets = useSafeAreaInsets() // const {status} = useEvent(player, 'statusChange', {status: player.status})
const {status} = useEvent(player, 'statusChange', {status: player.status})
const videoAspectRatio =
(embed.aspectRatio?.width ?? 1) / (embed.aspectRatio?.height ?? 1)
// if the video tall enough (tiktok/reels are 9:16) go cover mode
const isCloseEnough = videoAspectRatio <= 9 / 16
return ( return (
<> <>
{/*
<VideoItemPlaceholder embed={embed} style={{
zIndex: status === 'loading' && isAndroid ? 1 : -1,
}} />
*/}
{active && player && ( {active && player && (
<VideoView <VideoView
style={[a.flex_1]} style={[a.absolute, a.inset_0]}
player={player} player={player}
nativeControls={false} nativeControls={false}
contentFit={isCloseEnough ? 'cover' : 'contain'} contentFit={isTallAspectRatio(embed.aspectRatio) ? 'cover' : 'contain'}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
)} )}
{embed.thumbnail && (
<Image
accessibilityIgnoresInvertColors
source={{uri: embed.thumbnail}}
style={[
a.flex_1,
a.absolute,
{
zIndex: status === 'loading' && isAndroid ? 1 : -1,
top: 0,
left: insets.left,
right: insets.right,
bottom: insets.bottom,
},
]}
contentFit={isCloseEnough ? 'cover' : 'contain'}
/>
)}
</> </>
) )
} }
@@ -467,41 +428,19 @@ function Overlay({
post, post,
active, active,
}: { }: {
player: VideoPlayer player?: VideoPlayer
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
active: boolean active: boolean
}) { }) {
const insets = useSafeAreaInsets()
const t = useTheme() const t = useTheme()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {status} = useEvent(player, 'statusChange', {status: player.status}) // const {status} = useEvent(player, 'statusChange', {status: player.status})
const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo')
const seekingAnimationSV = useSharedValue(0) const seekingAnimationSV = useSharedValue(0)
const pushToProfile = useNonReactiveCallback(() => { const pushToProfile = useNonReactiveCallback(() => {
navigation.navigate('Profile', {name: post.author.did}) navigation.navigate('Profile', {name: post.author.did})
}) })
const togglePlayPause = () => {
doubleTapRef.current = null
if (player.playing) {
player.pause()
} else {
player.play()
}
}
const onPress = () => {
if (doubleTapRef.current) {
clearTimeout(doubleTapRef.current)
doubleTapRef.current = null
queueLike()
} else {
doubleTapRef.current = setTimeout(togglePlayPause, 200)
}
}
const gesture = useMemo(() => { const gesture = useMemo(() => {
const dragLeftGesture = Gesture.Pan() const dragLeftGesture = Gesture.Pan()
.activeOffsetX([0, 10]) .activeOffsetX([0, 10])
@@ -533,49 +472,43 @@ function Overlay({
<> <>
<View style={[a.absolute, a.inset_0, a.z_20]}> <View style={[a.absolute, a.inset_0, a.z_20]}>
<GestureDetector gesture={gesture}> <GestureDetector gesture={gesture}>
<Animated.View style={[a.flex_1, animatedStyle]}> <View style={[a.flex_1]}>
<Button <PlayPauseTapArea player={player} post={post} />
label="Toggle play/pause" </View>
accessibilityHint="Double tap to like" </GestureDetector>
onPress={onPress}
style={[a.flex_1]}> <LinearGradient
<View /> colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.8)', 'rgba(0,0,0,0.95)']}
</Button> style={[a.w_full, a.pt_md]}>
<LinearGradient <Animated.View style={[a.px_xl, animatedStyle]}>
colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.8)', 'rgba(0,0,0,0.95)']} <View style={[a.flex_row, a.gap_md, a.pb_sm, a.align_center]}>
style={[ <UserAvatar type='user' avatar={post.author.avatar} size={32} />
a.w_full, <View>
a.px_xl, <Text
a.py_sm, style={[a.text_md, a.font_heavy]}
a.gap_md, emoji
a.pb_3xl, numberOfLines={1}>
{marginBottom: tokens.space.xl * -1}, {sanitizeDisplayName(
]}> post.author.displayName || post.author.handle,
<View style={[a.flex_row, a.gap_md, a.align_center]}> )}
<PreviewableUserAvatar profile={post.author} size={32} /> </Text>
<View> <Text
<Text style={[a.text_sm, t.atoms.text_contrast_high]}
style={[a.text_md, a.font_heavy]} numberOfLines={1}>
emoji {sanitizeHandle(post.author.handle, '@')}
numberOfLines={1}> </Text>
{sanitizeDisplayName(
post.author.displayName || post.author.handle,
)}
</Text>
<Text
style={[a.text_sm, t.atoms.text_contrast_high]}
numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</Text>
</View>
</View> </View>
{record?.text?.trim() && ( </View>
{record?.text?.trim() && (
<View style={[a.pb_sm]}>
<ExpandableRichTextView <ExpandableRichTextView
value={richText} value={richText}
authorHandle={post.author.handle} authorHandle={post.author.handle}
/> />
)} </View>
{record && ( )}
{record && (
<View style={[{left: -5}]}>
<PostCtrls <PostCtrls
richText={richText} richText={richText}
post={post} post={post}
@@ -589,22 +522,18 @@ function Overlay({
} }
big big
/> />
)} </View>
</LinearGradient> )}
</Animated.View> </Animated.View>
</GestureDetector>
{player && active ? ( {player && active ? (
<Scrubber player={player} seekingAnimationSV={seekingAnimationSV} /> <Scrubber player={player} seekingAnimationSV={seekingAnimationSV} />
) : ( ) : (
<View <ScrubberPlaceholder />
style={[ )}
{height: tokens.space.xl}, </LinearGradient>
a.w_full,
{paddingBottom: insets.bottom},
]}
/>
)}
</View> </View>
{/*
{isAndroid && status === 'loading' && ( {isAndroid && status === 'loading' && (
<View <View
style={[ style={[
@@ -618,64 +547,16 @@ function Overlay({
<Loader size="2xl" /> <Loader size="2xl" />
</View> </View>
)} )}
*/}
</> </>
) )
} }
function ExpandableRichTextView({ /**
value, * Magic number that matches the Scrubber height
authorHandle, */
}: { function ScrubberPlaceholder() {
value: RichTextAPI return <View style={[a.w_full, {height: 62}]} />
authorHandle?: string
}) {
const {height: screenHeight} = useWindowDimensions()
const [expanded, setExpanded] = useState(false)
const [constrained, setConstrained] = useState(false)
const [contentHeight, setContentHeight] = useState(0)
const {_} = useLingui()
return (
<ScrollView
scrollEnabled={expanded}
onContentSizeChange={(_w, h) => {
if (expanded) {
LayoutAnimation.configureNext({
duration: 500,
update: {type: 'spring', springDamping: 0.6},
})
}
setContentHeight(h)
}}
style={{height: Math.min(contentHeight, screenHeight * 0.5)}}
contentContainerStyle={[
a.gap_xs,
expanded ? [a.align_start] : a.flex_row,
]}>
<RichText
value={value}
style={[a.text_sm, a.flex_1]}
authorHandle={authorHandle}
enableTags
numberOfLines={expanded ? undefined : constrained ? 1 : 2}
onTextLayout={evt => {
if (!constrained && evt.nativeEvent.lines.length > 1) {
setConstrained(true)
}
}}
/>
{constrained && (
<Button
label={expanded ? _(msg`Read less`) : _(msg`Read more`)}
hitSlop={HITSLOP_20}
onPress={() => setExpanded(prev => !prev)}>
<ButtonText>
{expanded ? <Trans>Read less</Trans> : <Trans>Read more</Trans>}
</ButtonText>
</Button>
)}
</ScrollView>
)
} }
function Scrubber({ function Scrubber({
@@ -796,7 +677,7 @@ function Scrubber({
{ {
left: 0, left: 0,
right: 0, right: 0,
bottom: tokens.space.xl + tokens.space._4xl + insets.bottom, bottom: insets.bottom + 48,
}, },
timeStyle, timeStyle,
]} ]}
@@ -818,15 +699,18 @@ function Scrubber({
</Text> </Text>
</Text> </Text>
</Animated.View> </Animated.View>
<GestureDetector gesture={gesture}> <GestureDetector gesture={gesture}>
<View <View
style={[ style={[
a.w_full,
{height: tokens.space.md},
a.pt_sm,
a.justify_end,
{paddingBottom: insets.bottom},
a.relative, a.relative,
a.w_full,
a.pt_lg,
a.justify_end,
{
height: tokens.space.md,
paddingBottom: insets.bottom + 12,
},
a.z_10, a.z_10,
]}> ]}>
<Animated.View <Animated.View
@@ -838,7 +722,136 @@ function Scrubber({
) )
} }
function ExpandableRichTextView({
value,
authorHandle,
}: {
value: RichTextAPI
authorHandle?: string
}) {
const {height: screenHeight} = useWindowDimensions()
const [expanded, setExpanded] = useState(false)
const [constrained, setConstrained] = useState(false)
const [contentHeight, setContentHeight] = useState(0)
const {_} = useLingui()
return (
<ScrollView
scrollEnabled={expanded}
onContentSizeChange={(_w, h) => {
if (expanded) {
LayoutAnimation.configureNext({
duration: 500,
update: {type: 'spring', springDamping: 0.6},
})
}
setContentHeight(h)
}}
style={{height: Math.min(contentHeight, screenHeight * 0.5)}}
contentContainerStyle={[
a.gap_xs,
expanded ? [a.align_start] : a.flex_row,
]}>
<RichText
value={value}
style={[a.text_sm, a.flex_1]}
authorHandle={authorHandle}
enableTags
numberOfLines={expanded ? undefined : constrained ? 1 : 2}
onTextLayout={evt => {
if (!constrained && evt.nativeEvent.lines.length > 1) {
setConstrained(true)
}
}}
/>
{constrained && (
<Button
label={expanded ? _(msg`Read less`) : _(msg`Read more`)}
hitSlop={HITSLOP_20}
onPress={() => setExpanded(prev => !prev)}>
<ButtonText>
{expanded ? <Trans>Read less</Trans> : <Trans>Read more</Trans>}
</ButtonText>
</Button>
)}
</ScrollView>
)
}
function VideoItemPlaceholder({
embed,
style,
}: {
embed: AppBskyEmbedVideo.View
style?: ImageStyle
}) {
const src = embed.thumbnail
return src ? (
<Image
accessibilityIgnoresInvertColors
source={{uri: src}}
style={[
a.absolute,
a.inset_0,
style,
]}
contentFit={isTallAspectRatio(embed.aspectRatio) ? 'cover' : 'contain'}
/>
) : null
}
function PlayPauseTapArea({
player,
post,
}: {
player?: VideoPlayer
post: Shadow<AppBskyFeedDefs.PostView>
}) {
const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo')
const togglePlayPause = () => {
if (!player) return
doubleTapRef.current = null
if (player.playing) {
player.pause()
} else {
player.play()
}
}
const onPress = () => {
if (doubleTapRef.current) {
clearTimeout(doubleTapRef.current)
doubleTapRef.current = null
queueLike()
} else {
doubleTapRef.current = setTimeout(togglePlayPause, 200)
}
}
return (
<Button
disabled={!player}
label="Toggle play/pause"
accessibilityHint="Double tap to like"
onPress={onPress}
style={[a.absolute, a.inset_0]}
>
<View />
</Button>
)
}
function clamp(num: number, min: number, max: number) { function clamp(num: number, min: number, max: number) {
'worklet' 'worklet'
return Math.min(Math.max(num, min), max) return Math.min(Math.max(num, min), max)
} }
/*
* If the video is taller than 9:16
*/
function isTallAspectRatio(aspectRatio: AppBskyEmbedVideo.View['aspectRatio']) {
const videoAspectRatio =
(aspectRatio?.width ?? 1) / (aspectRatio?.height ?? 1)
return videoAspectRatio <= 9 / 16
}