Media Viewer: Various improvements and fixes (#1657)
This commit is contained in:
parent
a1885d5e29
commit
92f15c200b
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -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>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -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
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'
|
| '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) {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user