From 0079e8b7abb1bc4516e8dd676a21ca0f5a23ec51 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 15 Apr 2023 13:50:44 +0200 Subject: [PATCH] [Perf] Media Viewer: Use signals (#2838) --- src/components/mediaViewer/MediaViewer.tsx | 18 +- .../mediaViewer/MediaViewerActions.tsx | 20 +-- .../mediaViewer/MediaViewerContent.tsx | 16 +- .../mediaViewer/MediaViewerFooter.tsx | 8 +- .../mediaViewer/MediaViewerSlides.tsx | 166 +++++++++--------- src/components/mediaViewer/VideoPlayer.tsx | 17 +- .../mediaViewer/VideoPlayerControls.tsx | 8 +- .../mediaViewer/hooks/useControlsSignal.ts | 15 ++ .../mediaViewer/hooks/useZoomChangeSignal.ts | 14 ++ src/hooks/useSignalRef.ts | 18 ++ 10 files changed, 175 insertions(+), 125 deletions(-) create mode 100644 src/components/mediaViewer/hooks/useControlsSignal.ts create mode 100644 src/components/mediaViewer/hooks/useZoomChangeSignal.ts create mode 100644 src/hooks/useSignalRef.ts diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index da1e45e17..8e2ff7204 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -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 = ({ /* Controls */ const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); - const [zoomLevelChange, setZoomLevelChange] = useState(1); const { webPagePhoto, @@ -220,20 +220,23 @@ const MediaViewer: FC = ({ 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 = ({ onReport={openReportModal} onCloseMediaViewer={handleClose} onForward={handleForward} - zoomLevelChange={zoomLevelChange} - setZoomLevelChange={setZoomLevelChange} /> = ({ origin={origin} isOpen={isOpen} hasFooter={hasFooter} - zoomLevelChange={zoomLevelChange} isVideo={isVideo} animationLevel={animationLevel} onClose={handleClose} diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index a8cf24f69..8f6340fe2 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -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 = ({ @@ -75,7 +74,6 @@ const MediaViewerActions: FC = ({ isDownloading, isProtected, canReport, - zoomLevelChange, canDelete, canUpdate, messageListType, @@ -84,9 +82,9 @@ const MediaViewerActions: FC = ({ 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 = ({ }, [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; diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index 3600759af..eb6fc9908 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -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 = (props) => { message, origin, animationLevel, - areControlsVisible, isProtected, volume, playbackRate, @@ -76,7 +74,6 @@ const MediaViewerContent: FC = (props) => { isHidden, onClose, onFooterClick, - setControlsVisible, isMoving, } = props; @@ -99,13 +96,11 @@ const MediaViewerContent: FC = (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 = (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 = (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 = (props) => { onClick={onFooterClick} isProtected={isProtected} isForceMobileVersion={isForceMobileVersion} - isHidden={IS_TOUCH_ENV ? !areControlsVisible : false} isForVideo={isVideo && !isGif} /> )} diff --git a/src/components/mediaViewer/MediaViewerFooter.tsx b/src/components/mediaViewer/MediaViewerFooter.tsx index 596acfa80..7735f1b8e 100644 --- a/src/components/mediaViewer/MediaViewerFooter.tsx +++ b/src/components/mediaViewer/MediaViewerFooter.tsx @@ -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 = ({ - 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; diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index aa7ed1e9c..a438797e3 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -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 = ({ isPhoto, isOpen, hasFooter, - zoomLevelChange, animationLevel, isHidden, ...rest @@ -92,19 +93,26 @@ const MediaViewerSlides: FC = ({ const containerRef = useRef(null); // eslint-disable-next-line no-null/no-null const activeSlideRef = useRef(null); - const transformRef = useRef({ x: 0, y: 0, scale: 1 }); + // eslint-disable-next-line no-null/no-null + const leftSlideRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const rightSlideRef = useRef(null); const lastTransformRef = useRef({ x: 0, y: 0, scale: 1 }); const swipeDirectionRef = useRef(undefined); - const isActiveRef = useRef(true); const isReleasedRef = useRef(false); - const [activeMediaId, setActiveMediaId] = useState(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({ x: 0, y: 0, scale: 1 }); + const transformRef = useSignalRef(getTransform); + const [getActiveMediaId, setActiveMediaId] = useSignal(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 = ({ 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 = ({ 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 = ({ }, 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 = ({ return true; } cancelAnimation = animateNumber({ - from: transformRef.current.x, + from: x, to: 0, duration: ANIMATION_DURATION, timing: easeOutCubic, @@ -201,7 +213,8 @@ const MediaViewerSlides: FC = ({ }; 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 = ({ }; 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 = ({ 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 = ({ } // 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ cleanup(); document.removeEventListener('keydown', handleKeyDown, false); }; - }, [ + }, + [ onClose, setTransform, getMediaId, - activeMediaId, windowWidth, windowHeight, clickXThreshold, @@ -633,14 +641,22 @@ const MediaViewerSlides: FC = ({ 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 = ({ containerRef.current.dispatchEvent(wheelEvent); }, }); - }, [zoomLevelChange, hasZoomChanged, isHidden, isFullscreen]); + }, [getZoomChange, isHidden, isFullscreen, transformRef]); if (activeMediaId === undefined) return undefined; @@ -674,25 +690,21 @@ const MediaViewerSlides: FC = ({ 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 (
- {hasPrev && scale === 1 && !isResizing && ( -
+
+ {hasPrev && !isScaled && !isResizing && ( -
- )} + )} +
= ({ )} onClick={handleControlsVisibility} ref={activeSlideRef} - style={getAnimationStyle(offsetX, offsetY, scale)} >
- {hasNext && scale === 1 && !isResizing && ( -
+
+ {hasNext && !isScaled && !isResizing && ( -
- )} - {hasPrev && scale === 1 && !IS_TOUCH_ENV && ( + )} +
+ {hasPrev && !isScaled && !IS_TOUCH_ENV && (