mirror of
https://github.com/danog/telegram-tt.git
synced 2024-11-27 04:45:08 +01:00
Media Viewer: Various fixes and improvements (#1710)
This commit is contained in:
parent
831d1589b4
commit
611ec32ee8
@ -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<OwnProps & StateProps> = (props) => {
|
||||
onFooterClick,
|
||||
isFooterHidden,
|
||||
isProtected,
|
||||
setIsFooterHidden,
|
||||
} = props;
|
||||
/* Content */
|
||||
const photo = message ? getMessagePhoto(message) : undefined;
|
||||
@ -139,6 +141,10 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (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<OwnProps & StateProps> = (props) => {
|
||||
)}
|
||||
{isVideo && ((!isActive && IS_TOUCH_ENV) ? renderVideoPreview(
|
||||
bestImageData,
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter, false),
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
) : (
|
||||
<VideoPlayer
|
||||
@ -197,10 +203,12 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (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<OwnProps & StateProps> = (props) => {
|
||||
<MediaViewerFooter
|
||||
text={textParts}
|
||||
onClick={onFooterClick}
|
||||
isHidden={isFooterHidden}
|
||||
isHidden={isFooterHidden && IS_TOUCH_ENV}
|
||||
isForVideo={isVideo && !isGif}
|
||||
/>
|
||||
)}
|
||||
|
@ -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<OwnProps> = ({
|
||||
const debounceActive = useDebounce(DEBOUNCE_ACTIVE, true);
|
||||
|
||||
const handleToggleFooterVisibility = useCallback((e: React.MouseEvent<HTMLDivElement, 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<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
<div className="MediaViewerSlides" ref={containerRef}>
|
||||
{previousMessageId && scale === 1 && (
|
||||
<div className="MediaViewerSlide" style={getAnimationStyle(-window.innerWidth + offsetX - SLIDES_GAP)}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<MediaViewerContent {...rest} messageId={previousMessageId} isFooterHidden={isFooterHidden} />
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
messageId={previousMessageId} />
|
||||
</div>
|
||||
)}
|
||||
{activeMessageId && (
|
||||
@ -524,14 +531,17 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
{...rest}
|
||||
messageId={activeMessageId}
|
||||
isActive={isActive && isActiveRef.current}
|
||||
setIsFooterHidden={setIsFooterHidden}
|
||||
isFooterHidden={isFooterHidden || isZoomed || scale !== 1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nextMessageId && scale === 1 && (
|
||||
<div className="MediaViewerSlide" style={getAnimationStyle(window.innerWidth + offsetX + SLIDES_GAP)}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<MediaViewerContent {...rest} messageId={nextMessageId} isFooterHidden={isFooterHidden} />
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
messageId={nextMessageId}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
@ -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<HTMLElement, MouseEvent>) => void;
|
||||
};
|
||||
|
||||
@ -41,12 +40,13 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
isMediaViewerOpen,
|
||||
noPlay,
|
||||
onClose,
|
||||
toggleControls,
|
||||
areControlsVisible,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const videoRef = useRef<HTMLVideoElement>(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<OwnProps> = ({
|
||||
} 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<HTMLElement, MouseEvent>) => {
|
||||
if (e.target === videoRef.current) {
|
||||
toggleControls(false);
|
||||
}
|
||||
}, [toggleControls]);
|
||||
|
||||
const handleTimeUpdate = useCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
setCurrentTime(e.currentTarget.currentTime);
|
||||
@ -111,8 +110,8 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
videoRef.current!.currentTime = position;
|
||||
}, []);
|
||||
|
||||
const toggleControls = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsControlsVisible(!isControlsVisible);
|
||||
}, [isControlsVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const togglePayingStateBySpace = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@ -152,8 +146,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
return (
|
||||
<div
|
||||
className="VideoPlayer"
|
||||
onClick={!isGif && IS_SINGLE_COLUMN_LAYOUT ? toggleControls : undefined}
|
||||
onMouseOver={!isGif && !IS_TOUCH_ENV ? handleMouseOver : undefined}
|
||||
onMouseMove={!isGif && !IS_TOUCH_ENV ? handleMouseMove : undefined}
|
||||
onMouseOut={!isGif && !IS_TOUCH_ENV ? handleMouseOut : undefined}
|
||||
>
|
||||
<div
|
||||
@ -172,7 +165,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
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}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<HTMLButtonElement, MouseEvent>) => void;
|
||||
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
isVisible: boolean;
|
||||
setVisibility: (isVisible: boolean) => void;
|
||||
onSeek: (position: number) => void;
|
||||
};
|
||||
|
||||
@ -32,68 +33,61 @@ const stopEvent = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const HIDE_CONTROLS_TIMEOUT_MS = 800;
|
||||
const HIDE_CONTROLS_TIMEOUT_MS = 1500;
|
||||
|
||||
const VideoPlayerControls: FC<IProps> = ({
|
||||
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<HTMLDivElement>(null);
|
||||
const isSeeking = useRef<boolean>(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<IProps> = ({
|
||||
});
|
||||
}, [isVisible, handleStartSeek, handleSeek, handleStopSeek]);
|
||||
|
||||
const isActive = isVisible || isForceVisible;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isActive && 'active')}
|
||||
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')}
|
||||
onClick={stopEvent}
|
||||
>
|
||||
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}
|
||||
|
Loading…
Reference in New Issue
Block a user