[Perf] Media Viewer: Use signals (#2838)
This commit is contained in:
parent
c467da8680
commit
0079e8b7ab
@ -1,6 +1,6 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiChat, ApiMessage, ApiUser } from '../../api/types';
|
||||
@ -38,6 +38,7 @@ import useLang from '../../hooks/useLang';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
|
||||
import ReportModal from '../common/ReportModal';
|
||||
import Button from '../ui/Button';
|
||||
@ -103,7 +104,6 @@ const MediaViewer: FC<StateProps> = ({
|
||||
|
||||
/* Controls */
|
||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||
const [zoomLevelChange, setZoomLevelChange] = useState<number>(1);
|
||||
|
||||
const {
|
||||
webPagePhoto,
|
||||
@ -220,20 +220,23 @@ const MediaViewer: FC<StateProps> = ({
|
||||
|
||||
const handleClose = useCallback(() => closeMediaViewer(), [closeMediaViewer]);
|
||||
|
||||
const mediaIdRef = useStateRef(mediaId);
|
||||
const handleFooterClick = useCallback(() => {
|
||||
handleClose();
|
||||
|
||||
if (!chatId || !mediaId) return;
|
||||
const currentMediaId = mediaIdRef.current;
|
||||
|
||||
if (!chatId || !currentMediaId) return;
|
||||
|
||||
if (isMobile) {
|
||||
setTimeout(() => {
|
||||
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, threadId, messageId: mediaId });
|
||||
focusMessage({ chatId, threadId, messageId: currentMediaId });
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
focusMessage({ chatId, threadId, messageId: mediaId });
|
||||
focusMessage({ chatId, threadId, messageId: currentMediaId });
|
||||
}
|
||||
}, [handleClose, isMobile, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
|
||||
}, [handleClose, mediaIdRef, chatId, isMobile, threadId]);
|
||||
|
||||
const handleForward = useCallback(() => {
|
||||
openForwardMenu({
|
||||
@ -342,8 +345,6 @@ const MediaViewer: FC<StateProps> = ({
|
||||
onReport={openReportModal}
|
||||
onCloseMediaViewer={handleClose}
|
||||
onForward={handleForward}
|
||||
zoomLevelChange={zoomLevelChange}
|
||||
setZoomLevelChange={setZoomLevelChange}
|
||||
/>
|
||||
<ReportModal
|
||||
isOpen={isReportModalOpen}
|
||||
@ -364,7 +365,6 @@ const MediaViewer: FC<StateProps> = ({
|
||||
origin={origin}
|
||||
isOpen={isOpen}
|
||||
hasFooter={hasFooter}
|
||||
zoomLevelChange={zoomLevelChange}
|
||||
isVideo={isVideo}
|
||||
animationLevel={animationLevel}
|
||||
onClose={handleClose}
|
||||
|
||||
@ -25,6 +25,7 @@ import useLang from '../../hooks/useLang';
|
||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useZoomChange from './hooks/useZoomChangeSignal';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
@ -48,7 +49,6 @@ type StateProps = {
|
||||
type OwnProps = {
|
||||
mediaData?: string;
|
||||
isVideo: boolean;
|
||||
zoomLevelChange: number;
|
||||
message?: ApiMessage;
|
||||
canUpdateMedia?: boolean;
|
||||
isSingleMedia?: boolean;
|
||||
@ -61,7 +61,6 @@ type OwnProps = {
|
||||
onBeforeDelete: NoneToVoidFunction;
|
||||
onCloseMediaViewer: NoneToVoidFunction;
|
||||
onForward: NoneToVoidFunction;
|
||||
setZoomLevelChange: (change: number) => void;
|
||||
};
|
||||
|
||||
const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
@ -75,7 +74,6 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
isDownloading,
|
||||
isProtected,
|
||||
canReport,
|
||||
zoomLevelChange,
|
||||
canDelete,
|
||||
canUpdate,
|
||||
messageListType,
|
||||
@ -84,9 +82,9 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
onCloseMediaViewer,
|
||||
onBeforeDelete,
|
||||
onForward,
|
||||
setZoomLevelChange,
|
||||
}) => {
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
|
||||
const [getZoomChange, setZoomChange] = useZoomChange();
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const {
|
||||
@ -111,14 +109,16 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
}, [cancelMessageMediaDownload, downloadMessageMedia, isDownloading, message]);
|
||||
|
||||
const handleZoomOut = useCallback(() => {
|
||||
const change = zoomLevelChange < 0 ? zoomLevelChange : 0;
|
||||
setZoomLevelChange(change - 1);
|
||||
}, [setZoomLevelChange, zoomLevelChange]);
|
||||
const zoomChange = getZoomChange();
|
||||
const change = zoomChange < 0 ? zoomChange : 0;
|
||||
setZoomChange(change - 1);
|
||||
}, [getZoomChange, setZoomChange]);
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
const change = zoomLevelChange > 0 ? zoomLevelChange : 0;
|
||||
setZoomLevelChange(change + 1);
|
||||
}, [setZoomLevelChange, zoomLevelChange]);
|
||||
const zoomChange = getZoomChange();
|
||||
const change = zoomChange > 0 ? zoomChange : 0;
|
||||
setZoomChange(change + 1);
|
||||
}, [getZoomChange, setZoomChange]);
|
||||
|
||||
const handleUpdate = useCallback(() => {
|
||||
if (!avatarPhoto || !avatarOwnerId) return;
|
||||
|
||||
@ -19,6 +19,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
|
||||
import Spinner from '../ui/Spinner';
|
||||
import MediaViewerFooter from './MediaViewerFooter';
|
||||
@ -36,8 +37,6 @@ type OwnProps = {
|
||||
animationLevel: AnimationLevel;
|
||||
onClose: () => void;
|
||||
onFooterClick: () => void;
|
||||
setControlsVisible?: (isVisible: boolean) => void;
|
||||
areControlsVisible: boolean;
|
||||
isMoving?: boolean;
|
||||
};
|
||||
|
||||
@ -68,7 +67,6 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
message,
|
||||
origin,
|
||||
animationLevel,
|
||||
areControlsVisible,
|
||||
isProtected,
|
||||
volume,
|
||||
playbackRate,
|
||||
@ -76,7 +74,6 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
isHidden,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
setControlsVisible,
|
||||
isMoving,
|
||||
} = props;
|
||||
|
||||
@ -99,13 +96,11 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
message, avatarOwner, mediaId, origin, delay: isGhostAnimation && ANIMATION_DURATION,
|
||||
});
|
||||
|
||||
const [, toggleControls] = useControlsSignal();
|
||||
|
||||
const isOpen = Boolean(avatarOwner || mediaId);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const toggleControls = useCallback((isVisible) => {
|
||||
setControlsVisible?.(isVisible);
|
||||
}, [setControlsVisible]);
|
||||
|
||||
const toggleControlsOnMove = useCallback(() => {
|
||||
toggleControls(true);
|
||||
}, [toggleControls]);
|
||||
@ -134,8 +129,6 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
loadProgress={loadProgress}
|
||||
fileSize={videoSize!}
|
||||
isMediaViewerOpen={isOpen && isActive}
|
||||
areControlsVisible={areControlsVisible}
|
||||
toggleControls={toggleControls}
|
||||
isProtected={isProtected}
|
||||
noPlay={!isActive}
|
||||
onClose={onClose}
|
||||
@ -184,9 +177,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
posterSize={posterSize}
|
||||
loadProgress={loadProgress}
|
||||
fileSize={videoSize!}
|
||||
areControlsVisible={areControlsVisible}
|
||||
isMediaViewerOpen={isOpen && isActive}
|
||||
toggleControls={toggleControls}
|
||||
noPlay={!isActive}
|
||||
onClose={onClose}
|
||||
isMuted={isMuted}
|
||||
@ -204,7 +195,6 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
onClick={onFooterClick}
|
||||
isProtected={isProtected}
|
||||
isForceMobileVersion={isForceMobileVersion}
|
||||
isHidden={IS_TOUCH_ENV ? !areControlsVisible : false}
|
||||
isForVideo={isVideo && !isGif}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -3,10 +3,13 @@ import React, { useEffect, useState } from '../../lib/teact/teact';
|
||||
|
||||
import type { TextPart } from '../../types';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { REM } from '../common/helpers/mediaDimensions';
|
||||
import { throttle } from '../../util/schedulers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
|
||||
import './MediaViewerFooter.scss';
|
||||
|
||||
@ -15,17 +18,18 @@ const RESIZE_THROTTLE_MS = 500;
|
||||
type OwnProps = {
|
||||
text: TextPart | TextPart[];
|
||||
onClick: () => void;
|
||||
isHidden?: boolean;
|
||||
isForVideo: boolean;
|
||||
isForceMobileVersion?: boolean;
|
||||
isProtected?: boolean;
|
||||
};
|
||||
|
||||
const MediaViewerFooter: FC<OwnProps> = ({
|
||||
text = '', isHidden, isForVideo, onClick, isProtected, isForceMobileVersion,
|
||||
text = '', isForVideo, onClick, isProtected, isForceMobileVersion,
|
||||
}) => {
|
||||
const [isMultiline, setIsMultiline] = useState(false);
|
||||
const { isMobile } = useAppLayout();
|
||||
const [getIsVisible] = useControlsSignal();
|
||||
const isHidden = useDerivedState(() => (IS_TOUCH_ENV ? !getIsVisible() : false), [getIsVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const footerContent = document.querySelector('.MediaViewerFooter .media-text') as HTMLDivElement | null;
|
||||
|
||||
@ -14,17 +14,20 @@ import { clamp, isBetween, round } from '../../util/math';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
|
||||
import useDebouncedCallback from '../../hooks/useDebouncedCallback';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useTimeout from '../../hooks/useTimeout';
|
||||
import useWindowSize from '../../hooks/useWindowSize';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useSignal from '../../hooks/useSignal';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
import { useFullscreenStatus } from '../../hooks/useFullscreen';
|
||||
import useZoomChange from './hooks/useZoomChangeSignal';
|
||||
|
||||
import MediaViewerContent from './MediaViewerContent';
|
||||
|
||||
import './MediaViewerSlides.scss';
|
||||
import { useSignalRef } from '../../hooks/useSignalRef';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
|
||||
const { easeOutCubic, easeOutQuart } = timingFunctions;
|
||||
|
||||
@ -45,14 +48,13 @@ type OwnProps = {
|
||||
isHidden?: boolean;
|
||||
hasFooter?: boolean;
|
||||
onFooterClick: () => void;
|
||||
zoomLevelChange: number;
|
||||
};
|
||||
|
||||
const SWIPE_X_THRESHOLD = 50;
|
||||
const SWIPE_Y_THRESHOLD = 50;
|
||||
const SLIDES_GAP = IS_TOUCH_ENV ? 40 : 0;
|
||||
const ANIMATION_DURATION = 350;
|
||||
const DEBOUNCE_MESSAGE = 350;
|
||||
const DEBOUNCE_SELECT_MEDIA = 350;
|
||||
const DEBOUNCE_SWIPE = 500;
|
||||
const DEBOUNCE_ACTIVE = 800;
|
||||
const DOUBLE_TAP_ZOOM = 3;
|
||||
@ -83,7 +85,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
isPhoto,
|
||||
isOpen,
|
||||
hasFooter,
|
||||
zoomLevelChange,
|
||||
animationLevel,
|
||||
isHidden,
|
||||
...rest
|
||||
@ -92,19 +93,26 @@ 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>({ x: 0, y: 0, scale: 1 });
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const leftSlideRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const rightSlideRef = useRef<HTMLDivElement>(null);
|
||||
const lastTransformRef = useRef<Transform>({ x: 0, y: 0, scale: 1 });
|
||||
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
|
||||
const isActiveRef = useRef(true);
|
||||
const isReleasedRef = useRef(false);
|
||||
const [activeMediaId, setActiveMediaId] = useState<number | undefined>(mediaId);
|
||||
const prevZoomLevelChange = usePrevious(zoomLevelChange);
|
||||
const hasZoomChanged = prevZoomLevelChange !== undefined && prevZoomLevelChange !== zoomLevelChange;
|
||||
const forceUpdate = useForceUpdate();
|
||||
const [areControlsVisible, setControlsVisible] = useState(false);
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [getZoomChange] = useZoomChange();
|
||||
const prevZoomChangeRef = useRef(getZoomChange());
|
||||
const isFullscreen = useFullscreenStatus();
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
const [getTransform, setTransform] = useSignal<Transform>({ x: 0, y: 0, scale: 1 });
|
||||
const transformRef = useSignalRef(getTransform);
|
||||
const [getActiveMediaId, setActiveMediaId] = useSignal<number | undefined>(mediaId);
|
||||
const activeMediaIdRef = useSignalRef(getActiveMediaId);
|
||||
const isScaled = useDerivedState(() => getTransform().scale !== 1, [getTransform]);
|
||||
const activeMediaId = useDerivedState(getActiveMediaId);
|
||||
const { height: windowHeight, width: windowWidth, isResizing } = useWindowSize();
|
||||
const [getControlsVisible, setControlsVisible, lockControls] = useControlsSignal();
|
||||
const { onClose } = rest;
|
||||
|
||||
const lang = useLang();
|
||||
@ -115,19 +123,12 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
shouldBeReplaced: true,
|
||||
});
|
||||
|
||||
const setTransform = useCallback((value: Transform) => {
|
||||
transformRef.current = value;
|
||||
forceUpdate();
|
||||
}, [forceUpdate]);
|
||||
|
||||
const selectMediaDebounced = useDebouncedCallback(selectMedia, [selectMedia], DEBOUNCE_MESSAGE, true);
|
||||
const selectMediaDebounced = useDebouncedCallback(selectMedia, [selectMedia], DEBOUNCE_SELECT_MEDIA, true);
|
||||
const clearSwipeDirectionDebounced = useDebouncedCallback(() => {
|
||||
swipeDirectionRef.current = undefined;
|
||||
}, [], DEBOUNCE_SWIPE, true);
|
||||
const setIsActiveDebounced = useDebouncedCallback((value: boolean) => {
|
||||
isActiveRef.current = value;
|
||||
forceUpdate();
|
||||
}, [forceUpdate], DEBOUNCE_ACTIVE, true);
|
||||
|
||||
const setIsActiveDebounced = useDebouncedCallback((value) => setIsActive(value), [], DEBOUNCE_ACTIVE, true);
|
||||
|
||||
const shouldCloseOnVideo = isGif && !IS_IOS;
|
||||
const clickXThreshold = IS_TOUCH_ENV ? 40 : windowWidth / 10;
|
||||
@ -137,17 +138,27 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const isFooter = windowHeight - e.pageY < CLICK_Y_THRESHOLD;
|
||||
if (!isFooter && e.pageX < clickXThreshold) return;
|
||||
if (!isFooter && e.pageX > windowWidth - clickXThreshold) return;
|
||||
setControlsVisible(!areControlsVisible);
|
||||
}, [clickXThreshold, areControlsVisible, windowHeight, windowWidth]);
|
||||
setControlsVisible(!getControlsVisible());
|
||||
}, [clickXThreshold, getControlsVisible, setControlsVisible, windowHeight, windowWidth]);
|
||||
|
||||
useTimeout(() => setControlsVisible(true), ANIMATION_DURATION + 100);
|
||||
useTimeout(() => setControlsVisible(true), ANIMATION_DURATION);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMediaId(mediaId);
|
||||
}, [mediaId]);
|
||||
const { x, y, scale } = getTransform();
|
||||
lockControls(scale !== 1);
|
||||
if (leftSlideRef.current) {
|
||||
leftSlideRef.current.style.transform = getTransformStyle(-windowWidth + x - SLIDES_GAP);
|
||||
}
|
||||
if (activeSlideRef.current) {
|
||||
activeSlideRef.current.style.transform = getTransformStyle(x, y, scale);
|
||||
}
|
||||
if (rightSlideRef.current) {
|
||||
rightSlideRef.current.style.transform = getTransformStyle(windowWidth + x + SLIDES_GAP);
|
||||
}
|
||||
}, [getTransform, lockControls, windowWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || activeMediaId === undefined || isHidden || isFullscreen) {
|
||||
if (!containerRef.current || activeMediaIdRef.current === undefined || isHidden || isFullscreen) {
|
||||
return undefined;
|
||||
}
|
||||
let lastTransform = lastTransformRef.current;
|
||||
@ -171,11 +182,12 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}, 500, false, true);
|
||||
|
||||
const changeSlide = (direction: number) => {
|
||||
const mId = getMediaId(activeMediaId, direction);
|
||||
const mId = getMediaId(activeMediaIdRef.current, direction);
|
||||
if (mId !== undefined) {
|
||||
const offset = (windowWidth + SLIDES_GAP) * direction;
|
||||
transformRef.current.x += offset;
|
||||
isActiveRef.current = false;
|
||||
const transform = transformRef.current;
|
||||
const x = transform.x + offset;
|
||||
setIsActive(false);
|
||||
setActiveMediaId(mId);
|
||||
selectMediaDebounced(mId);
|
||||
setIsActiveDebounced(true);
|
||||
@ -185,7 +197,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
return true;
|
||||
}
|
||||
cancelAnimation = animateNumber({
|
||||
from: transformRef.current.x,
|
||||
from: x,
|
||||
to: 0,
|
||||
duration: ANIMATION_DURATION,
|
||||
timing: easeOutCubic,
|
||||
@ -201,7 +213,8 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
};
|
||||
|
||||
const changeSlideOnClick = (e: MouseEvent): [boolean, boolean] => {
|
||||
if (transformRef.current.scale !== 1) return [false, false];
|
||||
const { scale } = transformRef.current;
|
||||
if (scale !== 1) return [false, false];
|
||||
if ((e.target as HTMLElement).closest('div.VideoPlayerControls')) {
|
||||
return [false, false];
|
||||
}
|
||||
@ -220,7 +233,8 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (transformRef.current.scale !== 1) return;
|
||||
const { scale } = transformRef.current;
|
||||
if (scale !== 1) return;
|
||||
switch (e.key) {
|
||||
case 'Left': // IE/Edge specific value
|
||||
case 'ArrowLeft':
|
||||
@ -271,13 +285,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
if (e.type === 'mouseup') {
|
||||
setIsMouseDown(false);
|
||||
}
|
||||
const absX = Math.abs(transformRef.current.x);
|
||||
const absY = Math.abs(transformRef.current.y);
|
||||
const {
|
||||
scale,
|
||||
x,
|
||||
y,
|
||||
} = transformRef.current;
|
||||
const transform = transformRef.current;
|
||||
const { y, scale } = transform;
|
||||
let x = transform.x;
|
||||
const absX = Math.abs(x);
|
||||
const absY = Math.abs(y);
|
||||
|
||||
clearSwipeDirectionDebounced();
|
||||
setIsActiveDebounced(true);
|
||||
@ -362,7 +374,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}
|
||||
// Get horizontal swipe direction
|
||||
const direction = x < 0 ? 1 : -1;
|
||||
const mId = getMediaId(activeMediaId, x < 0 ? 1 : -1);
|
||||
const mId = getMediaId(activeMediaIdRef.current, x < 0 ? 1 : -1);
|
||||
// Get the direction of the last pan gesture.
|
||||
// Could be different from the total horizontal swipe direction
|
||||
// if user starts a swipe in one direction and then changes the direction
|
||||
@ -372,20 +384,20 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const offset = (windowWidth + SLIDES_GAP) * direction;
|
||||
// If image is shifted by more than SWIPE_X_THRESHOLD,
|
||||
// We shift everything by one screen width and then set new active message id
|
||||
transformRef.current.x += offset;
|
||||
x += offset;
|
||||
setActiveMediaId(mId);
|
||||
selectMediaDebounced(mId);
|
||||
}
|
||||
// Then we always return to the original position
|
||||
cancelAnimation = animateNumber({
|
||||
from: transformRef.current.x,
|
||||
from: x,
|
||||
to: 0,
|
||||
duration: ANIMATION_DURATION,
|
||||
timing: easeOutCubic,
|
||||
onUpdate: (value) => setTransform({
|
||||
y: 0,
|
||||
x: value,
|
||||
scale: transformRef.current?.scale ?? 1,
|
||||
scale: scale ?? 1,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@ -399,15 +411,15 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
doubleTapZoom: DOUBLE_TAP_ZOOM,
|
||||
onCapture: (e) => {
|
||||
if (checkIfControlTarget(e)) return;
|
||||
const { x, y, scale } = transformRef.current;
|
||||
if (e.type === 'mousedown') {
|
||||
setIsMouseDown(true);
|
||||
if (transformRef.current.scale !== 1) {
|
||||
if (scale !== 1) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lastGestureTime = Date.now();
|
||||
const { x, y, scale } = transformRef.current;
|
||||
if (x === 0 && y === 0 && scale === 1) {
|
||||
if (!activeSlideRef.current) return;
|
||||
content = activeSlideRef.current.querySelector('img, video');
|
||||
@ -438,11 +450,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
lastDragOffset.y = dragOffsetY;
|
||||
const absOffsetX = Math.abs(dragOffsetX);
|
||||
const absOffsetY = Math.abs(dragOffsetY);
|
||||
const {
|
||||
scale,
|
||||
x,
|
||||
y,
|
||||
} = transformRef.current;
|
||||
const { x, y, scale } = transformRef.current;
|
||||
const threshold = 10;
|
||||
const tolerance = 1.5;
|
||||
|
||||
@ -476,7 +484,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
if (swipeDirectionRef.current === SwipeDirection.Horizontal
|
||||
|| Math.abs(x) > threshold || absOffsetX / absOffsetY > tolerance) {
|
||||
swipeDirectionRef.current = SwipeDirection.Horizontal;
|
||||
isActiveRef.current = false;
|
||||
setIsActive(false);
|
||||
const limit = windowWidth + SLIDES_GAP;
|
||||
const x1 = clamp(dragOffsetX, -limit, limit);
|
||||
setTransform({
|
||||
@ -619,11 +627,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
cleanup();
|
||||
document.removeEventListener('keydown', handleKeyDown, false);
|
||||
};
|
||||
}, [
|
||||
},
|
||||
[
|
||||
onClose,
|
||||
setTransform,
|
||||
getMediaId,
|
||||
activeMediaId,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
clickXThreshold,
|
||||
@ -633,14 +641,22 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
clearSwipeDirectionDebounced,
|
||||
animationLevel,
|
||||
setIsMouseDown,
|
||||
setIsActive,
|
||||
isHidden,
|
||||
isFullscreen,
|
||||
transformRef,
|
||||
setActiveMediaId,
|
||||
activeMediaIdRef,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const zoomChange = getZoomChange();
|
||||
const hasZoomChanged = prevZoomChangeRef.current !== undefined
|
||||
&& prevZoomChangeRef.current !== zoomChange;
|
||||
if (!containerRef.current || !hasZoomChanged || isHidden || isFullscreen) return;
|
||||
prevZoomChangeRef.current = zoomChange;
|
||||
const { scale } = transformRef.current;
|
||||
const dir = zoomLevelChange > 0 ? -1 : +1;
|
||||
const dir = zoomChange > 0 ? -1 : +1;
|
||||
const minZoom = MIN_ZOOM * 0.6;
|
||||
const maxZoom = MAX_ZOOM * 3;
|
||||
let steps = 100;
|
||||
@ -666,7 +682,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
containerRef.current.dispatchEvent(wheelEvent);
|
||||
},
|
||||
});
|
||||
}, [zoomLevelChange, hasZoomChanged, isHidden, isFullscreen]);
|
||||
}, [getZoomChange, isHidden, isFullscreen, transformRef]);
|
||||
|
||||
if (activeMediaId === undefined) return undefined;
|
||||
|
||||
@ -674,25 +690,21 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const prevMediaId = getMediaId(activeMediaId, -1);
|
||||
const hasPrev = prevMediaId !== undefined;
|
||||
const hasNext = nextMediaId !== undefined;
|
||||
const offsetX = transformRef.current.x;
|
||||
const offsetY = transformRef.current.y;
|
||||
const { scale } = transformRef.current;
|
||||
const isMoving = isMouseDown && scale > 1;
|
||||
const isMoving = isMouseDown && isScaled;
|
||||
|
||||
return (
|
||||
<div className="MediaViewerSlides" ref={containerRef}>
|
||||
{hasPrev && scale === 1 && !isResizing && (
|
||||
<div className="MediaViewerSlide" style={getAnimationStyle(-windowWidth + offsetX - SLIDES_GAP)}>
|
||||
<div className="MediaViewerSlide" ref={leftSlideRef}>
|
||||
{hasPrev && !isScaled && !isResizing && (
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
animationLevel={animationLevel}
|
||||
isMoving={isMoving}
|
||||
areControlsVisible={areControlsVisible}
|
||||
mediaId={prevMediaId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={buildClassName(
|
||||
'MediaViewerSlide',
|
||||
@ -701,32 +713,28 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
)}
|
||||
onClick={handleControlsVisibility}
|
||||
ref={activeSlideRef}
|
||||
style={getAnimationStyle(offsetX, offsetY, scale)}
|
||||
>
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
mediaId={activeMediaId}
|
||||
animationLevel={animationLevel}
|
||||
isActive={isActiveRef.current}
|
||||
setControlsVisible={setControlsVisible}
|
||||
isActive={isActive}
|
||||
isMoving={isMoving}
|
||||
areControlsVisible={areControlsVisible && scale === 1}
|
||||
/>
|
||||
</div>
|
||||
{hasNext && scale === 1 && !isResizing && (
|
||||
<div className="MediaViewerSlide" style={getAnimationStyle(windowWidth + offsetX + SLIDES_GAP)}>
|
||||
<div className="MediaViewerSlide" ref={rightSlideRef}>
|
||||
{hasNext && !isScaled && !isResizing && (
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
animationLevel={animationLevel}
|
||||
isMoving={isMoving}
|
||||
areControlsVisible={areControlsVisible}
|
||||
mediaId={nextMediaId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasPrev && scale === 1 && !IS_TOUCH_ENV && (
|
||||
)}
|
||||
</div>
|
||||
{hasPrev && !isScaled && !IS_TOUCH_ENV && (
|
||||
<button
|
||||
type="button"
|
||||
className={`navigation prev ${isVideo && !isGif && 'inline'}`}
|
||||
@ -734,7 +742,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
/>
|
||||
)}
|
||||
{hasNext && scale === 1 && !IS_TOUCH_ENV && (
|
||||
{hasNext && !isScaled && !IS_TOUCH_ENV && (
|
||||
<button
|
||||
type="button"
|
||||
className={`navigation next ${isVideo && !isGif && 'inline'}`}
|
||||
@ -748,8 +756,8 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
|
||||
export default memo(MediaViewerSlides);
|
||||
|
||||
function getAnimationStyle(x = 0, y = 0, scale = 1) {
|
||||
return `transform: translate3d(${x.toFixed(3)}px, ${y.toFixed(3)}px, 0px) scale(${scale.toFixed(3)});`;
|
||||
function getTransformStyle(x = 0, y = 0, scale = 1) {
|
||||
return `translate3d(${x.toFixed(3)}px, ${y.toFixed(3)}px, 0px) scale(${scale.toFixed(3)})`;
|
||||
}
|
||||
|
||||
function checkIfInsideSelector(element: HTMLElement, selector: string) {
|
||||
|
||||
@ -16,6 +16,7 @@ import usePictureInPicture from '../../hooks/usePictureInPicture';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
@ -37,10 +38,8 @@ type OwnProps = {
|
||||
isHidden?: boolean;
|
||||
playbackRate: number;
|
||||
isProtected?: boolean;
|
||||
areControlsVisible: boolean;
|
||||
shouldCloseOnClick?: boolean;
|
||||
isForceMobileVersion?: boolean;
|
||||
toggleControls: (isVisible: boolean) => void;
|
||||
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
isClickDisabled?: boolean;
|
||||
};
|
||||
@ -62,8 +61,6 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
playbackRate,
|
||||
onClose,
|
||||
isForceMobileVersion,
|
||||
toggleControls,
|
||||
areControlsVisible,
|
||||
shouldCloseOnClick,
|
||||
isProtected,
|
||||
isClickDisabled,
|
||||
@ -98,6 +95,8 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
isInPictureInPicture,
|
||||
] = usePictureInPicture(videoRef, handleEnterFullscreen, handleLeaveFullscreen);
|
||||
|
||||
const [, toggleControls] = useControlsSignal();
|
||||
|
||||
const handleVideoMove = useCallback(() => {
|
||||
toggleControls(true);
|
||||
}, [toggleControls]);
|
||||
@ -214,8 +213,12 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
useEffect(() => {
|
||||
if (!isMediaViewerOpen) return undefined;
|
||||
const rewind = (dir: number) => {
|
||||
if (!isFullscreen) return;
|
||||
const video = videoRef.current!;
|
||||
video.currentTime = clamp(video.currentTime + dir * REWIND_STEP, 0, video.duration);
|
||||
const newTime = clamp(video.currentTime + dir * REWIND_STEP, 0, video.duration);
|
||||
if (Number.isFinite(newTime)) {
|
||||
video.currentTime = newTime;
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (isInPictureInPicture) return;
|
||||
@ -243,7 +246,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, false);
|
||||
};
|
||||
}, [togglePlayState, isMediaViewerOpen, isInPictureInPicture]);
|
||||
}, [togglePlayState, isMediaViewerOpen, isFullscreen, isInPictureInPicture]);
|
||||
|
||||
const wrapperStyle = posterSize && `width: ${posterSize.width}px; height: ${posterSize.height}px`;
|
||||
const videoStyle = `background-image: url(${posterData})`;
|
||||
@ -322,8 +325,6 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
isFullscreen={isFullscreen}
|
||||
fileSize={fileSize}
|
||||
duration={duration}
|
||||
isVisible={areControlsVisible}
|
||||
setVisibility={toggleControls}
|
||||
isForceMobileVersion={isForceMobileVersion}
|
||||
onSeek={handleSeek}
|
||||
onChangeFullscreen={handleFullscreenChange}
|
||||
|
||||
@ -13,6 +13,8 @@ import { captureEvents } from '../../util/captureEvents';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import RangeSlider from '../ui/RangeSlider';
|
||||
@ -32,7 +34,6 @@ type OwnProps = {
|
||||
isFullscreenSupported: boolean;
|
||||
isPictureInPictureSupported: boolean;
|
||||
isFullscreen: boolean;
|
||||
isVisible: boolean;
|
||||
isBuffered: boolean;
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
@ -43,7 +44,6 @@ type OwnProps = {
|
||||
onVolumeChange: (volume: number) => void;
|
||||
onPlaybackRateChange: (playbackRate: number) => void;
|
||||
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
setVisibility: (isVisible: boolean) => void;
|
||||
onSeek: (position: number) => void;
|
||||
};
|
||||
|
||||
@ -70,7 +70,6 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
||||
isPlaying,
|
||||
isFullscreenSupported,
|
||||
isFullscreen,
|
||||
isVisible,
|
||||
isBuffered,
|
||||
volume,
|
||||
isMuted,
|
||||
@ -82,7 +81,6 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
||||
isPictureInPictureSupported,
|
||||
onPictureInPictureChange,
|
||||
onPlayPause,
|
||||
setVisibility,
|
||||
onSeek,
|
||||
}) => {
|
||||
const [isPlaybackMenuOpen, openPlaybackMenu, closePlaybackMenu] = useFlag();
|
||||
@ -92,6 +90,8 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
||||
const isSeeking = isSeekingRef.current;
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const [getIsVisible, setVisibility] = useControlsSignal();
|
||||
const isVisible = useDerivedState(getIsVisible);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_TOUCH_ENV && !isForceMobileVersion) return undefined;
|
||||
|
||||
15
src/components/mediaViewer/hooks/useControlsSignal.ts
Normal file
15
src/components/mediaViewer/hooks/useControlsSignal.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { createSignal } from '../../../util/signals';
|
||||
import useDerivedSignal from '../../../hooks/useDerivedSignal';
|
||||
|
||||
const [getControlsVisible, setControlsVisible] = createSignal(false);
|
||||
const [getIsLocked, setIsLocked] = createSignal(false);
|
||||
|
||||
export default function useControlsSignal() {
|
||||
const getVisible = useDerivedSignal(
|
||||
() => getControlsVisible() && !getIsLocked(),
|
||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||
[getControlsVisible, getIsLocked],
|
||||
);
|
||||
|
||||
return [getVisible, setControlsVisible, setIsLocked] as const;
|
||||
}
|
||||
14
src/components/mediaViewer/hooks/useZoomChangeSignal.ts
Normal file
14
src/components/mediaViewer/hooks/useZoomChangeSignal.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { createSignal } from '../../../util/signals';
|
||||
import { useEffect } from '../../../lib/teact/teact';
|
||||
|
||||
const [getZoomChange, setZoomChange] = createSignal(1);
|
||||
|
||||
export default function useZoomChange() {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setZoomChange(1);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [getZoomChange, setZoomChange] as const;
|
||||
}
|
||||
18
src/hooks/useSignalRef.ts
Normal file
18
src/hooks/useSignalRef.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useRef } from '../lib/teact/teact';
|
||||
|
||||
import type { Signal } from '../util/signals';
|
||||
|
||||
import useEffectOnce from './useEffectOnce';
|
||||
|
||||
// Allows to use signal value as "silent" dependency in hooks (not causing updates)
|
||||
export function useSignalRef<T>(getValue: Signal<T>) {
|
||||
const ref = useRef<T>(getValue());
|
||||
|
||||
useEffectOnce(() => {
|
||||
return getValue.subscribe(() => {
|
||||
ref.current = getValue();
|
||||
});
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user