diff --git a/src/App.tsx b/src/App.tsx index 76f9198e97b8..a3a9f7a3f3b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvide import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; +import {FullScreenContextProvider} from './components/VideoPlayerContexts/FullScreenContext'; import {PlaybackContextProvider} from './components/VideoPlayerContexts/PlaybackContext'; import {VideoPopoverMenuContextProvider} from './components/VideoPlayerContexts/VideoPopoverMenuContext'; import {VolumeContextProvider} from './components/VideoPlayerContexts/VolumeContext'; @@ -78,6 +79,7 @@ function App({url}: AppProps) { ActiveElementRoleProvider, ActiveWorkspaceContextProvider, PlaybackContextProvider, + FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, ]} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 4988110538fe..ec2687d634bb 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -82,6 +82,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr isHovered={isModalHovered} isFocused={isFocused} duration={item.duration} + isUsedInCarousel /> diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f05abfd6a0de..3a7e0f19c4cd 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Illustrations from '@components/Icon/Illustrations'; +import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -32,6 +33,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const theme = useTheme(); const {translate} = useLocalize(); const styles = useThemeStyles(); + const {isFullScreenRef} = useFullScreenContext(); const scrollRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -76,6 +78,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /** Updates the page state when the user navigates between attachments */ const updatePage = useCallback( ({viewableItems}: UpdatePageProps) => { + if (isFullScreenRef.current) { + return; + } + Keyboard.dismiss(); // Since we can have only one item in view at a time, we can use the first item in the array @@ -95,12 +101,16 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate(entry.item); } }, - [onNavigate], + [isFullScreenRef, onNavigate], ); /** Increments or decrements the index to get another selected item */ const cycleThroughAttachments = useCallback( (deltaSlide: number) => { + if (isFullScreenRef.current) { + return; + } + const nextIndex = page + deltaSlide; const nextItem = attachments[nextIndex]; @@ -110,7 +120,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen}); }, - [attachments, canUseTouchScreen, page], + [attachments, canUseTouchScreen, isFullScreenRef, page], ); const extractItemKey = useCallback( @@ -145,7 +155,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, return ( setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width))} + onLayout={({nativeEvent}) => { + if (isFullScreenRef.current) { + return; + } + setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width)); + }} onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)} onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)} > diff --git a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx index 03e0c0252a66..9e91bfd64fed 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx @@ -12,13 +12,13 @@ type AttachmentViewVideoProps = Pick url.startsWith(prefix)); - const shouldUseSharedVideoElementRef = useRef(shouldUseSharedVideoElement); - - const [isFullscreen, setIsFullscreen] = useState(false); + const videoStateRef = useRef(null); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; if (!isCurrentlyURLSet) { updateCurrentlyPlayingURL(url); - } else if (isPlaying && !isFullscreen) { + } else if (isPlaying) { pauseVideo(); - } else if (!isFullscreen) { + } else { playVideo(); } - }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, isFullscreen]); + }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); const showPopoverMenu = (e) => { setPopoverAnchorPosition({horizontal: e.nativeEvent.pageX, vertical: e.nativeEvent.pageY}); @@ -99,7 +105,7 @@ function BaseVideoPlayer({ } videoResumeTryNumber.current -= 1; }, - [playVideo], + [playVideo, videoResumeTryNumber], ); const handlePlaybackStatusUpdate = useCallback( @@ -118,7 +124,7 @@ function BaseVideoPlayer({ setIsBuffering(e.isBuffering || false); setDuration(currentDuration); setPosition(currentPositon); - + videoStateRef.current = e; onPlaybackStatusUpdate(e); }, [onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration], @@ -127,22 +133,20 @@ function BaseVideoPlayer({ const handleFullscreenUpdate = useCallback( (e) => { onFullscreenUpdate(e); - - setIsFullscreen(e.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_PRESENT); - // fix for iOS native and mWeb: when switching to fullscreen and then exiting // the fullscreen mode while playing, the video pauses - if (!isPlaying || e.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) { - return; + if (e.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) { + isFullScreenRef.current = false; + // we need to use video state ref to check if video is playing, to catch proper state after exiting fullscreen + // and also fix a bug with fullscreen mode dismissing when handleFullscreenUpdate function changes + if (videoStateRef.current && videoStateRef.current.isPlaying) { + pauseVideo(); + playVideo(); + videoResumeTryNumber.current = 3; + } } - - if (isMobileSafari) { - pauseVideo(); - } - playVideo(); - videoResumeTryNumber.current = 3; }, - [isPlaying, onFullscreenUpdate, pauseVideo, playVideo], + [isFullScreenRef, onFullscreenUpdate, pauseVideo, playVideo, videoResumeTryNumber], ); const bindFunctions = useCallback(() => { @@ -156,7 +160,7 @@ function BaseVideoPlayer({ }, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]); useEffect(() => { - if (!isUploading) { + if (!isUploading || !videoPlayerRef.current) { return; } @@ -164,37 +168,29 @@ function BaseVideoPlayer({ currentVideoPlayerRef.current = videoPlayerRef.current; }, [url, currentVideoPlayerRef, isUploading]); - useEffect(() => { - shouldUseSharedVideoElementRef.current = shouldUseSharedVideoElement; - }, [shouldUseSharedVideoElement]); - - useEffect( - () => () => { - if (shouldUseSharedVideoElementRef.current) { - return; - } - - // If it's not a shared video player, clear the video player ref. - currentVideoPlayerRef.current = null; - }, - [currentVideoPlayerRef], - ); - // update shared video elements useEffect(() => { - if (shouldUseSharedVideoElement || url !== currentlyPlayingURL) { + if (shouldUseSharedVideoElement || url !== currentlyPlayingURL || isFullScreenRef.current) { return; } shareVideoPlayerElements(videoPlayerRef.current, videoPlayerElementParentRef.current, videoPlayerElementRef.current, isUploading); - }, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url, isUploading]); + }, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url, isUploading, isFullScreenRef]); // append shared video element to new parent (used for example in attachment modal) useEffect(() => { - if (url !== currentlyPlayingURL || !sharedElement || !shouldUseSharedVideoElement) { + if (url !== currentlyPlayingURL || !sharedElement || isFullScreenRef.current) { return; } const newParentRef = sharedVideoPlayerParentRef.current; + + if (!shouldUseSharedVideoElement) { + if (newParentRef && newParentRef.childNodes[0] && newParentRef.childNodes[0].remove) { + newParentRef.childNodes[0].remove(); + } + return; + } + videoPlayerRef.current = currentVideoPlayerRef.current; if (currentlyPlayingURL === url) { newParentRef.appendChild(sharedElement); @@ -204,9 +200,10 @@ function BaseVideoPlayer({ if (!originalParent && !newParentRef.childNodes[0]) { return; } + newParentRef.childNodes[0].remove(); originalParent.appendChild(sharedElement); }; - }, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, originalParent, sharedElement, shouldUseSharedVideoElement, url]); + }, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isFullScreenRef, originalParent, sharedElement, shouldUseSharedVideoElement, url]); return ( <> @@ -222,6 +219,9 @@ function BaseVideoPlayer({ { + if (isFullScreenRef.current) { + return; + } togglePlayCurrentVideo(); }} style={styles.flex1} diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/index.tsx index 7c61721b67b7..1ac07fd1eaaf 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/index.tsx @@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import IconButton from '@components/VideoPlayer/IconButton'; import {convertMillisecondsToTime} from '@components/VideoPlayer/utils'; +import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -48,6 +49,7 @@ function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying const styles = useThemeStyles(); const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); + const {isFullScreenRef} = useFullScreenContext(); const [shouldShowTime, setShouldShowTime] = useState(false); const iconSpacing = small ? styles.mr3 : styles.mr4; @@ -56,9 +58,10 @@ function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying }; const enterFullScreenMode = useCallback(() => { + isFullScreenRef.current = true; updateCurrentlyPlayingURL(url); videoPlayerRef.current.presentFullscreenPlayer(); - }, [updateCurrentlyPlayingURL, url, videoPlayerRef]); + }, [isFullScreenRef, updateCurrentlyPlayingURL, url, videoPlayerRef]); const seekPosition = useCallback( (newPosition: number) => { diff --git a/src/components/VideoPlayerContexts/FullScreenContext.tsx b/src/components/VideoPlayerContexts/FullScreenContext.tsx new file mode 100644 index 000000000000..8634e27b9d1e --- /dev/null +++ b/src/components/VideoPlayerContexts/FullScreenContext.tsx @@ -0,0 +1,34 @@ +import React, {useCallback, useContext, useMemo, useRef} from 'react'; +import type WindowDimensions from '@hooks/useWindowDimensions/types'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {FullScreenContext} from './types'; + +const Context = React.createContext(null); + +function FullScreenContextProvider({children}: ChildrenProps) { + const isFullScreenRef = useRef(false); + const lockedWindowDimensionsRef = useRef(null); + + const lockWindowDimensions = useCallback((newWindowDimensions: WindowDimensions) => { + lockedWindowDimensionsRef.current = newWindowDimensions; + }, []); + + const unlockWindowDimensions = useCallback(() => { + lockedWindowDimensionsRef.current = null; + }, []); + + const contextValue = useMemo(() => ({isFullScreenRef, lockedWindowDimensionsRef, lockWindowDimensions, unlockWindowDimensions}), [lockWindowDimensions, unlockWindowDimensions]); + return {children}; +} + +function useFullScreenContext() { + const fullscreenContext = useContext(Context); + if (!fullscreenContext) { + throw new Error('useFullScreenContext must be used within a FullScreenContextProvider'); + } + return fullscreenContext; +} + +FullScreenContextProvider.displayName = 'FullScreenContextProvider'; + +export {Context as FullScreenContext, FullScreenContextProvider, useFullScreenContext}; diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index fc07f3d20ea0..71d999da6aec 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -13,6 +13,7 @@ function PlaybackContextProvider({children}: ChildrenProps) { const [originalParent, setOriginalParent] = useState(null); const currentVideoPlayerRef = useRef