diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 1c79d524c7..96f096f70d 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useState} from 'react' +import {useCallback, useMemo, useRef, useState} from 'react' import { LayoutAnimation, ListRenderItem, @@ -13,12 +13,7 @@ import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {useEvent} from 'expo' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' -import { - useVideoPlayer, - VideoPlayer, - VideoPlayerStatus, - VideoView, -} from 'expo-video' +import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import { AppBskyEmbedVideo, AppBskyFeedDefs, @@ -41,7 +36,8 @@ import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isAndroid} from '#/platform/detection' -import {POST_TOMBSTONE, usePostShadow} from '#/state/cache/post-shadow' +import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' +import {usePostLikeMutationQueue} from '#/state/queries/post' import {FeedPostSliceItem, usePostFeedQuery} from '#/state/queries/post-feed' import {useSetMinimalShellMode} from '#/state/shell' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' @@ -56,6 +52,18 @@ import {Loader} from '#/components/Loader' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +function createThreeVideoPlayers( + sources?: [string, string, string], +): [VideoPlayer, VideoPlayer, VideoPlayer] { + const p1 = createVideoPlayer(sources?.[0] ?? '') + p1.loop = true + const p2 = createVideoPlayer(sources?.[1] ?? '') + p2.loop = true + const p3 = createVideoPlayer(sources?.[2] ?? '') + p3.loop = true + return [p1, p2, p3] +} + type Props = NativeStackScreenProps export function VibeScreen({}: Props) { const {top} = useSafeAreaInsets() @@ -78,7 +86,7 @@ export function VibeScreen({}: Props) { @@ -109,23 +117,17 @@ function YoloFeed() { fetchNextPage, } = usePostFeedQuery(`feedgen|${VIBES_FEED_URI}`) + const videos = data?.pages.flatMap(page => + page.slices.flatMap(slice => slice.items), + ) + const [currentSources, setCurrentSources] = useState< [string | null, string | null, string | null] >([null, null, null]) - const player1 = useVideoPlayer('', p => { - p.loop = true - }) - const player2 = useVideoPlayer('', p => { - p.loop = true - }) - const player3 = useVideoPlayer('', p => { - p.loop = true - }) - - const videos = data?.pages.flatMap(page => - page.slices.flatMap(slice => slice.items), - ) + const [players, setPlayers] = useState< + [VideoPlayer, VideoPlayer, VideoPlayer] | null + >(createThreeVideoPlayers) const [currentIndex, setCurrentIndex] = useState(0) @@ -136,7 +138,7 @@ function YoloFeed() { return null } - const player = [player1, player2, player3][index % 3] + const player = players?.[index % 3] const currentSource = currentSources[index % 3] return ( @@ -152,12 +154,18 @@ function YoloFeed() { /> ) }, - [player1, player2, player3, currentIndex, isFocused, currentSources], + [players, currentIndex, isFocused, currentSources], ) - const updateVideoState = useNonReactiveCallback((index: number) => { + const updateVideoState = useNonReactiveCallback((index?: number) => { if (!videos) return - setCurrentIndex(index) + + if (index === undefined) { + index = currentIndex + } else { + setCurrentIndex(index) + } + setCurrentSources(oldSources => { const currentSources = [...oldSources] as [ string | null, @@ -181,32 +189,59 @@ function YoloFeed() { ? nextEmbed.playlist : null - const prevPlayer = [player1, player2, player3][(index + 2) % 3] const prevPlayerCurrentSource = currentSources[(index + 2) % 3] - const currPlayer = [player1, player2, player3][index % 3] const currPlayerCurrentSource = currentSources[index % 3] - const nextPlayer = [player1, player2, player3][(index + 1) % 3] const nextPlayerCurrentSource = currentSources[(index + 1) % 3] + if (!players) { + const args = ['', '', ''] satisfies [string, string, string] + if (prevVideo) args[(index + 2) % 3] = prevVideo + if (currVideo) args[index % 3] = currVideo + if (nextVideo) args[(index + 1) % 3] = nextVideo + const [player1, player2, player3] = createThreeVideoPlayers(args) + + setPlayers([player1, player2, player3]) + + if (currVideo) { + const currPlayer = [player1, player2, player3][index % 3] + currPlayer.play() + } + } else { + const [player1, player2, player3] = players + + const prevPlayer = [player1, player2, player3][(index + 2) % 3] + const currPlayer = [player1, player2, player3][index % 3] + const nextPlayer = [player1, player2, player3][(index + 1) % 3] + + if (prevVideo && prevVideo !== prevPlayerCurrentSource) { + prevPlayer.replace(prevVideo) + } + prevPlayer.pause() + + if (currVideo) { + if (currVideo !== currPlayerCurrentSource) { + currPlayer.replace(currVideo) + } + currPlayer.play() + } + + if (nextVideo && nextVideo !== nextPlayerCurrentSource) { + nextPlayer.replace(nextVideo) + } + nextPlayer.pause() + } + if (prevVideo && prevVideo !== prevPlayerCurrentSource) { - prevPlayer.replace(prevVideo) currentSources[(index + 2) % 3] = prevVideo } - prevPlayer.pause() - if (currVideo) { - if (currVideo !== currPlayerCurrentSource) { - currPlayer.replace(currVideo) - currentSources[index % 3] = currVideo - } - currPlayer.play() + if (currVideo && currVideo !== currPlayerCurrentSource) { + currentSources[index % 3] = currVideo } if (nextVideo && nextVideo !== nextPlayerCurrentSource) { - nextPlayer.replace(nextVideo) currentSources[(index + 1) % 3] = nextVideo } - nextPlayer.pause() // use old array if no changes if ( @@ -220,6 +255,20 @@ function YoloFeed() { }) }) + useFocusEffect( + useCallback(() => { + if (!players) { + updateVideoState() + } + return () => { + if (players) { + players.forEach(p => p.release()) + setPlayers(null) + } + } + }, [players, updateVideoState]), + ) + const onViewableItemsChanged = useCallback( ({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { if (viewableItems[0] && viewableItems[0].index !== null) { @@ -266,22 +315,15 @@ function VibeItem({ embed, active, }: { - player: VideoPlayer + player?: VideoPlayer post: AppBskyFeedDefs.PostView embed: AppBskyEmbedVideo.View active: boolean }) { + const postShadow = usePostShadow(post) const {height, width} = useWindowDimensions() const insets = useSafeAreaInsets() - 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 - - const {status} = useEvent(player, 'statusChange', {status: player.status}) - return ( - {active && ( - + {player ? ( + + ) : ( + embed.thumbnail && ( + + ) )} - {embed.thumbnail && ( - + ) : ( + + a.inset_0, + a.z_20, + a.align_center, + a.justify_center, + {backgroundColor: 'rgba(0, 0, 0, 0.8)'}, + ]}> + + Post has been deleted + + )} - ) } +function VibeItemInner({ + player, + embed, + active, +}: { + player: VideoPlayer + embed: AppBskyEmbedVideo.View + active: boolean +}) { + const insets = useSafeAreaInsets() + 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 ( + <> + {active && player && ( + + )} + {embed.thumbnail && ( + + )} + + ) +} + function VibeOverlay({ player, post, - status, }: { player: VideoPlayer - post: AppBskyFeedDefs.PostView - status: VideoPlayerStatus + post: Shadow }) { - const postShadow = usePostShadow(post) const insets = useSafeAreaInsets() const t = useTheme() const navigation = useNavigation() const [expanded, setExpanded] = useState(false) + const {status} = useEvent(player, 'statusChange', {status: player.status}) + const doubleTapRef = useRef | null>(null) + const [queueLike] = usePostLikeMutationQueue(post, 'Vibe') const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) @@ -349,6 +453,16 @@ function VibeOverlay({ } } + const onPress = () => { + if (doubleTapRef.current) { + clearTimeout(doubleTapRef.current) + doubleTapRef.current = null + queueLike() + } else { + doubleTapRef.current = setTimeout(togglePlayPause, 300) + } + } + const gesture = useMemo(() => { const dragLeftGesture = Gesture.Pan() .activeOffsetX([0, 10]) @@ -375,10 +489,11 @@ function VibeOverlay({ return ( <> - + @@ -412,10 +527,10 @@ function VibeOverlay({ authorHandle={post.author.handle} /> )} - {postShadow !== POST_TOMBSTONE && record && ( + {record && (