Media Viewer: Various improvements and fixes (#1657)
This commit is contained in:
parent
a1885d5e29
commit
92f15c200b
@ -3,7 +3,7 @@ import {
|
||||
} from '../../../api/types';
|
||||
|
||||
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 { getPhotoInlineDimensions, getVideoDimensions } from '../../../modules/helpers';
|
||||
|
||||
@ -110,10 +110,9 @@ export function getMediaViewerAvailableDimensions(withFooter: boolean, isVideo:
|
||||
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
|
||||
const { width: windowWidth, height: windowHeight } = windowSize.get();
|
||||
let occupiedHeightRem = isVideo && mql.matches ? 10 : 8.25;
|
||||
if (withFooter) {
|
||||
occupiedHeightRem = mql.matches ? 10 : 15;
|
||||
if (withFooter && !IS_TOUCH_ENV) {
|
||||
occupiedHeightRem = mql.matches ? 10 : 12.5;
|
||||
}
|
||||
|
||||
return {
|
||||
width: windowWidth,
|
||||
height: windowHeight - occupiedHeightRem * REM,
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
body.ghost-animating & {
|
||||
> .pan-wrapper, > .Transition, > button {
|
||||
> .pan-wrapper, > button, .MediaViewerContent img, .MediaViewerContent .VideoPlayer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,12 +121,13 @@ const MediaViewer: FC<StateProps> = ({
|
||||
const isAvatar = Boolean(avatarOwner);
|
||||
|
||||
/* Navigation */
|
||||
const isSingleSlide = Boolean(webPagePhoto || webPageVideo);
|
||||
const singleMessageId = webPagePhoto || webPageVideo ? messageId : undefined;
|
||||
|
||||
const messageIds = useMemo(() => {
|
||||
return isSingleSlide && messageId
|
||||
? [messageId]
|
||||
return singleMessageId
|
||||
? [singleMessageId]
|
||||
: getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
|
||||
}, [isSingleSlide, messageId, chatMessages, collectionIds, isFromSharedMedia]);
|
||||
}, [singleMessageId, chatMessages, collectionIds, isFromSharedMedia]);
|
||||
|
||||
const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1;
|
||||
const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1;
|
||||
@ -513,6 +514,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
hasFooter={hasFooter}
|
||||
isZoomed={isZoomed}
|
||||
isActive={isActive}
|
||||
isVideo={isVideo}
|
||||
animationLevel={animationLevel}
|
||||
onClose={close}
|
||||
selectMessage={selectMessage}
|
||||
|
||||
@ -176,6 +176,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
if (!message) return undefined;
|
||||
const textParts = renderMessageText(message);
|
||||
const hasFooter = Boolean(textParts);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`}
|
||||
@ -192,23 +193,23 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
url={localBlobUrl || fullMediaBlobUrl}
|
||||
isGif={isGif}
|
||||
posterData={bestImageData}
|
||||
posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)}
|
||||
posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, false)}
|
||||
loadProgress={loadProgress}
|
||||
fileSize={videoSize!}
|
||||
isMediaViewerOpen={isOpen}
|
||||
isMediaViewerOpen={isOpen && isActive}
|
||||
noPlay={!isActive}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : renderVideoPreview(
|
||||
bestImageData,
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter, false),
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
))}
|
||||
{textParts && (
|
||||
<MediaViewerFooter
|
||||
text={textParts}
|
||||
onClick={onFooterClick}
|
||||
isHidden={isFooterHidden && (!isVideo || isGif)}
|
||||
isHidden={isFooterHidden}
|
||||
isForVideo={isVideo && !isGif}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -14,20 +14,24 @@
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding-bottom: 4.5rem;
|
||||
background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%);
|
||||
|
||||
&.is-for-video {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
padding-bottom: 5rem;
|
||||
|
||||
.video-controls-visible & {
|
||||
.video-controls-visible &:not(.is-hidden) {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.ghost-animating & {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.media-viewer-footer-content {
|
||||
position: relative;
|
||||
max-width: var(--messages-container-width);
|
||||
|
||||
@ -8,13 +8,14 @@ import useDebounce from '../../hooks/useDebounce';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import { animateNumber, timingFunctions } from '../../util/animation';
|
||||
import arePropsShallowEqual from '../../util/arePropsShallowEqual';
|
||||
import { captureEvents } from '../../util/captureEvents';
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD, RealTouchEvent } from '../../util/captureEvents';
|
||||
import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
|
||||
import MediaViewerContent from './MediaViewerContent';
|
||||
|
||||
import './MediaViewerSlides.scss';
|
||||
import useTimeout from '../../hooks/useTimeout';
|
||||
|
||||
type OwnProps = {
|
||||
messageId?: number;
|
||||
@ -47,6 +48,7 @@ const DEBOUNCE_ACTIVE = 800;
|
||||
const MAX_ZOOM = 4;
|
||||
const MIN_ZOOM = 0.6;
|
||||
const DOUBLE_TAP_ZOOM = 3;
|
||||
const CLICK_X_THRESHOLD = 120;
|
||||
let cancelAnimation: Function | undefined;
|
||||
|
||||
type Transform = {
|
||||
@ -55,11 +57,10 @@ type Transform = {
|
||||
scale: number;
|
||||
};
|
||||
|
||||
const INITIAL_TRANSFORM = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
};
|
||||
enum SwipeDirection {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
const MediaViewerSlides: FC<OwnProps> = ({
|
||||
messageId,
|
||||
@ -77,12 +78,12 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const activeSlideRef = useRef<HTMLDivElement>(null);
|
||||
const transformRef = useRef<Transform>(INITIAL_TRANSFORM);
|
||||
const isSwipingRef = useRef(false);
|
||||
const transformRef = useRef<Transform>({ x: 0, y: 0, scale: 1 });
|
||||
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
|
||||
const isActiveRef = useRef(true);
|
||||
const [activeMessageId, setActiveMessageId] = useState<number | undefined>(messageId);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const [isFooterHidden, setIsFooterHidden] = useState<boolean>(false);
|
||||
const [isFooterHidden, setIsFooterHidden] = useState<boolean>(true);
|
||||
|
||||
const {
|
||||
isZoomed,
|
||||
@ -94,26 +95,24 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
forceUpdate();
|
||||
}, [forceUpdate]);
|
||||
|
||||
const setIsSwiping = useCallback((value: boolean) => {
|
||||
isSwipingRef.current = value;
|
||||
forceUpdate();
|
||||
}, [forceUpdate]);
|
||||
|
||||
const setIsActive = useCallback((value: boolean) => {
|
||||
isActiveRef.current = value;
|
||||
forceUpdate();
|
||||
}, [forceUpdate]);
|
||||
|
||||
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 handleToggleFooterVisibility = useCallback(() => {
|
||||
if (IS_TOUCH_ENV && (isPhoto || isGif) && hasFooter) {
|
||||
setIsFooterHidden(!isFooterHidden);
|
||||
}
|
||||
const handleToggleFooterVisibility = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (!IS_TOUCH_ENV || !hasFooter || (!isPhoto && !isGif)) return;
|
||||
if (e.clientX < CLICK_X_THRESHOLD) return;
|
||||
if (e.clientX > window.innerWidth - CLICK_X_THRESHOLD) return;
|
||||
setIsFooterHidden(!isFooterHidden);
|
||||
}, [hasFooter, isFooterHidden, isGif, isPhoto]);
|
||||
|
||||
useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_TOUCH_ENV || !containerRef.current || isZoomed || !activeMessageId) {
|
||||
return undefined;
|
||||
@ -123,7 +122,10 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
const lastZoomCenter = { x: 0, y: 0 };
|
||||
const lastZoomCenter = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
const panDelta = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -134,18 +136,54 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const setLastGestureTime = debounce(() => {
|
||||
lastGestureTime = Date.now();
|
||||
}, 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, {
|
||||
isNotPassive: true,
|
||||
excludedClosestSelector: '.VideoPlayerControls, .MediaViewerFooter',
|
||||
onCapture: (event) => {
|
||||
// Prevent safari back swipe on mobile
|
||||
if (event.type === 'touchstart'
|
||||
&& 'pageX' in event
|
||||
&& !(event.pageX > 10 && event.pageX < window.innerWidth - 10)) {
|
||||
event.preventDefault();
|
||||
// Avoid conflicts with swipe-to-back gestures
|
||||
if (event.type === 'touchstart' && IS_IOS) {
|
||||
const x = (event as RealTouchEvent).touches[0].pageX;
|
||||
if (x <= IOS_SCREEN_EDGE_THRESHOLD || x >= window.innerWidth - IOS_SCREEN_EDGE_THRESHOLD) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
lastGestureTime = Date.now();
|
||||
if (arePropsShallowEqual(transformRef.current, INITIAL_TRANSFORM)) {
|
||||
if (arePropsShallowEqual(transformRef.current, { x: 0, y: 0, scale: 1 })) {
|
||||
if (!activeSlideRef.current) return;
|
||||
content = activeSlideRef.current.querySelector('img, video');
|
||||
if (!content) return;
|
||||
@ -167,7 +205,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
lastDragOffset.y = dragOffsetY;
|
||||
const absOffsetX = Math.abs(dragOffsetX);
|
||||
const absOffsetY = Math.abs(dragOffsetY);
|
||||
const { scale, x, y } = transformRef.current;
|
||||
const {
|
||||
scale,
|
||||
x,
|
||||
y,
|
||||
} = transformRef.current;
|
||||
const h = 10;
|
||||
|
||||
// If user is inactive but is still touching the screen
|
||||
@ -185,21 +227,25 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If user is swiping horizontally or horizontal shift is dominant
|
||||
// we change only horizontal position
|
||||
if (isSwipingRef.current || Math.abs(x) > h || (absOffsetX > h && absOffsetY < h)) {
|
||||
isSwipingRef.current = true;
|
||||
isActiveRef.current = false;
|
||||
setTransform({
|
||||
x: dragOffsetX,
|
||||
y: 0,
|
||||
scale,
|
||||
});
|
||||
return;
|
||||
if (swipeDirectionRef.current !== SwipeDirection.Vertical) {
|
||||
// If user is swiping horizontally or horizontal shift is dominant
|
||||
// we change only horizontal position
|
||||
if (swipeDirectionRef.current === SwipeDirection.Horizontal
|
||||
|| Math.abs(x) > h || (absOffsetX > h && absOffsetY < h)) {
|
||||
swipeDirectionRef.current = SwipeDirection.Horizontal;
|
||||
isActiveRef.current = false;
|
||||
setTransform({
|
||||
x: dragOffsetX,
|
||||
y: 0,
|
||||
scale,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isSwipingRef.current) return;
|
||||
// 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({
|
||||
x: 0,
|
||||
y: dragOffsetY,
|
||||
@ -240,14 +286,29 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
scale,
|
||||
});
|
||||
},
|
||||
onClick(e) {
|
||||
if (changeSlide(e as MouseEvent)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
onDoubleClick(e, {
|
||||
centerX,
|
||||
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
|
||||
const scaleOffsetX = (centerX - DOUBLE_TAP_ZOOM * centerX);
|
||||
const scaleOffsetY = (centerY - DOUBLE_TAP_ZOOM * centerY);
|
||||
const { scale, x, y } = transformRef.current;
|
||||
const {
|
||||
scale,
|
||||
x,
|
||||
y,
|
||||
} = transformRef.current;
|
||||
if (scale === 1) {
|
||||
if (x !== 0 || y !== 0) return undefined;
|
||||
lastTransform = {
|
||||
@ -256,7 +317,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
scale: DOUBLE_TAP_ZOOM,
|
||||
};
|
||||
} else {
|
||||
lastTransform = { x: 0, y: 0, scale: 1 };
|
||||
lastTransform = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
};
|
||||
}
|
||||
return animateNumber({
|
||||
from: [x, y, scale],
|
||||
@ -273,11 +338,22 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
onRelease: () => {
|
||||
const absX = Math.abs(transformRef.current.x);
|
||||
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 < 1) {
|
||||
lastTransform = INITIAL_TRANSFORM;
|
||||
lastTransform = { x: 0, y: 0, scale: 1 };
|
||||
return animateNumber({
|
||||
from: [x, y, scale],
|
||||
to: [0, 0, 1],
|
||||
@ -292,7 +368,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}
|
||||
if (scale > 1) {
|
||||
if (!content || !initialContentRect) {
|
||||
lastTransform = { x, y, scale };
|
||||
lastTransform = {
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
};
|
||||
return undefined;
|
||||
}
|
||||
// Get current content boundaries
|
||||
@ -355,7 +435,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
lastTransform = { x, y, scale };
|
||||
lastTransform = {
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
};
|
||||
if (absY >= SWIPE_Y_THRESHOLD) return onClose();
|
||||
// Bounce back if vertical swipe is below threshold
|
||||
if (absY > 0) {
|
||||
@ -387,8 +471,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
setActiveMessageId(mId);
|
||||
debounceSetMessage(() => selectMessage(mId));
|
||||
}
|
||||
debounceSwipe(() => setIsSwiping(false));
|
||||
debounceActive(() => setIsActive(true));
|
||||
// Then we always return to the original position
|
||||
cancelAnimation = animateNumber({
|
||||
from: transformRef.current.x,
|
||||
@ -411,7 +493,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
setTransform,
|
||||
getMessageId,
|
||||
activeMessageId,
|
||||
setIsSwiping,
|
||||
setIsActive,
|
||||
]);
|
||||
|
||||
|
||||
@ -5,16 +5,7 @@ import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import Transition, { TransitionProps } from '../ui/Transition';
|
||||
|
||||
const SlideTransition: FC<TransitionProps> = ({ children, ...props }) => {
|
||||
if (IS_TOUCH_ENV) {
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
if (IS_TOUCH_ENV) return children(true, true, 1);
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Transition {...props}>{children}</Transition>;
|
||||
};
|
||||
|
||||
@ -49,12 +49,6 @@
|
||||
@media (max-height: 640px) {
|
||||
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 {
|
||||
|
||||
@ -129,10 +129,6 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
const toggleControls = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsControlsVisible(!isControlsVisible);
|
||||
if (!isControlsVisible) {
|
||||
videoRef.current!.pause();
|
||||
setIsPlayed(false);
|
||||
}
|
||||
}, [isControlsVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -210,7 +206,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
isFullscreenSupported={Boolean(setFullscreen)}
|
||||
isFullscreen={isFullscreen}
|
||||
fileSize={fileSize}
|
||||
duration={videoRef.current ? videoRef.current.duration : 0}
|
||||
duration={videoRef.current ? videoRef.current.duration || 0 : 0}
|
||||
isForceVisible={isControlsVisible}
|
||||
isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH}
|
||||
onSeek={handleSeek}
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
padding-top: .625rem;
|
||||
font-size: .875rem;
|
||||
background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%);
|
||||
transition: opacity .15s;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
#MediaViewer.zoomed & {
|
||||
display: none;
|
||||
@ -20,6 +23,11 @@
|
||||
z-index: var(--z-media-viewer);
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.player-file-size {
|
||||
position: static;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
FC, useState, useEffect, useRef, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { formatMediaDuration } from '../../util/dateFormat';
|
||||
@ -117,12 +118,13 @@ const VideoPlayerControls: FC<IProps> = ({
|
||||
});
|
||||
}, [isVisible, handleStartSeek, handleSeek, handleStopSeek]);
|
||||
|
||||
if (!isVisible && !isForceVisible) {
|
||||
return undefined;
|
||||
}
|
||||
const isActive = isVisible || isForceVisible;
|
||||
|
||||
return (
|
||||
<div className={`VideoPlayerControls ${isForceMobileVersion ? 'mobile' : ''}`} onClick={stopEvent}>
|
||||
<div
|
||||
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isActive && 'active')}
|
||||
onClick={stopEvent}
|
||||
>
|
||||
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}
|
||||
<Button
|
||||
ariaLabel={lang('AccActionPlay')}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from '../../common/helpers/mediaDimensions';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
@ -287,8 +288,8 @@ function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement
|
||||
function getTopOffset(hasFooter: boolean) {
|
||||
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
|
||||
let topOffsetRem = 4.125;
|
||||
if (hasFooter) {
|
||||
topOffsetRem += mql.matches ? 0.875 : 3.375;
|
||||
if (hasFooter && !IS_TOUCH_ENV) {
|
||||
topOffsetRem += mql.matches ? 0.875 : 2.125;
|
||||
}
|
||||
|
||||
return topOffsetRem * REM;
|
||||
|
||||
19
src/hooks/useTimeout.ts
Normal file
19
src/hooks/useTimeout.ts
Normal 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;
|
||||
@ -57,7 +57,7 @@ type TSwipeAxis =
|
||||
| 'y'
|
||||
| undefined;
|
||||
|
||||
const IOS_SCREEN_EDGE_THRESHOLD = 20;
|
||||
export const IOS_SCREEN_EDGE_THRESHOLD = 20;
|
||||
const MOVED_THRESHOLD = 15;
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
|
||||
@ -96,13 +96,6 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
|
||||
if (e.type === 'mousedown') {
|
||||
document.addEventListener('mousemove', onMove);
|
||||
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') {
|
||||
// We need to always listen on `touchstart` target:
|
||||
// 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('touchmove', onMove);
|
||||
|
||||
captureEvent = undefined;
|
||||
|
||||
if (IS_IOS && options.selectorToPreventScroll) {
|
||||
Array.from(document.querySelectorAll<HTMLElement>(options.selectorToPreventScroll)).forEach((scrollable) => {
|
||||
scrollable.style.overflow = '';
|
||||
@ -162,8 +153,16 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
|
||||
if (options.onRelease) {
|
||||
options.onRelease(e);
|
||||
}
|
||||
} else if (options.onClick && (!('button' in e) || e.button === 0)) {
|
||||
options.onClick(e);
|
||||
} else if (e.type === 'mouseup') {
|
||||
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;
|
||||
initialSwipeAxis = undefined;
|
||||
initialTouchCenter = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
captureEvent = undefined;
|
||||
}
|
||||
|
||||
function onMove(e: MouseEvent | RealTouchEvent) {
|
||||
|
||||
@ -30,7 +30,7 @@ module.exports = (env = {}, argv = {}) => {
|
||||
port: 1234,
|
||||
host: '0.0.0.0',
|
||||
disableHostCheck: true,
|
||||
stats: 'minimal',
|
||||
stats: 'minimal'
|
||||
},
|
||||
output: {
|
||||
filename: '[name].[contenthash].js',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user