diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index 2ade806a4..190ac2a68 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from '../../lib/teact/teact'; +import React, { FC, memo, useCallback } from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; import { @@ -50,6 +50,7 @@ type OwnProps = { animationLevel: 0 | 1 | 2; onClose: () => void; onFooterClick: () => void; + setIsFooterHidden?: (isHidden: boolean) => void; isFooterHidden?: boolean; }; @@ -81,6 +82,7 @@ const MediaViewerContent: FC = (props) => { onFooterClick, isFooterHidden, isProtected, + setIsFooterHidden, } = props; /* Content */ const photo = message ? getMessagePhoto(message) : undefined; @@ -139,6 +141,10 @@ const MediaViewerContent: FC = (props) => { isGhostAnimation && ANIMATION_DURATION, ); + const toggleControls = useCallback((isVisible) => { + setIsFooterHidden?.(!isVisible); + }, [setIsFooterHidden]); + const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined; let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl; const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message)); @@ -189,7 +195,7 @@ const MediaViewerContent: FC = (props) => { )} {isVideo && ((!isActive && IS_TOUCH_ENV) ? renderVideoPreview( bestImageData, - message && calculateMediaViewerDimensions(dimensions!, hasFooter, false), + message && calculateMediaViewerDimensions(dimensions!, hasFooter, true), !IS_SINGLE_COLUMN_LAYOUT && !isProtected, ) : ( = (props) => { url={localBlobUrl || fullMediaBlobUrl} isGif={isGif} posterData={bestImageData} - posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, false)} + posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)} loadProgress={loadProgress} fileSize={videoSize!} isMediaViewerOpen={isOpen && isActive} + areControlsVisible={!isFooterHidden} + toggleControls={toggleControls} noPlay={!isActive} onClose={onClose} /> @@ -209,7 +217,7 @@ const MediaViewerContent: FC = (props) => { )} diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index e3b01655b..533b2ac69 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -49,6 +49,7 @@ const MAX_ZOOM = 4; const MIN_ZOOM = 0.6; const DOUBLE_TAP_ZOOM = 3; const CLICK_X_THRESHOLD = 40; +const CLICK_Y_THRESHOLD = 80; let cancelAnimation: Function | undefined; type Transform = { @@ -105,11 +106,12 @@ const MediaViewerSlides: FC = ({ const debounceActive = useDebounce(DEBOUNCE_ACTIVE, true); const handleToggleFooterVisibility = useCallback((e: React.MouseEvent) => { - if (!IS_TOUCH_ENV || !hasFooter || (!isPhoto && !isGif)) return; - if (e.pageX < CLICK_X_THRESHOLD) return; - if (e.pageX > window.innerWidth - CLICK_X_THRESHOLD) return; + if (!IS_TOUCH_ENV) return; + const isFooter = window.innerHeight - e.pageY < CLICK_Y_THRESHOLD; + if (!isFooter && e.pageX < CLICK_X_THRESHOLD) return; + if (!isFooter && e.pageX > window.innerWidth - CLICK_X_THRESHOLD) return; setIsFooterHidden(!isFooterHidden); - }, [hasFooter, isFooterHidden, isGif, isPhoto]); + }, [isFooterHidden]); useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150); @@ -140,6 +142,9 @@ const MediaViewerSlides: FC = ({ const changeSlide = (e: MouseEvent) => { if (transformRef.current.scale !== 1) return false; let direction = 0; + if (window.innerHeight - e.pageY < CLICK_Y_THRESHOLD) { + return false; + } if (e.pageX < CLICK_X_THRESHOLD) { direction = -1; } else if (e.pageX > window.innerWidth - CLICK_X_THRESHOLD) { @@ -508,8 +513,10 @@ const MediaViewerSlides: FC = ({
{previousMessageId && scale === 1 && (
- {/* eslint-disable-next-line react/jsx-props-no-spreading */} - +
)} {activeMessageId && ( @@ -524,14 +531,17 @@ const MediaViewerSlides: FC = ({ {...rest} messageId={activeMessageId} isActive={isActive && isActiveRef.current} + setIsFooterHidden={setIsFooterHidden} isFooterHidden={isFooterHidden || isZoomed || scale !== 1} />
)} {nextMessageId && scale === 1 && (
- {/* eslint-disable-next-line react/jsx-props-no-spreading */} - +
)} diff --git a/src/components/mediaViewer/VideoPlayer.scss b/src/components/mediaViewer/VideoPlayer.scss index fc79e1a84..6ca8480b4 100644 --- a/src/components/mediaViewer/VideoPlayer.scss +++ b/src/components/mediaViewer/VideoPlayer.scss @@ -49,6 +49,11 @@ @media (max-height: 640px) { max-height: calc(100vh - 10rem); } + + // Disable fullscreen on double tap on mobile devices + .is-touch-env & { + pointer-events: none; + } } .play-button { diff --git a/src/components/mediaViewer/VideoPlayer.tsx b/src/components/mediaViewer/VideoPlayer.tsx index 71de96b3d..ec2565f3d 100644 --- a/src/components/mediaViewer/VideoPlayer.tsx +++ b/src/components/mediaViewer/VideoPlayer.tsx @@ -1,22 +1,19 @@ -import React, { - FC, memo, useCallback, useEffect, useRef, useState, -} from '../../lib/teact/teact'; - import { ApiDimensions } from '../../api/types'; - -import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; -import useShowTransition from '../../hooks/useShowTransition'; import useBuffering from '../../hooks/useBuffering'; import useFullscreenStatus from '../../hooks/useFullscreen'; +import useShowTransition from '../../hooks/useShowTransition'; import useVideoCleanup from '../../hooks/useVideoCleanup'; -import safePlay from '../../util/safePlay'; +import React, { FC, memo, useCallback, useEffect, useRef, useState } from '../../lib/teact/teact'; -import VideoPlayerControls from './VideoPlayerControls'; -import ProgressSpinner from '../ui/ProgressSpinner'; +import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; +import safePlay from '../../util/safePlay'; import Button from '../ui/Button'; +import ProgressSpinner from '../ui/ProgressSpinner'; import './VideoPlayer.scss'; +import VideoPlayerControls from './VideoPlayerControls'; + type OwnProps = { url?: string; isGif?: boolean; @@ -26,6 +23,8 @@ type OwnProps = { fileSize: number; isMediaViewerOpen?: boolean; noPlay?: boolean; + areControlsVisible: boolean; + toggleControls: (isVisible: boolean) => void; onClose: (e: React.MouseEvent) => void; }; @@ -41,12 +40,13 @@ const VideoPlayer: FC = ({ isMediaViewerOpen, noPlay, onClose, + toggleControls, + areControlsVisible, }) => { // eslint-disable-next-line no-null/no-null const videoRef = useRef(null); const [isPlayed, setIsPlayed] = useState(!IS_TOUCH_ENV || !IS_IOS); const [currentTime, setCurrentTime] = useState(0); - const [isControlsVisible, setIsControlsVisible] = useState(true); const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreenStatus(videoRef, setIsPlayed); @@ -88,21 +88,20 @@ const VideoPlayer: FC = ({ } else { safePlay(videoRef.current!); setIsPlayed(true); - if (IS_SINGLE_COLUMN_LAYOUT) { - setIsControlsVisible(false); - } } }, [isPlayed]); useVideoCleanup(videoRef, []); - const handleMouseOver = useCallback(() => { - setIsControlsVisible(true); - }, []); + const handleMouseMove = useCallback(() => { + toggleControls(true); + }, [toggleControls]); - const handleMouseOut = useCallback(() => { - setIsControlsVisible(false); - }, []); + const handleMouseOut = useCallback((e: React.MouseEvent) => { + if (e.target === videoRef.current) { + toggleControls(false); + } + }, [toggleControls]); const handleTimeUpdate = useCallback((e: React.SyntheticEvent) => { setCurrentTime(e.currentTarget.currentTime); @@ -111,8 +110,8 @@ const VideoPlayer: FC = ({ const handleEnded = useCallback(() => { setCurrentTime(0); setIsPlayed(false); - setIsControlsVisible(true); - }, []); + toggleControls(true); + }, [toggleControls]); const handleFullscreenChange = useCallback(() => { if (isFullscreen && exitFullscreen) { @@ -126,11 +125,6 @@ const VideoPlayer: FC = ({ videoRef.current!.currentTime = position; }, []); - const toggleControls = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - setIsControlsVisible(!isControlsVisible); - }, [isControlsVisible]); - useEffect(() => { const togglePayingStateBySpace = (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { @@ -152,8 +146,7 @@ const VideoPlayer: FC = ({ return (
= ({ onPlay={IS_IOS ? () => setIsPlayed(true) : undefined} onEnded={handleEnded} onClick={!IS_SINGLE_COLUMN_LAYOUT ? togglePlayState : undefined} - onDoubleClick={handleFullscreenChange} + onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined} // eslint-disable-next-line react/jsx-props-no-spreading {...bufferingHandlers} onTimeUpdate={handleTimeUpdate} @@ -205,7 +198,8 @@ const VideoPlayer: FC = ({ isFullscreen={isFullscreen} fileSize={fileSize} duration={videoRef.current ? videoRef.current.duration || 0 : 0} - isForceVisible={isControlsVisible} + isVisible={areControlsVisible} + setVisibility={toggleControls} isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH} onSeek={handleSeek} onChangeFullscreen={handleFullscreenChange} diff --git a/src/components/mediaViewer/VideoPlayerControls.scss b/src/components/mediaViewer/VideoPlayerControls.scss index 9c5cb7eb6..56f083cc8 100644 --- a/src/components/mediaViewer/VideoPlayerControls.scss +++ b/src/components/mediaViewer/VideoPlayerControls.scss @@ -5,10 +5,10 @@ left: 0; bottom: 0; width: 100%; - padding-top: 0.625rem; + padding: 1rem 0.5rem 0.5rem; font-size: 0.875rem; background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%); - transition: opacity 0.15s; + transition: opacity 0.3s; opacity: 0; pointer-events: none; diff --git a/src/components/mediaViewer/VideoPlayerControls.tsx b/src/components/mediaViewer/VideoPlayerControls.tsx index e0e8a55fd..b521f6a9c 100644 --- a/src/components/mediaViewer/VideoPlayerControls.tsx +++ b/src/components/mediaViewer/VideoPlayerControls.tsx @@ -1,5 +1,5 @@ import React, { - FC, useState, useEffect, useRef, useCallback, + FC, useEffect, useRef, useCallback, } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; @@ -18,13 +18,14 @@ type IProps = { currentTime: number; duration: number; fileSize: number; - isForceVisible: boolean; isForceMobileVersion?: boolean; isPlayed: boolean; isFullscreenSupported: boolean; isFullscreen: boolean; onChangeFullscreen: (e: React.MouseEvent) => void; onPlayPause: (e: React.MouseEvent) => void; + isVisible: boolean; + setVisibility: (isVisible: boolean) => void; onSeek: (position: number) => void; }; @@ -32,68 +33,61 @@ const stopEvent = (e: React.MouseEvent) => { e.stopPropagation(); }; -const HIDE_CONTROLS_TIMEOUT_MS = 800; +const HIDE_CONTROLS_TIMEOUT_MS = 1500; const VideoPlayerControls: FC = ({ bufferedProgress, currentTime, duration, fileSize, - isForceVisible, isForceMobileVersion, isPlayed, isFullscreenSupported, isFullscreen, onChangeFullscreen, onPlayPause, + isVisible, + setVisibility, onSeek, }) => { - const [isVisible, setVisibility] = useState(true); // eslint-disable-next-line no-null/no-null const seekerRef = useRef(null); const isSeeking = useRef(false); - useEffect(() => { - if (isForceVisible) { - setVisibility(isForceVisible); - } - }, [isForceVisible]); useEffect(() => { let timeout: number | undefined; - - if (!isForceVisible) { - if (IS_SINGLE_COLUMN_LAYOUT) { - setVisibility(false); - } else { - timeout = window.setTimeout(() => { - setVisibility(false); - }, HIDE_CONTROLS_TIMEOUT_MS); - } + if (!isVisible || !isPlayed) { + if (timeout) window.clearTimeout(timeout); + return; } - + timeout = window.setTimeout(() => { + setVisibility(false); + }, HIDE_CONTROLS_TIMEOUT_MS); return () => { - if (timeout) { - window.clearTimeout(timeout); - } + if (timeout) window.clearTimeout(timeout); }; - }, [isForceVisible]); + }, [isPlayed, isVisible, setVisibility]); useEffect(() => { - if (isVisible || isForceVisible) { + if (isVisible) { document.body.classList.add('video-controls-visible'); + } else { + document.body.classList.remove('video-controls-visible'); } - return () => { document.body.classList.remove('video-controls-visible'); }; - }, [isForceVisible, isVisible]); + }, [isVisible]); const lang = useLang(); const handleSeek = useCallback((e: MouseEvent | TouchEvent) => { if (isSeeking.current && seekerRef.current) { - const { width, left } = seekerRef.current.getBoundingClientRect(); + const { + width, + left, + } = seekerRef.current.getBoundingClientRect(); const clientX = e instanceof MouseEvent ? e.clientX : e.targetTouches[0].clientX; onSeek(Math.max(Math.min(duration * ((clientX - left) / width), duration), 0)); } @@ -118,11 +112,10 @@ const VideoPlayerControls: FC = ({ }); }, [isVisible, handleStartSeek, handleSeek, handleStopSeek]); - const isActive = isVisible || isForceVisible; return (
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}