[Perf] Media Viewer: Use signals (#2838)

This commit is contained in:
Alexander Zinchuk 2023-04-15 13:50:44 +02:00
parent c467da8680
commit 0079e8b7ab
10 changed files with 175 additions and 125 deletions

View File

@ -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}

View File

@ -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;

View File

@ -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}
/>
)}

View File

@ -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;

View File

@ -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) {

View File

@ -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}

View File

@ -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;

View 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;
}

View 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
View 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;
}