Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS migration] Migrate VideoPlayerPreview, VideoPlayerControls and VideoPopoverMenu component files to TypeScript #37425

Merged
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7ef3d28
VideoPlayerThumbnail migreated to ts
smelaa Feb 27, 2024
1a70990
Migration of VideoPlayerPreview
smelaa Feb 28, 2024
aed45f8
Addressing review comments
smelaa Feb 28, 2024
0afcaae
Merge branch 'ts-migration-videoplayercontext' into ts-migration-vide…
smelaa Feb 28, 2024
d176eaf
VideoPopovermenu migrated to ts
smelaa Feb 28, 2024
03eae41
ProgressBar migrated to ts
smelaa Feb 29, 2024
2dbfe11
Merge branch 'ts-migration-videoplayercontext' into ts-migration-vide…
smelaa Feb 29, 2024
07947cd
VolumeButton migrated to ts
smelaa Feb 29, 2024
1c8b789
VideoPlayerControls migrated to ts
smelaa Feb 29, 2024
d2567a9
Changing types in VideoPlayerControls to match IconButton
smelaa Feb 29, 2024
528387f
Merging with main
smelaa Mar 1, 2024
361d4a0
Merge branch 'ts-migration-videoplayercontext' into ts-migration-vide…
smelaa Mar 1, 2024
2150628
Wrapping style in StyleProp component
smelaa Mar 1, 2024
4d84ed3
Migrate CategoryPicker to typescript
mateuuszzzzz Feb 12, 2024
b419a09
Merge branch 'ts-migration-videoplayercontext' into ts-migration-vide…
smelaa Mar 6, 2024
c768f5d
Address part of the review
smelaa Mar 13, 2024
6a50480
Address review comments
smelaa Mar 13, 2024
d2258f4
Solve typescript errors
smelaa Mar 13, 2024
fbef252
Merge branch 'main' into ts-migration-videoplayerpreview
smelaa Mar 14, 2024
db92a62
Add anchorref to VideoPopoverMenu
smelaa Mar 14, 2024
bea6377
Adress review comments
smelaa Mar 15, 2024
1a5c507
Address review comments.
smelaa Mar 15, 2024
410ef9f
Remove default value of props which are always set by a caller
smelaa Mar 18, 2024
7edb283
Merge branch 'main' into ts-migration-videoplayerpreview
smelaa Mar 18, 2024
607070c
Merge branch 'main' into ts-migration-videoplayerpreview
smelaa Mar 20, 2024
83a7bfd
Merge branch 'main' into ts-migration-videoplayerpreview
smelaa Mar 20, 2024
b0a2e69
Address review comments
smelaa Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Popover/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type PopoverProps = BaseModalProps &
anchorAlignment?: AnchorAlignment;

/** The anchor ref of the popover */
anchorRef: RefObject<View | HTMLDivElement>;
anchorRef?: RefObject<View | HTMLDivElement>;

/** Whether disable the animations */
disableAnimation?: boolean;
Expand Down
38 changes: 5 additions & 33 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type {ImageContentFit} from 'expo-image';
import type {RefObject} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
Expand All @@ -10,48 +9,21 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type IconAsset from '@src/types/utils/IconAsset';
import * as Expensicons from './Icon/Expensicons';
import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
import Text from './Text';

type PopoverMenuItem = {
/** An icon element displayed on the left side */
icon: IconAsset;

type PopoverMenuItem = MenuItemProps & {
/** Text label */
text: string;

/** A callback triggered when this item is selected */
onSelected: () => void;

/** A description text to show under the title */
description?: string;

/** The fill color to pass into the icon. */
iconFill?: string;

/** Icon Width */
iconWidth?: number;

/** Icon Height */
iconHeight?: number;

/** Icon should be displayed in its own color */
displayInDefaultIconColor?: boolean;

/** Determines how the icon should be resized to fit its container */
contentFit?: ImageContentFit;
onSelected?: () => void;

/** Sub menu items to be rendered after a menu item is selected */
subMenuItems?: PopoverMenuItem[];

/** Determines whether an icon should be displayed on the right side of the menu item. */
shouldShowRightIcon?: boolean;

/** Adds padding to the left of the text when there is no icon. */
shouldPutLeftPaddingWhenNoIcon?: boolean;
};

type PopoverModalProps = Pick<ModalProps, 'animationIn' | 'animationOut' | 'animationInTiming'>;
Expand Down Expand Up @@ -79,7 +51,7 @@ type PopoverMenuProps = Partial<PopoverModalProps> & {
anchorPosition: AnchorPosition;

/** Ref of the anchor */
anchorRef: RefObject<View | HTMLDivElement>;
anchorRef?: RefObject<View | HTMLDivElement>;

/** Where the popover should be positioned relative to the anchor points. */
anchorAlignment?: AnchorAlignment;
Expand Down Expand Up @@ -181,7 +153,7 @@ function PopoverMenu({
const onModalHide = () => {
setFocusedIndex(-1);
if (selectedItemIndex.current !== null) {
currentMenuItems[selectedItemIndex.current].onSelected();
currentMenuItems[selectedItemIndex.current].onSelected?.();
selectedItemIndex.current = null;
}
};
Expand Down
265 changes: 265 additions & 0 deletions src/components/VideoPlayer/BaseVideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/* eslint-disable no-underscore-dangle */
import {AVPlaybackStatus, Video, VideoFullscreenUpdate} from 'expo-av';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {GestureResponderEvent} from 'react-native';
import {View} from 'react-native';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Hoverable from '@components/Hoverable';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import VideoPopoverMenu from '@components/VideoPopoverMenu';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import * as Browser from '@libs/Browser';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import CONST from '@src/CONST';
import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes';
import shouldReplayVideo from './shouldReplayVideo';
import type VideoPlayerProps from './types';
import VideoPlayerControls from './VideoPlayerControls';

const isMobileSafari = Browser.isMobileSafari();

function BaseVideoPlayer({
url,
resizeMode,
onVideoLoaded,
isLooping,
style,
videoPlayerStyle,
videoStyle,
videoControlsStyle,
videoDuration,
shouldUseSharedVideoElement,
shouldUseSmallVideoControls,
shouldShowVideoControls,
onPlaybackStatusUpdate,
onFullscreenUpdate,
// TODO: investigate what is the root cause of the bug with unexpected video switching
// isVideoHovered caused a bug with unexpected video switching. We are investigating the root cause of the issue,
// but current workaround is just not to use it here for now. This causes not displaying the video controls when
// user hovers the mouse over the carousel arrows, but this UI bug feels much less troublesome for now.
// eslint-disable-next-line no-unused-vars
isVideoHovered,
}: VideoPlayerProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const {pauseVideo, playVideo, currentlyPlayingURL, updateSharedElements, sharedElement, originalParent, shareVideoPlayerElements, currentVideoPlayerRef, updateCurrentlyPlayingURL} =
usePlaybackContext();
const [duration, setDuration] = useState(videoDuration * 1000);
const [position, setPosition] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isBuffering, setIsBuffering] = useState(true);
const [sourceURL] = useState(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url));
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState({horizontal: 0, vertical: 0});
const videoPlayerRef = useRef<>(null);
const videoPlayerElementParentRef = useRef(null);
const videoPlayerElementRef = useRef(null);
const sharedVideoPlayerParentRef = useRef(null);
const videoResumeTryNumber = useRef(0);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const isCurrentlyURLSet = currentlyPlayingURL === url;
smelaa marked this conversation as resolved.
Show resolved Hide resolved
const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix));

const togglePlayCurrentVideo = useCallback(() => {
videoResumeTryNumber.current = 0;
if (!isCurrentlyURLSet) {
updateCurrentlyPlayingURL(url);
} else if (isPlaying) {
pauseVideo();
} else {
playVideo();
}
}, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url]);

const showPopoverMenu = (event: GestureResponderEvent) => {
setPopoverAnchorPosition({horizontal: event.nativeEvent.pageX, vertical: event.nativeEvent.pageY});
setIsPopoverVisible(true);
};

const hidePopoverMenu = () => {
setIsPopoverVisible(false);
};

// fix for iOS mWeb: preventing iOS native player edfault behavior from pausing the video when exiting fullscreen
const preventPausingWhenExitingFullscreen = useCallback(
(isVideoPlaying: boolean) => {
if (videoResumeTryNumber.current === 0 || isVideoPlaying) {
return;
}
if (videoResumeTryNumber.current === 1) {
playVideo();
}
videoResumeTryNumber.current -= 1;
},
[playVideo],
);

const handlePlaybackStatusUpdate = useCallback(
(status: AVPlaybackStatus) => {
if (!status.isLoaded){
return;
}
if (shouldReplayVideo(status, isPlaying, duration, position)) {
videoPlayerRef.current?.setStatusAsync?.({positionMillis: 0, shouldPlay: true});
}
const isVideoPlaying = status.isPlaying || false;
preventPausingWhenExitingFullscreen(isVideoPlaying);
setIsPlaying(isVideoPlaying);
setIsLoading(!status.isLoaded || Number.isNaN(status.durationMillis)); // when video is ready to display duration is not NaN
setIsBuffering(status.isBuffering || false);
setDuration(status.durationMillis || videoDuration * 1000);
setPosition(status.positionMillis || 0);

onPlaybackStatusUpdate(status);
},
[onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration, isPlaying, duration, position],
);

const handleFullscreenUpdate = useCallback(
(event) => {
onFullscreenUpdate(event);
// fix for iOS native and mWeb: when switching to fullscreen and then exiting
// the fullscreen mode while playing, the video pauses
if (!isPlaying || event.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
return;
}

if (isMobileSafari) {
pauseVideo();
}
playVideo();
videoResumeTryNumber.current = 3;
},
[isPlaying, onFullscreenUpdate, pauseVideo, playVideo],
);

const bindFunctions = useCallback(() => {
currentVideoPlayerRef.current._onPlaybackStatusUpdate = handlePlaybackStatusUpdate;
currentVideoPlayerRef.current._onFullscreenUpdate = handleFullscreenUpdate;
// update states after binding
currentVideoPlayerRef.current.getStatusAsync().then((status) => {
handlePlaybackStatusUpdate(status);
});
}, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]);

// update shared video elements
useEffect(() => {
if (shouldUseSharedVideoElement || url !== currentlyPlayingURL) {
return;
}
shareVideoPlayerElements(videoPlayerRef.current, videoPlayerElementParentRef.current, videoPlayerElementRef.current, isUploading);
}, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url, isUploading]);

// append shared video element to new parent (used for example in attachment modal)
useEffect(() => {
if (url !== currentlyPlayingURL || !sharedElement || !shouldUseSharedVideoElement) {
return;
}

const newParentRef = sharedVideoPlayerParentRef.current;
videoPlayerRef.current = currentVideoPlayerRef.current;
if (currentlyPlayingURL === url) {
newParentRef.appendChild(sharedElement);
bindFunctions();
}
return () => {
if (!originalParent && !newParentRef.childNodes[0]) {
return;
}
originalParent.appendChild(sharedElement);
};
}, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isSmallScreenWidth, originalParent, sharedElement, shouldUseSharedVideoElement, url]);

return (
<>
<View style={style}>
<Hoverable>
{(isHovered) => (
<View style={[styles.w100, styles.h100]}>
{shouldUseSharedVideoElement ? (
<>
<View
ref={sharedVideoPlayerParentRef}
style={[styles.flex1]}
/>
{/* We are adding transparent absolute View between appended video component and control buttons to enable
catching onMouse events from Attachment Carousel. Due to late appending React doesn't handle
element's events properly. */}
<View style={[styles.w100, styles.h100, styles.pAbsolute]} />
</>
) : (
<View
style={styles.flex1}
ref={(el) => {
if (!el) {
return;
}
videoPlayerElementParentRef.current = el;
if (el.childNodes && el.childNodes[0]) {
videoPlayerElementRef.current = el.childNodes[0];
}
}}
>
<PressableWithoutFeedback
accessibilityRole="button"
onPress={() => {
togglePlayCurrentVideo();
}}
style={styles.flex1}
>
<Video
ref={videoPlayerRef}
style={[styles.w100, styles.h100, videoPlayerStyle]}
videoStyle={[styles.w100, styles.h100, videoStyle]}
source={{
uri: sourceURL,
}}
shouldPlay={false}
useNativeControls={false}
resizeMode={resizeMode}
isLooping={isLooping}
onReadyForDisplay={onVideoLoaded}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
onFullscreenUpdate={handleFullscreenUpdate}
/>
</PressableWithoutFeedback>
</View>
)}

{(isLoading || isBuffering) && <FullScreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />}

{shouldShowVideoControls && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && (
<VideoPlayerControls
duration={duration}
position={position}
url={url}
videoPlayerRef={videoPlayerRef}
isPlaying={isPlaying}
small={shouldUseSmallVideoControls}
style={videoControlsStyle}
togglePlayCurrentVideo={togglePlayCurrentVideo}
showPopoverMenu={showPopoverMenu}
/>
)}
</View>
)}
</Hoverable>
</View>
<VideoPopoverMenu
isPopoverVisible={isPopoverVisible}
hidePopover={hidePopoverMenu}
anchorPosition={popoverAnchorPosition}
/>
</>
);
}

BaseVideoPlayer.propTypes = videoPlayerPropTypes;
BaseVideoPlayer.defaultProps = videoPlayerDefaultProps;
BaseVideoPlayer.displayName = 'BaseVideoPlayer';

export default BaseVideoPlayer;
Loading
Loading