diff --git a/native/media/video-playback-modal.react.js b/native/media/video-playback-modal.react.js index e2c3175bb..6a876a2af 100644 --- a/native/media/video-playback-modal.react.js +++ b/native/media/video-playback-modal.react.js @@ -1,495 +1,565 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useState } from 'react'; import { View, Text } from 'react-native'; import { TapGestureHandler, TouchableWithoutFeedback, } from 'react-native-gesture-handler'; import * as Progress from 'react-native-progress'; import Animated from 'react-native-reanimated'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Video from 'react-native-video'; import { useIsAppBackgroundedOrInactive } from 'lib/shared/lifecycle-utils'; import type { MediaInfo } from 'lib/types/media-types'; import type { ChatMultimediaMessageInfoItem } from '../chat/multimedia-message.react'; import Button from '../components/button.react'; import ConnectedStatusBar from '../connected-status-bar.react'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { OverlayContext } from '../navigation/overlay-context'; import type { NavigationRoute } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors'; import { useStyles } from '../themes/colors'; import type { VerticalBounds, LayoutCoordinates } from '../types/layout-types'; import { gestureJustEnded, animateTowards } from '../utils/animation-utils'; import { formatDuration } from './video-utils'; /* eslint-disable import/no-named-as-default-member */ const { Extrapolate, cond, set, add, sub, multiply, divide, not, max, min, abs, interpolate, useValue, event, } = Animated; export type VideoPlaybackModalParams = {| +presentedFrom: string, +mediaInfo: MediaInfo, +initialCoordinates: LayoutCoordinates, +verticalBounds: VerticalBounds, +item: ChatMultimediaMessageInfoItem, |}; type Props = {| +navigation: AppNavigationProp<'VideoPlaybackModal'>, +route: NavigationRoute<'VideoPlaybackModal'>, |}; function VideoPlaybackModal(props: Props) { const { mediaInfo } = props.route.params; /* ===== START FADE CONTROL ANIMATION ===== */ const singleTapState = useValue(-1); const singleTapX = useValue(0); const singleTapY = useValue(0); const singleTapEvent = React.useCallback( event([ { nativeEvent: { state: singleTapState, x: singleTapX, y: singleTapY, }, }, ]), [], ); const controlsShowing = useValue(1); const controlsOpacity = React.useMemo( () => animateTowards( [ cond( gestureJustEnded(singleTapState), set(controlsShowing, not(controlsShowing)), ), controlsShowing, ], 150, ), [singleTapState, controlsShowing], ); /* ===== END FADE CONTROL ANIMATION ===== */ const mediaDimensions = mediaInfo.dimensions; const screenDimensions = useSelector(derivedDimensionsInfoSelector); const frame = React.useMemo( () => ({ width: screenDimensions.width, height: screenDimensions.safeAreaHeight, }), [screenDimensions], ); const mediaDisplayDimensions = React.useMemo(() => { let { height: maxHeight, width: maxWidth } = frame; if (maxHeight > maxWidth) { maxHeight -= 100; } else { maxWidth -= 100; } if ( mediaDimensions.height < maxHeight && mediaDimensions.width < maxWidth ) { return mediaDimensions; } const heightRatio = maxHeight / mediaDimensions.height; const widthRatio = maxWidth / mediaDimensions.width; if (heightRatio < widthRatio) { return { height: maxHeight, width: mediaDimensions.width * heightRatio, }; } else { return { width: maxWidth, height: mediaDimensions.height * widthRatio, }; } }, [frame, mediaDimensions]); const centerX = useValue(frame.width / 2); const centerY = useValue(frame.height / 2 + screenDimensions.topInset); const frameWidth = useValue(frame.width); const frameHeight = useValue(frame.height); const imageWidth = useValue(mediaDisplayDimensions.width); const imageHeight = useValue(mediaDisplayDimensions.height); React.useEffect(() => { const { width: frameW, height: frameH } = frame; const { topInset } = screenDimensions; frameWidth.setValue(frameW); frameHeight.setValue(frameH); centerX.setValue(frameW / 2); centerY.setValue(frameH / 2 + topInset); const { width, height } = mediaDisplayDimensions; imageWidth.setValue(width); imageHeight.setValue(height); }, [ screenDimensions, frame, mediaDisplayDimensions, frameWidth, frameHeight, centerX, centerY, imageWidth, imageHeight, ]); - const left = sub(centerX, divide(imageWidth, 2)); - const top = sub(centerY, divide(imageHeight, 2)); + const left = React.useMemo(() => sub(centerX, divide(imageWidth, 2)), [ + centerX, + imageWidth, + ]); + const top = React.useMemo(() => sub(centerY, divide(imageHeight, 2)), [ + centerY, + imageHeight, + ]); const { initialCoordinates } = props.route.params; - const initialScale = divide(initialCoordinates.width, imageWidth); - const initialTranslateX = sub( - initialCoordinates.x + initialCoordinates.width / 2, - add(left, divide(imageWidth, 2)), + + const initialScale = React.useMemo( + () => divide(initialCoordinates.width, imageWidth), + [initialCoordinates, imageWidth], ); - const initialTranslateY = sub( - initialCoordinates.y + initialCoordinates.height / 2, - add(top, divide(imageHeight, 2)), + + const initialTranslateX = React.useMemo( + () => + sub( + initialCoordinates.x + initialCoordinates.width / 2, + add(left, divide(imageWidth, 2)), + ), + [initialCoordinates, left, imageWidth], + ); + + const initialTranslateY = React.useMemo( + () => + sub( + initialCoordinates.y + initialCoordinates.height / 2, + add(top, divide(imageHeight, 2)), + ), + [initialCoordinates, top, imageHeight], ); // The all-important outputs const curScale = useValue(1); const curX = useValue(0); const curY = useValue(0); const curBackdropOpacity = useValue(1); - const progressiveOpacity = max( - min( - sub(1, abs(divide(curX, frameWidth))), - sub(1, abs(divide(curY, frameHeight))), - ), - 0, + const progressiveOpacity = React.useMemo( + () => + max( + min( + sub(1, abs(divide(curX, frameWidth))), + sub(1, abs(divide(curY, frameHeight))), + ), + 0, + ), + [curX, curY, frameWidth, frameHeight], ); - const updates = [set(curBackdropOpacity, progressiveOpacity)]; - const updatedScale = [updates, curScale]; - const updatedCurX = [updates, curX]; - const updatedCurY = [updates, curY]; - const updatedBackdropOpacity = [updates, curBackdropOpacity]; + const updates = React.useMemo( + () => [set(curBackdropOpacity, progressiveOpacity)], + [curBackdropOpacity, progressiveOpacity], + ); + const updatedScale = React.useMemo(() => [updates, curScale], [ + updates, + curScale, + ]); + const updatedCurX = React.useMemo(() => [updates, curX], [updates, curX]); + const updatedCurY = React.useMemo(() => [updates, curY], [updates, curY]); + const updatedBackdropOpacity = React.useMemo( + () => [updates, curBackdropOpacity], + [updates, curBackdropOpacity], + ); const overlayContext = React.useContext(OverlayContext); invariant(overlayContext, 'VideoPlaybackModal should have OverlayContext'); const navigationProgress = overlayContext.position; - const reverseNavigationProgress = sub(1, navigationProgress); - const scale = add( - multiply(reverseNavigationProgress, initialScale), - multiply(navigationProgress, updatedScale), + const reverseNavigationProgress = React.useMemo( + () => sub(1, navigationProgress), + [navigationProgress], ); - const x = add( - multiply(reverseNavigationProgress, initialTranslateX), - multiply(navigationProgress, updatedCurX), + + const scale = React.useMemo( + () => + add( + multiply(reverseNavigationProgress, initialScale), + multiply(navigationProgress, updatedScale), + ), + [reverseNavigationProgress, initialScale, navigationProgress, updatedScale], + ); + + const x = React.useMemo( + () => + add( + multiply(reverseNavigationProgress, initialTranslateX), + multiply(navigationProgress, updatedCurX), + ), + [ + reverseNavigationProgress, + initialTranslateX, + navigationProgress, + updatedCurX, + ], ); - const y = add( - multiply(reverseNavigationProgress, initialTranslateY), - multiply(navigationProgress, updatedCurY), + + const y = React.useMemo( + () => + add( + multiply(reverseNavigationProgress, initialTranslateY), + multiply(navigationProgress, updatedCurY), + ), + [ + reverseNavigationProgress, + initialTranslateY, + navigationProgress, + updatedCurY, + ], + ); + + const backdropOpacity = React.useMemo( + () => multiply(navigationProgress, updatedBackdropOpacity), + [navigationProgress, updatedBackdropOpacity], + ); + + const imageContainerOpacity = React.useMemo( + () => + interpolate(navigationProgress, { + inputRange: [0, 0.1], + outputRange: [0, 1], + extrapolate: Extrapolate.CLAMP, + }), + [navigationProgress], ); - const backdropOpacity = multiply(navigationProgress, updatedBackdropOpacity); - const imageContainerOpacity = interpolate(navigationProgress, { - inputRange: [0, 0.1], - outputRange: [0, 1], - extrapolate: Extrapolate.CLAMP, - }); const { verticalBounds } = props.route.params; const videoContainerStyle = React.useMemo(() => { const { height, width } = mediaDisplayDimensions; const { height: frameH, width: frameW } = frame; return { height, width, marginTop: (frameH - height) / 2 + screenDimensions.topInset - verticalBounds.y, marginLeft: (frameW - width) / 2, opacity: imageContainerOpacity, transform: [{ translateX: x }, { translateY: y }, { scale: scale }], }; }, [ mediaDisplayDimensions, frame, screenDimensions.topInset, verticalBounds.y, imageContainerOpacity, x, y, scale, ]); const styles = useStyles(unboundStyles); const [paused, setPaused] = useState(false); const [percentElapsed, setPercentElapsed] = useState(0); const [spinnerVisible, setSpinnerVisible] = useState(true); const [timeElapsed, setTimeElapsed] = useState('0:00'); const [totalDuration, setTotalDuration] = useState('0:00'); const videoRef = React.useRef(); const backgroundedOrInactive = useIsAppBackgroundedOrInactive(); React.useEffect(() => { if (backgroundedOrInactive) { setPaused(true); controlsShowing.setValue(1); } }, [backgroundedOrInactive, controlsShowing]); const { navigation, route: { params: { mediaInfo: { uri: videoUri }, }, }, } = props; const togglePlayback = React.useCallback(() => { setPaused(!paused); }, [paused]); const resetVideo = React.useCallback(() => { invariant(videoRef.current, 'videoRef.current should be set in resetVideo'); videoRef.current.seek(0); }, []); const progressCallback = React.useCallback((res) => { setTimeElapsed(formatDuration(res.currentTime)); setTotalDuration(formatDuration(res.seekableDuration)); setPercentElapsed( Math.ceil((res.currentTime / res.seekableDuration) * 100), ); }, []); const readyForDisplayCallback = React.useCallback(() => { setSpinnerVisible(false); }, []); const statusBar = overlayContext.isDismissing ? null : (