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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { useCallback } from '../../../lib/teact/teact';
import { fastRaf } from '../../../util/schedulers';
import useDebounce from '../../../hooks/useDebounce';
import useRunDebounced from '../../../hooks/useRunDebounced';
import useFlag from '../../../hooks/useFlag';
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.
const [isScrolled, markIsScrolled] = useFlag(false);
const runDebounced = useDebounce(DEBOUNCE, true);
const runDebounced = useRunDebounced(DEBOUNCE, true);
const updateStickyDates = useCallback((container: HTMLDivElement, hasTools?: boolean) => {
markIsScrolled();

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../global';
import { ManagementScreens, ProfileState } from '../../types';
import { ApiExportedInvite } from '../../api/types';
import { ANIMATION_END_DELAY } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { debounce } from '../../util/schedulers';
import buildClassName from '../../util/buildClassName';
@ -43,7 +44,6 @@ type OwnProps = {
isGifSearch?: boolean;
isPollResults?: boolean;
isAddingChatMembers?: boolean;
shouldSkipAnimation?: boolean;
profileState?: ProfileState;
managementScreen?: ManagementScreens;
onClose: () => void;
@ -61,9 +61,10 @@ type StateProps = {
gifSearchQuery?: string;
isEditingInvite?: boolean;
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);
enum HeaderContent {
@ -121,10 +122,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
messageSearchQuery,
stickerSearchQuery,
gifSearchQuery,
shouldSkipAnimation,
isEditingInvite,
canViewStatistics,
currentInviteInfo,
shouldSkipHistoryAnimations,
}) => {
const {
setLocalTextSearchQuery,
@ -179,7 +180,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
useEffect(() => {
setTimeout(() => {
setShouldSkipTransition(!isColumnOpen);
}, COLUMN_CLOSE_DELAY_MS);
}, COLUMN_ANIMATION_DURATION);
}, [isColumnOpen]);
const lang = useLang();
@ -436,7 +437,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const buttonClassName = buildClassName(
'animated-close-icon',
isBackButton && 'state-back',
(shouldSkipTransition || shouldSkipAnimation) && 'no-transition',
(shouldSkipTransition || shouldSkipHistoryAnimations) && 'no-transition',
);
return (
@ -452,7 +453,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
<div ref={backButtonRef} className={buttonClassName} />
</Button>
<Transition
name={(shouldSkipTransition || shouldSkipAnimation) ? 'none' : 'slide-fade'}
name={(shouldSkipTransition || shouldSkipHistoryAnimations) ? 'none' : 'slide-fade'}
activeKey={renderingContentKey}
>
{renderHeaderContent()}
@ -495,6 +496,7 @@ export default memo(withGlobal<OwnProps>(
gifSearchQuery,
isEditingInvite,
currentInviteInfo,
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
};
},
)(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 useHeavyAnimationCheck from './useHeavyAnimationCheck';
import useFlag from './useFlag';
import useHeavyAnimationCheck, { isHeavyAnimating } from './useHeavyAnimationCheck';
import useForceUpdate from './useForceUpdate';
export default function useDebouncedMemo<R extends any, D extends any[]>(
resolverFn: () => R, ms: number, dependencies: D,
): R | undefined {
const runDebounced = useDebounce(ms, true);
const [value, setValue] = useState<R>();
const [isFrozen, freeze, unfreeze] = useFlag();
useHeavyAnimationCheck(freeze, unfreeze);
const { isFrozen, updateWhenUnfrozen } = useHeavyAnimationFreeze();
const runDebounced = useRunDebounced(ms, true);
useOnChange(() => {
if (isFrozen) {
updateWhenUnfrozen();
return;
}
@ -26,3 +25,30 @@ export default function useDebouncedMemo<R extends any, D extends any[]>(
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 {
shouldRender,
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): [T, StateHookSetter<T>];
export function useState<T>(initial?: T): [T, StateHookSetter<T>] {
export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter<T>] {
const { cursor, byCursor } = renderingInstance.hooks.state;
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 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,
);
}

View File

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

View File

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