[Refactoring] Main: Simplify column animation

This commit is contained in:
Alexander Zinchuk 2022-04-19 15:12:04 +02:00
parent c67560d431
commit 4d448e4d07
26 changed files with 208 additions and 161 deletions

View File

@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../../global';
import { IAnchorPosition } from '../../../types'; import { IAnchorPosition } from '../../../types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useThrottle from '../../../hooks/useThrottle'; import useRunThrottled from '../../../hooks/useRunThrottled';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls'; import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls';
@ -80,7 +80,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); }, [id]);
const runThrottled = useThrottle(VOLUME_CHANGE_THROTTLE); const runThrottled = useRunThrottled(VOLUME_CHANGE_THROTTLE);
const handleRemove = useCallback((e: React.SyntheticEvent<any>) => { const handleRemove = useCallback((e: React.SyntheticEvent<any>) => {
e.stopPropagation(); e.stopPropagation();

View File

@ -1,5 +1,5 @@
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import useDebounce from '../../../hooks/useDebounce'; import useRunDebounced from '../../../hooks/useRunDebounced';
import React, { import React, {
FC, memo, useCallback, useEffect, FC, memo, useCallback, useEffect,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
@ -59,7 +59,7 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
loadNotificationSettings(); loadNotificationSettings();
}, [loadNotificationSettings]); }, [loadNotificationSettings]);
const runDebounced = useDebounce(500, true); const runDebounced = useRunDebounced(500, true);
const handleSettingsChange = useCallback(( const handleSettingsChange = useCallback((
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,

View File

@ -12,7 +12,7 @@ import {
getMessageContentFilename, getMessageMediaHash, getMessageContentFilename, getMessageMediaHash,
} from '../../global/helpers'; } from '../../global/helpers';
import useDebounce from '../../hooks/useDebounce'; import useRunDebounced from '../../hooks/useRunDebounced';
type StateProps = { type StateProps = {
activeDownloads: Record<string, number[]>; activeDownloads: Record<string, number[]>;
@ -33,17 +33,17 @@ const DownloadManager: FC<StateProps> = ({
}) => { }) => {
const { cancelMessagesMediaDownload } = getActions(); const { cancelMessagesMediaDownload } = getActions();
const debouncedGlobalUpdate = useDebounce(GLOBAL_UPDATE_DEBOUNCE, true); const runDebounced = useRunDebounced(GLOBAL_UPDATE_DEBOUNCE, true);
const handleMessageDownloaded = useCallback((message: ApiMessage) => { const handleMessageDownloaded = useCallback((message: ApiMessage) => {
downloadedMessages.add(message); downloadedMessages.add(message);
debouncedGlobalUpdate(() => { runDebounced(() => {
if (downloadedMessages.size) { if (downloadedMessages.size) {
cancelMessagesMediaDownload({ messages: Array.from(downloadedMessages) }); cancelMessagesMediaDownload({ messages: Array.from(downloadedMessages) });
downloadedMessages.clear(); downloadedMessages.clear();
} }
}); });
}, [cancelMessagesMediaDownload, debouncedGlobalUpdate]); }, [cancelMessagesMediaDownload, runDebounced]);
useEffect(() => { useEffect(() => {
const activeMessages = Object.entries(activeDownloads).map(([chatId, messageIds]) => ( const activeMessages = Object.entries(activeDownloads).map(([chatId, messageIds]) => (

View File

@ -20,6 +20,7 @@
.has-call-header { .has-call-header {
--call-header-height: 2rem; --call-header-height: 2rem;
#LeftColumn, #MiddleColumn, #RightColumn-wrapper { #LeftColumn, #MiddleColumn, #RightColumn-wrapper {
height: calc(100% - 2rem); height: calc(100% - 2rem);
margin-top: 2rem; margin-top: 2rem;
@ -74,9 +75,10 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: black; background: black;
opacity: var(--layer-blackout-opacity); opacity: 0;
transition: opacity var(--layer-transition); transition: opacity var(--layer-transition);
z-index: 1; z-index: 1;
pointer-events: none;
body.animation-level-0 & { body.animation-level-0 & {
transition: none; transition: none;
@ -87,18 +89,18 @@
display: none; display: none;
} }
body.is-android .middle-column-shown & { body.is-android #Main.left-column-animating & {
display: block; display: block;
} }
body:not(.is-android) #Main:not(.left-column-open) &,
body.android-left-blackout-open & {
opacity: var(--layer-blackout-opacity);
}
} }
#Main:not(.middle-column-open) & { #Main.left-column-open & {
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
&::after {
opacity: 0;
pointer-events: none;
}
} }
#Main.history-animation-disabled & { #Main.history-animation-disabled & {
@ -154,7 +156,7 @@
transition: none; transition: none;
} }
#Main:not(.middle-column-open) & { #Main.left-column-open & {
transform: translate3d(26.5rem, 0, 0); transform: translate3d(26.5rem, 0, 0);
} }
} }
@ -162,7 +164,7 @@
@media (max-width: 600px) { @media (max-width: 600px) {
border-left: none; border-left: none;
#Main:not(.middle-column-open) & { #Main.left-column-open & {
transform: translate3d(100vw, 0, 0); transform: translate3d(100vw, 0, 0);
} }
@ -187,7 +189,7 @@ body.is-android.animation-level-1 {
transition: transform var(--layer-transition), opacity var(--layer-transition); transition: transform var(--layer-transition), opacity var(--layer-transition);
} }
#Main:not(.middle-column-shown) { #Main.left-column-open:not(.left-column-animating) {
#MiddleColumn { #MiddleColumn {
display: none; display: none;
} }
@ -200,14 +202,15 @@ body.is-android.animation-level-1 {
} }
} }
#Main.middle-column-open { #Main:not(.left-column-open) {
#LeftColumn { #LeftColumn {
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
opacity: 0; opacity: 0;
} }
} }
#Main:not(.right-column-shown) { // @optimization
#Main:not(.right-column-open):not(.right-column-animating) {
#RightColumn { #RightColumn {
display: none; display: none;
} }

View File

@ -1,5 +1,5 @@
import React, { import React, {
FC, useEffect, memo, useCallback, FC, useEffect, memo, useCallback, useState, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
@ -11,6 +11,7 @@ import '../../global/actions/all';
import { import {
BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER, PAGE_TITLE, BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER, PAGE_TITLE,
} from '../../config'; } from '../../config';
import { IS_ANDROID } from '../../util/environment';
import { import {
selectChatMessage, selectChatMessage,
selectIsForwardModalOpen, selectIsForwardModalOpen,
@ -20,18 +21,19 @@ import {
} from '../../global/selectors'; } from '../../global/selectors';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { fastRaf } from '../../util/schedulers';
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink'; import { processDeepLink } from '../../util/deeplink';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import windowSize from '../../util/windowSize'; import windowSize from '../../util/windowSize';
import { getAllNotificationsCount } from '../../util/folderManager'; import { getAllNotificationsCount } from '../../util/folderManager';
import useShowTransition from '../../hooks/useShowTransition';
import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBackgroundMode from '../../hooks/useBackgroundMode';
import useBeforeUnload from '../../hooks/useBeforeUnload'; import useBeforeUnload from '../../hooks/useBeforeUnload';
import useOnChange from '../../hooks/useOnChange'; import useOnChange from '../../hooks/useOnChange';
import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture'; import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture';
import useForceUpdate from '../../hooks/useForceUpdate';
import { LOCATION_HASH } from '../../hooks/useHistoryBack'; import { LOCATION_HASH } from '../../hooks/useHistoryBack';
import useShowTransition from '../../hooks/useShowTransition';
import { fastRaf } from '../../util/schedulers';
import StickerSetModal from '../common/StickerSetModal.async'; import StickerSetModal from '../common/StickerSetModal.async';
import UnreadCount from '../common/UnreadCounter'; import UnreadCount from '../common/UnreadCounter';
@ -59,8 +61,8 @@ type StateProps = {
connectionState?: ApiUpdateConnectionStateType; connectionState?: ApiUpdateConnectionStateType;
authState?: ApiUpdateAuthorizationStateType; authState?: ApiUpdateAuthorizationStateType;
lastSyncTime?: number; lastSyncTime?: number;
isLeftColumnShown: boolean; isLeftColumnOpen: boolean;
isRightColumnShown: boolean; isRightColumnOpen: boolean;
isMediaViewerOpen: boolean; isMediaViewerOpen: boolean;
isForwardModalOpen: boolean; isForwardModalOpen: boolean;
hasNotifications: boolean; hasNotifications: boolean;
@ -95,8 +97,8 @@ const Main: FC<StateProps> = ({
connectionState, connectionState,
authState, authState,
lastSyncTime, lastSyncTime,
isLeftColumnShown, isLeftColumnOpen,
isRightColumnShown, isRightColumnOpen,
isMediaViewerOpen, isMediaViewerOpen,
isForwardModalOpen, isForwardModalOpen,
hasNotifications, hasNotifications,
@ -224,51 +226,69 @@ const Main: FC<StateProps> = ({
}; };
}, [activeGroupCallId]); }, [activeGroupCallId]);
const { const leftColumnTransition = useShowTransition(
transitionClassNames: middleColumnTransitionClassNames, isLeftColumnOpen, undefined, true, undefined, shouldSkipHistoryAnimations,
} = useShowTransition(!isLeftColumnShown, undefined, true, undefined, shouldSkipHistoryAnimations);
const {
transitionClassNames: rightColumnTransitionClassNames,
} = useShowTransition(isRightColumnShown, undefined, true, undefined, shouldSkipHistoryAnimations);
const className = buildClassName(
middleColumnTransitionClassNames.replace(/([\w-]+)/g, 'middle-column-$1'),
rightColumnTransitionClassNames.replace(/([\w-]+)/g, 'right-column-$1'),
shouldSkipHistoryAnimations && 'history-animation-disabled',
); );
const willAnimateLeftColumnRef = useRef(false);
const forceUpdate = useForceUpdate();
// Dispatch heavy transition event when opening middle column // Handle opening middle column
useOnChange(([prevIsLeftColumnShown]) => { useOnChange(([prevIsLeftColumnOpen]) => {
if (prevIsLeftColumnShown === undefined || animationLevel === 0) { if (prevIsLeftColumnOpen === undefined || animationLevel === 0) {
return; return;
} }
willAnimateLeftColumnRef.current = true;
if (IS_ANDROID) {
fastRaf(() => {
document.body.classList.toggle('android-left-blackout-open', !isLeftColumnOpen);
});
}
const dispatchHeavyAnimationEnd = dispatchHeavyAnimationEvent(); const dispatchHeavyAnimationEnd = dispatchHeavyAnimationEvent();
waitForTransitionEnd(document.getElementById('MiddleColumn')!, dispatchHeavyAnimationEnd); waitForTransitionEnd(document.getElementById('MiddleColumn')!, () => {
}, [isLeftColumnShown]); dispatchHeavyAnimationEnd();
willAnimateLeftColumnRef.current = false;
forceUpdate();
});
}, [isLeftColumnOpen]);
// Dispatch heavy transition event and add body class when opening right column const rightColumnTransition = useShowTransition(
useOnChange(([prevIsRightColumnShown]) => { isRightColumnOpen, undefined, true, undefined, shouldSkipHistoryAnimations,
if (prevIsRightColumnShown === undefined || animationLevel === 0) { );
const willAnimateRightColumnRef = useRef(false);
const [isNarrowMessageList, setIsNarrowMessageList] = useState(isRightColumnOpen);
// Handle opening right column
useOnChange(([prevIsRightColumnOpen]) => {
if (prevIsRightColumnOpen === undefined || animationLevel === 0) {
return; return;
} }
fastRaf(() => { willAnimateRightColumnRef.current = true;
document.body.classList.add('animating-right-column');
});
const dispatchHeavyAnimationEnd = dispatchHeavyAnimationEvent(); const dispatchHeavyAnimationEnd = dispatchHeavyAnimationEvent();
waitForTransitionEnd(document.getElementById('RightColumn')!, () => { waitForTransitionEnd(document.getElementById('RightColumn')!, () => {
dispatchHeavyAnimationEnd(); dispatchHeavyAnimationEnd();
willAnimateRightColumnRef.current = false;
fastRaf(() => { forceUpdate();
document.body.classList.remove('animating-right-column'); setIsNarrowMessageList(isRightColumnOpen);
});
}); });
}, [isRightColumnShown]); }, [isRightColumnOpen]);
const className = buildClassName(
leftColumnTransition.hasShownClass && 'left-column-shown',
leftColumnTransition.hasOpenClass && 'left-column-open',
willAnimateLeftColumnRef.current && 'left-column-animating',
rightColumnTransition.hasShownClass && 'right-column-shown',
rightColumnTransition.hasOpenClass && 'right-column-open',
willAnimateRightColumnRef.current && 'right-column-animating',
isNarrowMessageList && 'narrow-message-list',
shouldSkipHistoryAnimations && 'history-animation-disabled',
);
const handleBlur = useCallback(() => { const handleBlur = useCallback(() => {
updateIsOnline(false); updateIsOnline(false);
@ -390,8 +410,8 @@ export default memo(withGlobal(
connectionState: global.connectionState, connectionState: global.connectionState,
authState: global.authState, authState: global.authState,
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
isLeftColumnShown: global.isLeftColumnShown, isLeftColumnOpen: global.isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global), isRightColumnOpen: selectIsRightColumnShown(global),
isMediaViewerOpen: selectIsMediaViewerOpen(global), isMediaViewerOpen: selectIsMediaViewerOpen(global),
isForwardModalOpen: selectIsForwardModalOpen(global), isForwardModalOpen: selectIsForwardModalOpen(global),
hasNotifications: Boolean(global.notifications.length), hasNotifications: Boolean(global.notifications.length),

View File

@ -4,18 +4,18 @@ import React, {
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import useDebounce from '../../hooks/useDebounce';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import { animateNumber, timingFunctions } from '../../util/animation'; import { animateNumber, timingFunctions } from '../../util/animation';
import arePropsShallowEqual from '../../util/arePropsShallowEqual'; import arePropsShallowEqual from '../../util/arePropsShallowEqual';
import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD, RealTouchEvent } from '../../util/captureEvents'; import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD, RealTouchEvent } from '../../util/captureEvents';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment'; import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment';
import { debounce } from '../../util/schedulers'; import { debounce } from '../../util/schedulers';
import useTimeout from '../../hooks/useTimeout';
import useDebouncedCallback from '../../hooks/useDebouncedCallback';
import MediaViewerContent from './MediaViewerContent'; import MediaViewerContent from './MediaViewerContent';
import './MediaViewerSlides.scss'; import './MediaViewerSlides.scss';
import useTimeout from '../../hooks/useTimeout';
type OwnProps = { type OwnProps = {
messageId?: number; messageId?: number;
@ -96,14 +96,14 @@ const MediaViewerSlides: FC<OwnProps> = ({
forceUpdate(); forceUpdate();
}, [forceUpdate]); }, [forceUpdate]);
const setIsActive = useCallback((value: boolean) => { const selectMessageDebounced = useDebouncedCallback(selectMessage, [], DEBOUNCE_MESSAGE, true);
const clearSwipeDirectionDebounced = useDebouncedCallback(() => {
swipeDirectionRef.current = undefined;
}, [], DEBOUNCE_SWIPE, true);
const setIsActiveDebounced = useDebouncedCallback((value: boolean) => {
isActiveRef.current = value; isActiveRef.current = value;
forceUpdate(); forceUpdate();
}, [forceUpdate]); }, [forceUpdate], DEBOUNCE_ACTIVE, true);
const debounceSetMessage = useDebounce(DEBOUNCE_MESSAGE, true);
const debounceSwipeDirection = useDebounce(DEBOUNCE_SWIPE, true);
const debounceActive = useDebounce(DEBOUNCE_ACTIVE, true);
const handleToggleFooterVisibility = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { const handleToggleFooterVisibility = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!IS_TOUCH_ENV) return; if (!IS_TOUCH_ENV) return;
@ -156,10 +156,8 @@ const MediaViewerSlides: FC<OwnProps> = ({
transformRef.current.x += offset; transformRef.current.x += offset;
isActiveRef.current = false; isActiveRef.current = false;
setActiveMessageId(mId); setActiveMessageId(mId);
debounceSetMessage(() => selectMessage(mId)); selectMessageDebounced(mId);
debounceActive(() => { setIsActiveDebounced(true);
setIsActive(true);
});
lastTransform = { x: 0, y: 0, scale: 1 }; lastTransform = { x: 0, y: 0, scale: 1 };
cancelAnimation = animateNumber({ cancelAnimation = animateNumber({
from: transformRef.current.x, from: transformRef.current.x,
@ -351,12 +349,8 @@ const MediaViewerSlides: FC<OwnProps> = ({
y, y,
} = transformRef.current; } = transformRef.current;
debounceSwipeDirection(() => { clearSwipeDirectionDebounced();
swipeDirectionRef.current = undefined; setIsActiveDebounced(true);
});
debounceActive(() => {
setIsActive(true);
});
// If scale is less than 1 we need to bounce back // If scale is less than 1 we need to bounce back
if (scale < 1) { if (scale < 1) {
@ -476,7 +470,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
// 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; transformRef.current.x += offset;
setActiveMessageId(mId); setActiveMessageId(mId);
debounceSetMessage(() => selectMessage(mId)); selectMessageDebounced(mId);
} }
// Then we always return to the original position // Then we always return to the original position
cancelAnimation = animateNumber({ cancelAnimation = animateNumber({
@ -493,14 +487,15 @@ const MediaViewerSlides: FC<OwnProps> = ({
return undefined; return undefined;
}, },
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
isZoomed, isZoomed,
onClose, onClose,
setTransform, setTransform,
getMessageId, getMessageId,
activeMessageId, activeMessageId,
setIsActive, selectMessageDebounced,
setIsActiveDebounced,
clearSwipeDirectionDebounced,
]); ]);
if (!activeMessageId) return undefined; if (!activeMessageId) return undefined;

View File

@ -32,6 +32,7 @@
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: bottom 150ms ease-out, transform var(--layer-transition); transition: bottom 150ms ease-out, transform var(--layer-transition);
body.keyboard-visible & { body.keyboard-visible & {
position: relative; position: relative;
bottom: calc(0px - env(safe-area-inset-bottom)); bottom: calc(0px - env(safe-area-inset-bottom));
@ -95,6 +96,7 @@
@media (max-width: 600px) { @media (max-width: 600px) {
&.with-bottom-shift { &.with-bottom-shift {
margin-bottom: 0; margin-bottom: 0;
.last-in-list { .last-in-list {
margin-bottom: 4.25rem; margin-bottom: 4.25rem;
@ -317,9 +319,7 @@
transform: translate3d(calc(var(--right-column-width) / -2), 0, 0); transform: translate3d(calc(var(--right-column-width) / -2), 0, 0);
} }
body:not(.animating-right-column) #Main.right-column-open &.select-mode-active, #Main.narrow-message-list & {
#Main.right-column-open &:not(.select-mode-active),
body.animating-right-column &:not(.select-mode-active) {
width: calc(100% - var(--right-column-width)); width: calc(100% - var(--right-column-width));
.messages-container { .messages-container {

View File

@ -185,7 +185,7 @@
} }
// @optimization // @optimization
@include while-transition() { #Main.right-column-animating & {
pointer-events: none; pointer-events: none;
} }
} }

View File

@ -31,7 +31,7 @@
transition: none; transition: none;
} }
&:not(.middle-column-open) { &.left-column-open {
transform: translate3d(100vw, 0, 0) !important; transform: translate3d(100vw, 0, 0) !important;
} }
} }

View File

@ -211,7 +211,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
const className = buildClassName( const className = buildClassName(
'SymbolMenu mobile-menu', 'SymbolMenu mobile-menu',
transitionClassNames, transitionClassNames,
!isLeftColumnShown && 'middle-column-open', isLeftColumnShown && 'left-column-open',
); );
return ( return (

View File

@ -1,7 +1,7 @@
import { useCallback } from '../../../lib/teact/teact'; import { useCallback } from '../../../lib/teact/teact';
import { fastRaf } from '../../../util/schedulers'; import { fastRaf } from '../../../util/schedulers';
import useDebounce from '../../../hooks/useDebounce'; import useRunDebounced from '../../../hooks/useRunDebounced';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
const DEBOUNCE = 1000; const DEBOUNCE = 1000;
@ -13,7 +13,7 @@ export default function useStickyDates() {
// so we will add `position: sticky` only after first scroll. There would be no animation on the first show though. // so we will add `position: sticky` only after first scroll. There would be no animation on the first show though.
const [isScrolled, markIsScrolled] = useFlag(false); const [isScrolled, markIsScrolled] = useFlag(false);
const runDebounced = useDebounce(DEBOUNCE, true); const runDebounced = useRunDebounced(DEBOUNCE, true);
const updateStickyDates = useCallback((container: HTMLDivElement, hasTools?: boolean) => { const updateStickyDates = useCallback((container: HTMLDivElement, hasTools?: boolean) => {
markIsScrolled(); markIsScrolled();

View File

@ -114,8 +114,7 @@
} }
} }
#Main.right-column-open &, #Main.right-column-shown & {
body.animating-right-column & {
visibility: visible; visibility: visible;
} }

View File

@ -7,7 +7,7 @@ import {
ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent, ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent,
} from '../../types'; } from '../../types';
import { MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config'; import { ANIMATION_END_DELAY, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import { import {
selectAreActiveChatsLoaded, selectAreActiveChatsLoaded,
@ -41,7 +41,7 @@ type StateProps = {
nextManagementScreen?: ManagementScreens; nextManagementScreen?: ManagementScreens;
}; };
const CLOSE_ANIMATION_DURATION = 300; const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
const MAIN_SCREENS_COUNT = Object.keys(RightColumnContent).length / 2; const MAIN_SCREENS_COUNT = Object.keys(RightColumnContent).length / 2;
const MANAGEMENT_SCREENS_COUNT = Object.keys(ManagementScreens).length / 2; const MANAGEMENT_SCREENS_COUNT = Object.keys(ManagementScreens).length / 2;
@ -191,7 +191,7 @@ const RightColumn: FC<StateProps> = ({
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setShouldSkipTransition(!isOpen); setShouldSkipTransition(!isOpen);
}, CLOSE_ANIMATION_DURATION); }, ANIMATION_DURATION);
}, [isOpen]); }, [isOpen]);
useEffect(() => { useEffect(() => {
@ -300,7 +300,6 @@ const RightColumn: FC<StateProps> = ({
profileState={profileState} profileState={profileState}
managementScreen={managementScreen} managementScreen={managementScreen}
onClose={close} onClose={close}
shouldSkipAnimation={shouldSkipTransition || shouldSkipHistoryAnimations}
onScreenSelect={setManagementScreen} onScreenSelect={setManagementScreen}
/> />
<Transition <Transition

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../global';
import { ManagementScreens, ProfileState } from '../../types'; import { ManagementScreens, ProfileState } from '../../types';
import { ApiExportedInvite } from '../../api/types'; import { ApiExportedInvite } from '../../api/types';
import { ANIMATION_END_DELAY } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { debounce } from '../../util/schedulers'; import { debounce } from '../../util/schedulers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -43,7 +44,6 @@ type OwnProps = {
isGifSearch?: boolean; isGifSearch?: boolean;
isPollResults?: boolean; isPollResults?: boolean;
isAddingChatMembers?: boolean; isAddingChatMembers?: boolean;
shouldSkipAnimation?: boolean;
profileState?: ProfileState; profileState?: ProfileState;
managementScreen?: ManagementScreens; managementScreen?: ManagementScreens;
onClose: () => void; onClose: () => void;
@ -61,9 +61,10 @@ type StateProps = {
gifSearchQuery?: string; gifSearchQuery?: string;
isEditingInvite?: boolean; isEditingInvite?: boolean;
currentInviteInfo?: ApiExportedInvite; currentInviteInfo?: ApiExportedInvite;
shouldSkipHistoryAnimations?: boolean;
}; };
const COLUMN_CLOSE_DELAY_MS = 300; const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
const runDebouncedForSearch = debounce((cb) => cb(), 200, false); const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
enum HeaderContent { enum HeaderContent {
@ -121,10 +122,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
messageSearchQuery, messageSearchQuery,
stickerSearchQuery, stickerSearchQuery,
gifSearchQuery, gifSearchQuery,
shouldSkipAnimation,
isEditingInvite, isEditingInvite,
canViewStatistics, canViewStatistics,
currentInviteInfo, currentInviteInfo,
shouldSkipHistoryAnimations,
}) => { }) => {
const { const {
setLocalTextSearchQuery, setLocalTextSearchQuery,
@ -179,7 +180,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setShouldSkipTransition(!isColumnOpen); setShouldSkipTransition(!isColumnOpen);
}, COLUMN_CLOSE_DELAY_MS); }, COLUMN_ANIMATION_DURATION);
}, [isColumnOpen]); }, [isColumnOpen]);
const lang = useLang(); const lang = useLang();
@ -436,7 +437,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const buttonClassName = buildClassName( const buttonClassName = buildClassName(
'animated-close-icon', 'animated-close-icon',
isBackButton && 'state-back', isBackButton && 'state-back',
(shouldSkipTransition || shouldSkipAnimation) && 'no-transition', (shouldSkipTransition || shouldSkipHistoryAnimations) && 'no-transition',
); );
return ( return (
@ -452,7 +453,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
<div ref={backButtonRef} className={buttonClassName} /> <div ref={backButtonRef} className={buttonClassName} />
</Button> </Button>
<Transition <Transition
name={(shouldSkipTransition || shouldSkipAnimation) ? 'none' : 'slide-fade'} name={(shouldSkipTransition || shouldSkipHistoryAnimations) ? 'none' : 'slide-fade'}
activeKey={renderingContentKey} activeKey={renderingContentKey}
> >
{renderHeaderContent()} {renderHeaderContent()}
@ -495,6 +496,7 @@ export default memo(withGlobal<OwnProps>(
gifSearchQuery, gifSearchQuery,
isEditingInvite, isEditingInvite,
currentInviteInfo, currentInviteInfo,
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
}; };
}, },
)(RightHeader)); )(RightHeader));

View File

@ -1,9 +0,0 @@
import { useMemo } from '../lib/teact/teact';
import { debounce } from '../util/schedulers';
export default function useDebounce(ms: number, noFirst?: boolean, noLast?: boolean) {
return useMemo(() => {
return debounce((cb) => cb(), ms, !noFirst, !noLast);
}, [ms, noFirst, noLast]);
}

View File

@ -0,0 +1,18 @@
import { useCallback, useMemo } from '../lib/teact/teact';
import { debounce } from '../util/schedulers';
export default function useDebouncedCallback<T extends AnyToVoidFunction>(
fn: T,
deps: any[],
ms: number,
noFirst?: boolean,
noLast?: boolean,
) {
// eslint-disable-next-line react-hooks/exhaustive-deps
const fnMemo = useCallback(fn, deps);
return useMemo(() => {
return debounce(fnMemo, ms, !noFirst, !noLast);
}, [fnMemo, ms, noFirst, noLast]);
}

View File

@ -1,21 +1,20 @@
import { useState } from '../lib/teact/teact'; import { useCallback, useRef, useState } from '../lib/teact/teact';
import useDebounce from './useDebounce'; import useRunDebounced from './useRunDebounced';
import useOnChange from './useOnChange'; import useOnChange from './useOnChange';
import useHeavyAnimationCheck from './useHeavyAnimationCheck'; import useHeavyAnimationCheck, { isHeavyAnimating } from './useHeavyAnimationCheck';
import useFlag from './useFlag'; import useForceUpdate from './useForceUpdate';
export default function useDebouncedMemo<R extends any, D extends any[]>( export default function useDebouncedMemo<R extends any, D extends any[]>(
resolverFn: () => R, ms: number, dependencies: D, resolverFn: () => R, ms: number, dependencies: D,
): R | undefined { ): R | undefined {
const runDebounced = useDebounce(ms, true);
const [value, setValue] = useState<R>(); const [value, setValue] = useState<R>();
const [isFrozen, freeze, unfreeze] = useFlag(); const { isFrozen, updateWhenUnfrozen } = useHeavyAnimationFreeze();
const runDebounced = useRunDebounced(ms, true);
useHeavyAnimationCheck(freeze, unfreeze);
useOnChange(() => { useOnChange(() => {
if (isFrozen) { if (isFrozen) {
updateWhenUnfrozen();
return; return;
} }
@ -26,3 +25,30 @@ export default function useDebouncedMemo<R extends any, D extends any[]>(
return value; return value;
} }
function useHeavyAnimationFreeze() {
const isPending = useRef(false);
const updateWhenUnfrozen = useCallback(() => {
isPending.current = true;
}, []);
const forceUpdate = useForceUpdate();
const handleUnfreeze = useCallback(() => {
if (!isPending.current) {
return;
}
isPending.current = false;
forceUpdate();
}, [forceUpdate]);
useHeavyAnimationCheck(noop, handleUnfreeze);
return {
isFrozen: isHeavyAnimating(),
updateWhenUnfrozen,
};
}
function noop() {
}

View File

@ -0,0 +1,7 @@
import useDebouncedCallback from './useDebouncedCallback';
export default function useRunDebounced(ms: number, noFirst?: boolean, noLast?: boolean) {
return useDebouncedCallback((cb: NoneToVoidFunction) => {
cb();
}, [], ms, noFirst, noLast);
}

View File

@ -0,0 +1,7 @@
import useThrottledCallback from './useThrottledCallback';
export default function useRunThrottled(ms: number, noFirst?: boolean) {
return useThrottledCallback((cb: NoneToVoidFunction) => {
cb();
}, [], ms, noFirst);
}

View File

@ -60,6 +60,8 @@ const useShowTransition = (
return { return {
shouldRender, shouldRender,
transitionClassNames, transitionClassNames,
hasShownClass: shouldRender,
hasOpenClass: shouldHaveOpenClassName,
}; };
}; };

View File

@ -1,11 +0,0 @@
import { useMemo } from '../lib/teact/teact';
import { throttle } from '../util/schedulers';
const useThrottle = (ms: number, noFirst = false) => {
return useMemo(() => {
return throttle((cb) => cb(), ms, !noFirst);
}, [ms, noFirst]);
};
export default useThrottle;

View File

@ -0,0 +1,17 @@
import { useCallback, useMemo } from '../lib/teact/teact';
import { throttle } from '../util/schedulers';
export default function useThrottledCallback<T extends AnyToVoidFunction>(
fn: T,
deps: any[],
ms: number,
noFirst?: boolean,
) {
// eslint-disable-next-line react-hooks/exhaustive-deps
const fnMemo = useCallback(fn, deps);
return useMemo(() => {
return throttle(fnMemo, ms, !noFirst);
}, [fnMemo, ms, noFirst]);
}

View File

@ -1,28 +0,0 @@
import { useState } from '../lib/teact/teact';
import useThrottle from './useThrottle';
import useOnChange from './useOnChange';
import useHeavyAnimationCheck from './useHeavyAnimationCheck';
import useFlag from './useFlag';
export default function useThrottledMemo<R extends any, D extends any[]>(
resolverFn: () => R, ms: number, dependencies: D,
): R | undefined {
const runThrottled = useThrottle(ms, true);
const [value, setValue] = useState<R>();
const [isFrozen, freeze, unfreeze] = useFlag();
useHeavyAnimationCheck(freeze, unfreeze);
useOnChange(() => {
if (isFrozen) {
return;
}
runThrottled(() => {
setValue(resolverFn());
});
}, [...dependencies, isFrozen]);
return value;
}

View File

@ -464,9 +464,7 @@ export function setTarget($element: VirtualElement, target: Node) {
} }
} }
export function useState<T>(): [T, StateHookSetter<T>]; export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter<T>] {
export function useState<T>(initial: T): [T, StateHookSetter<T>];
export function useState<T>(initial?: T): [T, StateHookSetter<T>] {
const { cursor, byCursor } = renderingInstance.hooks.state; const { cursor, byCursor } = renderingInstance.hooks.state;
if (byCursor[cursor] === undefined) { if (byCursor[cursor] === undefined) {
@ -501,7 +499,9 @@ export function useState<T>(initial?: T): [T, StateHookSetter<T>] {
componentInstance.Component && (componentInstance.Component as FC_withDebug).DEBUG_contentComponentName componentInstance.Component && (componentInstance.Component as FC_withDebug).DEBUG_contentComponentName
? `> ${(componentInstance.Component as FC_withDebug).DEBUG_contentComponentName}` ? `> ${(componentInstance.Component as FC_withDebug).DEBUG_contentComponentName}`
: '', : '',
`Forced update at cursor #${cursor}, next value: `, debugKey
? `State update for ${debugKey}, next value: `
: `State update at cursor #${cursor}, next value: `,
byCursor[cursor].nextValue, byCursor[cursor].nextValue,
); );
} }

View File

@ -1,6 +1,6 @@
// @optimization // @optimization
@mixin while-transition() { @mixin while-transition() {
.Transition > div:not(.Transition__slide--active) &, body.animating-right-column & { .Transition > div:not(.Transition__slide--active) & {
@content; @content;
} }
} }

View File

@ -1,6 +1,6 @@
export default function findInViewport( export default function findInViewport(
container: HTMLElement, container: HTMLElement,
selectorOrElements: string | NodeListOf<HTMLElement>, selectorOrElements: string | NodeListOf<HTMLElement> | HTMLElement[],
margin = 0, margin = 0,
isDense = false, isDense = false,
shouldContainBottom = false, shouldContainBottom = false,