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