Media Viewer: Various improvements and fixes (#1657)

This commit is contained in:
Alexander Zinchuk 2022-01-25 03:24:20 +01:00
parent a1885d5e29
commit 92f15c200b
15 changed files with 203 additions and 105 deletions

View File

@ -3,7 +3,7 @@ import {
} from '../../../api/types'; } from '../../../api/types';
import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config'; import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
import { getPhotoInlineDimensions, getVideoDimensions } from '../../../modules/helpers'; import { getPhotoInlineDimensions, getVideoDimensions } from '../../../modules/helpers';
@ -110,10 +110,9 @@ export function getMediaViewerAvailableDimensions(withFooter: boolean, isVideo:
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY); const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
const { width: windowWidth, height: windowHeight } = windowSize.get(); const { width: windowWidth, height: windowHeight } = windowSize.get();
let occupiedHeightRem = isVideo && mql.matches ? 10 : 8.25; let occupiedHeightRem = isVideo && mql.matches ? 10 : 8.25;
if (withFooter) { if (withFooter && !IS_TOUCH_ENV) {
occupiedHeightRem = mql.matches ? 10 : 15; occupiedHeightRem = mql.matches ? 10 : 12.5;
} }
return { return {
width: windowWidth, width: windowWidth,
height: windowHeight - occupiedHeightRem * REM, height: windowHeight - occupiedHeightRem * REM,

View File

@ -27,7 +27,7 @@
} }
body.ghost-animating & { body.ghost-animating & {
> .pan-wrapper, > .Transition, > button { > .pan-wrapper, > button, .MediaViewerContent img, .MediaViewerContent .VideoPlayer {
display: none; display: none;
} }
} }

View File

@ -121,12 +121,13 @@ const MediaViewer: FC<StateProps> = ({
const isAvatar = Boolean(avatarOwner); const isAvatar = Boolean(avatarOwner);
/* Navigation */ /* Navigation */
const isSingleSlide = Boolean(webPagePhoto || webPageVideo); const singleMessageId = webPagePhoto || webPageVideo ? messageId : undefined;
const messageIds = useMemo(() => { const messageIds = useMemo(() => {
return isSingleSlide && messageId return singleMessageId
? [messageId] ? [singleMessageId]
: getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); : getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
}, [isSingleSlide, messageId, chatMessages, collectionIds, isFromSharedMedia]); }, [singleMessageId, chatMessages, collectionIds, isFromSharedMedia]);
const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1; const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1;
const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1; const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1;
@ -513,6 +514,7 @@ const MediaViewer: FC<StateProps> = ({
hasFooter={hasFooter} hasFooter={hasFooter}
isZoomed={isZoomed} isZoomed={isZoomed}
isActive={isActive} isActive={isActive}
isVideo={isVideo}
animationLevel={animationLevel} animationLevel={animationLevel}
onClose={close} onClose={close}
selectMessage={selectMessage} selectMessage={selectMessage}

View File

@ -176,6 +176,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
if (!message) return undefined; if (!message) return undefined;
const textParts = renderMessageText(message); const textParts = renderMessageText(message);
const hasFooter = Boolean(textParts); const hasFooter = Boolean(textParts);
return ( return (
<div <div
className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`} className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`}
@ -192,23 +193,23 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
url={localBlobUrl || fullMediaBlobUrl} url={localBlobUrl || fullMediaBlobUrl}
isGif={isGif} isGif={isGif}
posterData={bestImageData} posterData={bestImageData}
posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)} posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, false)}
loadProgress={loadProgress} loadProgress={loadProgress}
fileSize={videoSize!} fileSize={videoSize!}
isMediaViewerOpen={isOpen} isMediaViewerOpen={isOpen && isActive}
noPlay={!isActive} noPlay={!isActive}
onClose={onClose} onClose={onClose}
/> />
) : renderVideoPreview( ) : renderVideoPreview(
bestImageData, bestImageData,
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true), message && calculateMediaViewerDimensions(dimensions!, hasFooter, false),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
))} ))}
{textParts && ( {textParts && (
<MediaViewerFooter <MediaViewerFooter
text={textParts} text={textParts}
onClick={onFooterClick} onClick={onFooterClick}
isHidden={isFooterHidden && (!isVideo || isGif)} isHidden={isFooterHidden}
isForVideo={isVideo && !isGif} isForVideo={isVideo && !isGif}
/> />
)} )}

View File

@ -14,20 +14,24 @@
} }
@media (max-width: 600px) { @media (max-width: 600px) {
padding-bottom: 4.5rem;
background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%);
&.is-for-video { &.is-for-video {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
padding-bottom: 5rem;
.video-controls-visible & { .video-controls-visible &:not(.is-hidden) {
opacity: 1; opacity: 1;
pointer-events: all; pointer-events: all;
} }
} }
} }
body.ghost-animating & {
opacity: 0;
}
.media-viewer-footer-content { .media-viewer-footer-content {
position: relative; position: relative;
max-width: var(--messages-container-width); max-width: var(--messages-container-width);

View File

@ -8,13 +8,14 @@ import useDebounce from '../../hooks/useDebounce';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import { animateNumber, timingFunctions } from '../../util/animation'; import { animateNumber, timingFunctions } from '../../util/animation';
import arePropsShallowEqual from '../../util/arePropsShallowEqual'; import arePropsShallowEqual from '../../util/arePropsShallowEqual';
import { captureEvents } from '../../util/captureEvents'; import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD, RealTouchEvent } from '../../util/captureEvents';
import { IS_TOUCH_ENV } from '../../util/environment'; import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment';
import { debounce } from '../../util/schedulers'; import { debounce } from '../../util/schedulers';
import MediaViewerContent from './MediaViewerContent'; import MediaViewerContent from './MediaViewerContent';
import './MediaViewerSlides.scss'; import './MediaViewerSlides.scss';
import useTimeout from '../../hooks/useTimeout';
type OwnProps = { type OwnProps = {
messageId?: number; messageId?: number;
@ -47,6 +48,7 @@ const DEBOUNCE_ACTIVE = 800;
const MAX_ZOOM = 4; const MAX_ZOOM = 4;
const MIN_ZOOM = 0.6; const MIN_ZOOM = 0.6;
const DOUBLE_TAP_ZOOM = 3; const DOUBLE_TAP_ZOOM = 3;
const CLICK_X_THRESHOLD = 120;
let cancelAnimation: Function | undefined; let cancelAnimation: Function | undefined;
type Transform = { type Transform = {
@ -55,11 +57,10 @@ type Transform = {
scale: number; scale: number;
}; };
const INITIAL_TRANSFORM = { enum SwipeDirection {
x: 0, Horizontal,
y: 0, Vertical,
scale: 1, }
};
const MediaViewerSlides: FC<OwnProps> = ({ const MediaViewerSlides: FC<OwnProps> = ({
messageId, messageId,
@ -77,12 +78,12 @@ const MediaViewerSlides: FC<OwnProps> = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const activeSlideRef = useRef<HTMLDivElement>(null); const activeSlideRef = useRef<HTMLDivElement>(null);
const transformRef = useRef<Transform>(INITIAL_TRANSFORM); const transformRef = useRef<Transform>({ x: 0, y: 0, scale: 1 });
const isSwipingRef = useRef(false); const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
const isActiveRef = useRef(true); const isActiveRef = useRef(true);
const [activeMessageId, setActiveMessageId] = useState<number | undefined>(messageId); const [activeMessageId, setActiveMessageId] = useState<number | undefined>(messageId);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const [isFooterHidden, setIsFooterHidden] = useState<boolean>(false); const [isFooterHidden, setIsFooterHidden] = useState<boolean>(true);
const { const {
isZoomed, isZoomed,
@ -94,26 +95,24 @@ const MediaViewerSlides: FC<OwnProps> = ({
forceUpdate(); forceUpdate();
}, [forceUpdate]); }, [forceUpdate]);
const setIsSwiping = useCallback((value: boolean) => {
isSwipingRef.current = value;
forceUpdate();
}, [forceUpdate]);
const setIsActive = useCallback((value: boolean) => { const setIsActive = useCallback((value: boolean) => {
isActiveRef.current = value; isActiveRef.current = value;
forceUpdate(); forceUpdate();
}, [forceUpdate]); }, [forceUpdate]);
const debounceSetMessage = useDebounce(DEBOUNCE_MESSAGE, true); const debounceSetMessage = useDebounce(DEBOUNCE_MESSAGE, true);
const debounceSwipe = useDebounce(DEBOUNCE_SWIPE, true); const debounceSwipeDirection = useDebounce(DEBOUNCE_SWIPE, true);
const debounceActive = useDebounce(DEBOUNCE_ACTIVE, true); const debounceActive = useDebounce(DEBOUNCE_ACTIVE, true);
const handleToggleFooterVisibility = useCallback(() => { const handleToggleFooterVisibility = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (IS_TOUCH_ENV && (isPhoto || isGif) && hasFooter) { if (!IS_TOUCH_ENV || !hasFooter || (!isPhoto && !isGif)) return;
setIsFooterHidden(!isFooterHidden); if (e.clientX < CLICK_X_THRESHOLD) return;
} if (e.clientX > window.innerWidth - CLICK_X_THRESHOLD) return;
setIsFooterHidden(!isFooterHidden);
}, [hasFooter, isFooterHidden, isGif, isPhoto]); }, [hasFooter, isFooterHidden, isGif, isPhoto]);
useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150);
useEffect(() => { useEffect(() => {
if (!IS_TOUCH_ENV || !containerRef.current || isZoomed || !activeMessageId) { if (!IS_TOUCH_ENV || !containerRef.current || isZoomed || !activeMessageId) {
return undefined; return undefined;
@ -123,7 +122,10 @@ const MediaViewerSlides: FC<OwnProps> = ({
x: 0, x: 0,
y: 0, y: 0,
}; };
const lastZoomCenter = { x: 0, y: 0 }; const lastZoomCenter = {
x: 0,
y: 0,
};
const panDelta = { const panDelta = {
x: 0, x: 0,
y: 0, y: 0,
@ -134,18 +136,54 @@ const MediaViewerSlides: FC<OwnProps> = ({
const setLastGestureTime = debounce(() => { const setLastGestureTime = debounce(() => {
lastGestureTime = Date.now(); lastGestureTime = Date.now();
}, 500, false, true); }, 500, false, true);
const changeSlide = (e: MouseEvent) => {
if (transformRef.current.scale !== 1) return false;
let direction = 0;
if ((e as MouseEvent).clientX < CLICK_X_THRESHOLD) {
direction = -1;
} else if ((e as MouseEvent).clientX > window.innerWidth - CLICK_X_THRESHOLD) {
direction = 1;
}
const mId = getMessageId(activeMessageId, direction);
if (mId) {
const offset = (window.innerWidth + SLIDES_GAP) * direction;
transformRef.current.x += offset;
isActiveRef.current = false;
setActiveMessageId(mId);
debounceSetMessage(() => selectMessage(mId));
debounceActive(() => {
setIsActive(true);
});
lastTransform = { x: 0, y: 0, scale: 1 };
cancelAnimation = animateNumber({
from: transformRef.current.x,
to: 0,
duration: ANIMATION_DURATION,
timing: timingFunctions.easeOutCubic,
onUpdate: (value) => setTransform({
y: 0,
x: value,
scale: 1,
}),
});
}
return direction !== 0;
};
return captureEvents(containerRef.current, { return captureEvents(containerRef.current, {
isNotPassive: true, isNotPassive: true,
excludedClosestSelector: '.VideoPlayerControls, .MediaViewerFooter', excludedClosestSelector: '.VideoPlayerControls, .MediaViewerFooter',
onCapture: (event) => { onCapture: (event) => {
// Prevent safari back swipe on mobile // Avoid conflicts with swipe-to-back gestures
if (event.type === 'touchstart' if (event.type === 'touchstart' && IS_IOS) {
&& 'pageX' in event const x = (event as RealTouchEvent).touches[0].pageX;
&& !(event.pageX > 10 && event.pageX < window.innerWidth - 10)) { if (x <= IOS_SCREEN_EDGE_THRESHOLD || x >= window.innerWidth - IOS_SCREEN_EDGE_THRESHOLD) {
event.preventDefault(); event.preventDefault();
}
} }
lastGestureTime = Date.now(); lastGestureTime = Date.now();
if (arePropsShallowEqual(transformRef.current, INITIAL_TRANSFORM)) { if (arePropsShallowEqual(transformRef.current, { x: 0, y: 0, scale: 1 })) {
if (!activeSlideRef.current) return; if (!activeSlideRef.current) return;
content = activeSlideRef.current.querySelector('img, video'); content = activeSlideRef.current.querySelector('img, video');
if (!content) return; if (!content) return;
@ -167,7 +205,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
lastDragOffset.y = dragOffsetY; lastDragOffset.y = dragOffsetY;
const absOffsetX = Math.abs(dragOffsetX); const absOffsetX = Math.abs(dragOffsetX);
const absOffsetY = Math.abs(dragOffsetY); const absOffsetY = Math.abs(dragOffsetY);
const { scale, x, y } = transformRef.current; const {
scale,
x,
y,
} = transformRef.current;
const h = 10; const h = 10;
// If user is inactive but is still touching the screen // If user is inactive but is still touching the screen
@ -185,21 +227,25 @@ const MediaViewerSlides: FC<OwnProps> = ({
} }
return; return;
} }
// If user is swiping horizontally or horizontal shift is dominant if (swipeDirectionRef.current !== SwipeDirection.Vertical) {
// we change only horizontal position // If user is swiping horizontally or horizontal shift is dominant
if (isSwipingRef.current || Math.abs(x) > h || (absOffsetX > h && absOffsetY < h)) { // we change only horizontal position
isSwipingRef.current = true; if (swipeDirectionRef.current === SwipeDirection.Horizontal
isActiveRef.current = false; || Math.abs(x) > h || (absOffsetX > h && absOffsetY < h)) {
setTransform({ swipeDirectionRef.current = SwipeDirection.Horizontal;
x: dragOffsetX, isActiveRef.current = false;
y: 0, setTransform({
scale, x: dragOffsetX,
}); y: 0,
return; scale,
});
return;
}
} }
if (isSwipingRef.current) return;
// If vertical shift is dominant we change only vertical position // If vertical shift is dominant we change only vertical position
if (Math.abs(y) > h || (absOffsetY > h && absOffsetX < h)) { if (swipeDirectionRef.current === SwipeDirection.Vertical
|| Math.abs(y) > h || (absOffsetY > h && absOffsetX < h)) {
swipeDirectionRef.current = SwipeDirection.Vertical;
setTransform({ setTransform({
x: 0, x: 0,
y: dragOffsetY, y: dragOffsetY,
@ -240,14 +286,29 @@ const MediaViewerSlides: FC<OwnProps> = ({
scale, scale,
}); });
}, },
onClick(e) {
if (changeSlide(e as MouseEvent)) {
e.preventDefault();
e.stopPropagation();
}
},
onDoubleClick(e, { onDoubleClick(e, {
centerX, centerX,
centerY, centerY,
}) { }) {
if (changeSlide(e as MouseEvent)) {
e.preventDefault();
e.stopPropagation();
return undefined;
}
// Calculate how much we need to shift the image to keep the zoom center at the same position // Calculate how much we need to shift the image to keep the zoom center at the same position
const scaleOffsetX = (centerX - DOUBLE_TAP_ZOOM * centerX); const scaleOffsetX = (centerX - DOUBLE_TAP_ZOOM * centerX);
const scaleOffsetY = (centerY - DOUBLE_TAP_ZOOM * centerY); const scaleOffsetY = (centerY - DOUBLE_TAP_ZOOM * centerY);
const { scale, x, y } = transformRef.current; const {
scale,
x,
y,
} = transformRef.current;
if (scale === 1) { if (scale === 1) {
if (x !== 0 || y !== 0) return undefined; if (x !== 0 || y !== 0) return undefined;
lastTransform = { lastTransform = {
@ -256,7 +317,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
scale: DOUBLE_TAP_ZOOM, scale: DOUBLE_TAP_ZOOM,
}; };
} else { } else {
lastTransform = { x: 0, y: 0, scale: 1 }; lastTransform = {
x: 0,
y: 0,
scale: 1,
};
} }
return animateNumber({ return animateNumber({
from: [x, y, scale], from: [x, y, scale],
@ -273,11 +338,22 @@ const MediaViewerSlides: FC<OwnProps> = ({
onRelease: () => { onRelease: () => {
const absX = Math.abs(transformRef.current.x); const absX = Math.abs(transformRef.current.x);
const absY = Math.abs(transformRef.current.y); const absY = Math.abs(transformRef.current.y);
const { scale, x, y } = transformRef.current; const {
scale,
x,
y,
} = transformRef.current;
debounceSwipeDirection(() => {
swipeDirectionRef.current = undefined;
});
debounceActive(() => {
setIsActive(true);
});
// If scale is less than 1 we need to bounce back // If scale is less than 1 we need to bounce back
if (scale < 1) { if (scale < 1) {
lastTransform = INITIAL_TRANSFORM; lastTransform = { x: 0, y: 0, scale: 1 };
return animateNumber({ return animateNumber({
from: [x, y, scale], from: [x, y, scale],
to: [0, 0, 1], to: [0, 0, 1],
@ -292,7 +368,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
} }
if (scale > 1) { if (scale > 1) {
if (!content || !initialContentRect) { if (!content || !initialContentRect) {
lastTransform = { x, y, scale }; lastTransform = {
x,
y,
scale,
};
return undefined; return undefined;
} }
// Get current content boundaries // Get current content boundaries
@ -355,7 +435,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
}); });
return undefined; return undefined;
} }
lastTransform = { x, y, scale }; lastTransform = {
x,
y,
scale,
};
if (absY >= SWIPE_Y_THRESHOLD) return onClose(); if (absY >= SWIPE_Y_THRESHOLD) return onClose();
// Bounce back if vertical swipe is below threshold // Bounce back if vertical swipe is below threshold
if (absY > 0) { if (absY > 0) {
@ -387,8 +471,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
setActiveMessageId(mId); setActiveMessageId(mId);
debounceSetMessage(() => selectMessage(mId)); debounceSetMessage(() => selectMessage(mId));
} }
debounceSwipe(() => setIsSwiping(false));
debounceActive(() => setIsActive(true));
// Then we always return to the original position // Then we always return to the original position
cancelAnimation = animateNumber({ cancelAnimation = animateNumber({
from: transformRef.current.x, from: transformRef.current.x,
@ -411,7 +493,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
setTransform, setTransform,
getMessageId, getMessageId,
activeMessageId, activeMessageId,
setIsSwiping,
setIsActive, setIsActive,
]); ]);

View File

@ -5,16 +5,7 @@ import { IS_TOUCH_ENV } from '../../util/environment';
import Transition, { TransitionProps } from '../ui/Transition'; import Transition, { TransitionProps } from '../ui/Transition';
const SlideTransition: FC<TransitionProps> = ({ children, ...props }) => { const SlideTransition: FC<TransitionProps> = ({ children, ...props }) => {
if (IS_TOUCH_ENV) { if (IS_TOUCH_ENV) return children(true, true, 1);
// Return dummy container to keep existing DOM structure, needed to preserve ghost animation
return (
<div className="Transition">
<div className="Transition__slide--active">
{children(true, true, 1)}
</div>
</div>
);
}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return <Transition {...props}>{children}</Transition>; return <Transition {...props}>{children}</Transition>;
}; };

View File

@ -49,12 +49,6 @@
@media (max-height: 640px) { @media (max-height: 640px) {
max-height: calc(100vh - 10rem); max-height: calc(100vh - 10rem);
} }
@at-root .has-footer #{&} {
max-height: calc(100vh - 15rem);
@media (max-height: 640px) {
max-height: calc(100vh - 10rem);
}
}
} }
.play-button { .play-button {

View File

@ -129,10 +129,6 @@ const VideoPlayer: FC<OwnProps> = ({
const toggleControls = useCallback((e: React.MouseEvent<HTMLDivElement>) => { const toggleControls = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
setIsControlsVisible(!isControlsVisible); setIsControlsVisible(!isControlsVisible);
if (!isControlsVisible) {
videoRef.current!.pause();
setIsPlayed(false);
}
}, [isControlsVisible]); }, [isControlsVisible]);
useEffect(() => { useEffect(() => {
@ -210,7 +206,7 @@ const VideoPlayer: FC<OwnProps> = ({
isFullscreenSupported={Boolean(setFullscreen)} isFullscreenSupported={Boolean(setFullscreen)}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
fileSize={fileSize} fileSize={fileSize}
duration={videoRef.current ? videoRef.current.duration : 0} duration={videoRef.current ? videoRef.current.duration || 0 : 0}
isForceVisible={isControlsVisible} isForceVisible={isControlsVisible}
isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH} isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH}
onSeek={handleSeek} onSeek={handleSeek}

View File

@ -8,6 +8,9 @@
padding-top: .625rem; padding-top: .625rem;
font-size: .875rem; font-size: .875rem;
background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%);
transition: opacity .15s;
opacity: 0;
pointer-events: none;
#MediaViewer.zoomed & { #MediaViewer.zoomed & {
display: none; display: none;
@ -20,6 +23,11 @@
z-index: var(--z-media-viewer); z-index: var(--z-media-viewer);
} }
&.active {
opacity: 1;
pointer-events: all;
}
&.mobile { &.mobile {
.player-file-size { .player-file-size {
position: static; position: static;

View File

@ -1,6 +1,7 @@
import React, { import React, {
FC, useState, useEffect, useRef, useCallback, FC, useState, useEffect, useRef, useCallback,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { formatMediaDuration } from '../../util/dateFormat'; import { formatMediaDuration } from '../../util/dateFormat';
@ -117,12 +118,13 @@ const VideoPlayerControls: FC<IProps> = ({
}); });
}, [isVisible, handleStartSeek, handleSeek, handleStopSeek]); }, [isVisible, handleStartSeek, handleSeek, handleStopSeek]);
if (!isVisible && !isForceVisible) { const isActive = isVisible || isForceVisible;
return undefined;
}
return ( return (
<div className={`VideoPlayerControls ${isForceMobileVersion ? 'mobile' : ''}`} onClick={stopEvent}> <div
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isActive && 'active')}
onClick={stopEvent}
>
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)} {renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}
<Button <Button
ariaLabel={lang('AccActionPlay')} ariaLabel={lang('AccActionPlay')}

View File

@ -11,6 +11,7 @@ import {
} from '../../common/helpers/mediaDimensions'; } from '../../common/helpers/mediaDimensions';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
import stopEvent from '../../../util/stopEvent'; import stopEvent from '../../../util/stopEvent';
import { IS_TOUCH_ENV } from '../../../util/environment';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
@ -287,8 +288,8 @@ function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement
function getTopOffset(hasFooter: boolean) { function getTopOffset(hasFooter: boolean) {
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY); const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
let topOffsetRem = 4.125; let topOffsetRem = 4.125;
if (hasFooter) { if (hasFooter && !IS_TOUCH_ENV) {
topOffsetRem += mql.matches ? 0.875 : 3.375; topOffsetRem += mql.matches ? 0.875 : 2.125;
} }
return topOffsetRem * REM; return topOffsetRem * REM;

19
src/hooks/useTimeout.ts Normal file
View File

@ -0,0 +1,19 @@
import { useEffect, useLayoutEffect, useRef } from '../lib/teact/teact';
function useTimeout(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useLayoutEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (typeof delay !== 'number') {
return undefined;
}
const id = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(id);
}, [delay]);
}
export default useTimeout;

View File

@ -57,7 +57,7 @@ type TSwipeAxis =
| 'y' | 'y'
| undefined; | undefined;
const IOS_SCREEN_EDGE_THRESHOLD = 20; export const IOS_SCREEN_EDGE_THRESHOLD = 20;
const MOVED_THRESHOLD = 15; const MOVED_THRESHOLD = 15;
const SWIPE_THRESHOLD = 50; const SWIPE_THRESHOLD = 50;
@ -96,13 +96,6 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
if (e.type === 'mousedown') { if (e.type === 'mousedown') {
document.addEventListener('mousemove', onMove); document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onRelease); document.addEventListener('mouseup', onRelease);
if (options.onDoubleClick && Date.now() - lastClickTime < 300) {
options.onDoubleClick(e, {
centerX: e.pageX!,
centerY: e.pageY!,
});
}
lastClickTime = Date.now();
} else if (e.type === 'touchstart') { } else if (e.type === 'touchstart') {
// We need to always listen on `touchstart` target: // We need to always listen on `touchstart` target:
// https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
@ -150,8 +143,6 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
(captureEvent.target as HTMLElement).removeEventListener('touchend', onRelease); (captureEvent.target as HTMLElement).removeEventListener('touchend', onRelease);
(captureEvent.target as HTMLElement).removeEventListener('touchmove', onMove); (captureEvent.target as HTMLElement).removeEventListener('touchmove', onMove);
captureEvent = undefined;
if (IS_IOS && options.selectorToPreventScroll) { if (IS_IOS && options.selectorToPreventScroll) {
Array.from(document.querySelectorAll<HTMLElement>(options.selectorToPreventScroll)).forEach((scrollable) => { Array.from(document.querySelectorAll<HTMLElement>(options.selectorToPreventScroll)).forEach((scrollable) => {
scrollable.style.overflow = ''; scrollable.style.overflow = '';
@ -162,8 +153,16 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
if (options.onRelease) { if (options.onRelease) {
options.onRelease(e); options.onRelease(e);
} }
} else if (options.onClick && (!('button' in e) || e.button === 0)) { } else if (e.type === 'mouseup') {
options.onClick(e); if (options.onDoubleClick && Date.now() - lastClickTime < 300) {
options.onDoubleClick(e, {
centerX: captureEvent!.pageX!,
centerY: captureEvent!.pageY!,
});
} else if (options.onClick && (!('button' in e) || e.button === 0)) {
options.onClick(e);
}
lastClickTime = Date.now();
} }
} }
@ -172,6 +171,7 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
initialDistance = 0; initialDistance = 0;
initialSwipeAxis = undefined; initialSwipeAxis = undefined;
initialTouchCenter = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; initialTouchCenter = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
captureEvent = undefined;
} }
function onMove(e: MouseEvent | RealTouchEvent) { function onMove(e: MouseEvent | RealTouchEvent) {

View File

@ -30,7 +30,7 @@ module.exports = (env = {}, argv = {}) => {
port: 1234, port: 1234,
host: '0.0.0.0', host: '0.0.0.0',
disableHostCheck: true, disableHostCheck: true,
stats: 'minimal', stats: 'minimal'
}, },
output: { output: {
filename: '[name].[contenthash].js', filename: '[name].[contenthash].js',