From 4d448e4d070aab9363597a7d649259aaba098dd9 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 19 Apr 2022 15:12:04 +0200 Subject: [PATCH] [Refactoring] Main: Simplify column animation --- .../calls/group/GroupCallParticipantMenu.tsx | 4 +- .../left/settings/SettingsNotifications.tsx | 4 +- src/components/main/DownloadManager.tsx | 8 +- src/components/main/Main.scss | 29 +++--- src/components/main/Main.tsx | 94 +++++++++++-------- .../mediaViewer/MediaViewerSlides.tsx | 37 ++++---- src/components/middle/MessageList.scss | 6 +- src/components/middle/MiddleHeader.scss | 2 +- .../middle/composer/SymbolMenu.scss | 2 +- src/components/middle/composer/SymbolMenu.tsx | 2 +- src/components/middle/hooks/useStickyDates.ts | 4 +- src/components/right/RightColumn.scss | 3 +- src/components/right/RightColumn.tsx | 7 +- src/components/right/RightHeader.tsx | 14 +-- src/hooks/useDebounce.ts | 9 -- src/hooks/useDebouncedCallback.ts | 18 ++++ src/hooks/useDebouncedMemo.ts | 42 +++++++-- src/hooks/useRunDebounced.ts | 7 ++ src/hooks/useRunThrottled.ts | 7 ++ src/hooks/useShowTransition.ts | 2 + src/hooks/useThrottle.ts | 11 --- src/hooks/useThrottledCallback.ts | 17 ++++ src/hooks/useThrottledMemo.ts | 28 ------ src/lib/teact/teact.ts | 8 +- src/styles/_mixins.scss | 2 +- src/util/findInViewport.ts | 2 +- 26 files changed, 208 insertions(+), 161 deletions(-) delete mode 100644 src/hooks/useDebounce.ts create mode 100644 src/hooks/useDebouncedCallback.ts create mode 100644 src/hooks/useRunDebounced.ts create mode 100644 src/hooks/useRunThrottled.ts delete mode 100644 src/hooks/useThrottle.ts create mode 100644 src/hooks/useThrottledCallback.ts delete mode 100644 src/hooks/useThrottledMemo.ts diff --git a/src/components/calls/group/GroupCallParticipantMenu.tsx b/src/components/calls/group/GroupCallParticipantMenu.tsx index 45c1de0f5..f1621dcdd 100644 --- a/src/components/calls/group/GroupCallParticipantMenu.tsx +++ b/src/components/calls/group/GroupCallParticipantMenu.tsx @@ -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 = ({ // 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) => { e.stopPropagation(); diff --git a/src/components/left/settings/SettingsNotifications.tsx b/src/components/left/settings/SettingsNotifications.tsx index 0b43ca009..a9169fe70 100644 --- a/src/components/left/settings/SettingsNotifications.tsx +++ b/src/components/left/settings/SettingsNotifications.tsx @@ -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 = ({ loadNotificationSettings(); }, [loadNotificationSettings]); - const runDebounced = useDebounce(500, true); + const runDebounced = useRunDebounced(500, true); const handleSettingsChange = useCallback(( e: ChangeEvent, diff --git a/src/components/main/DownloadManager.tsx b/src/components/main/DownloadManager.tsx index a4abc9599..d44067a9a 100644 --- a/src/components/main/DownloadManager.tsx +++ b/src/components/main/DownloadManager.tsx @@ -12,7 +12,7 @@ import { getMessageContentFilename, getMessageMediaHash, } from '../../global/helpers'; -import useDebounce from '../../hooks/useDebounce'; +import useRunDebounced from '../../hooks/useRunDebounced'; type StateProps = { activeDownloads: Record; @@ -33,17 +33,17 @@ const DownloadManager: FC = ({ }) => { 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]) => ( diff --git a/src/components/main/Main.scss b/src/components/main/Main.scss index ccf731ce2..c542bb029 100644 --- a/src/components/main/Main.scss +++ b/src/components/main/Main.scss @@ -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; } diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 395af9aa4..c58f85b35 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -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 = ({ connectionState, authState, lastSyncTime, - isLeftColumnShown, - isRightColumnShown, + isLeftColumnOpen, + isRightColumnOpen, isMediaViewerOpen, isForwardModalOpen, hasNotifications, @@ -224,51 +226,69 @@ const Main: FC = ({ }; }, [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), diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index 3a754bf55..7e6662230 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -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 = ({ 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) => { if (!IS_TOUCH_ENV) return; @@ -156,10 +156,8 @@ const MediaViewerSlides: FC = ({ 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 = ({ 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 = ({ // 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 = ({ return undefined; }, }); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isZoomed, onClose, setTransform, getMessageId, activeMessageId, - setIsActive, + selectMessageDebounced, + setIsActiveDebounced, + clearSwipeDirectionDebounced, ]); if (!activeMessageId) return undefined; diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index a753819bc..5cb572c93 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -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 { diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index 88d98bcd1..f98c4f42c 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -185,7 +185,7 @@ } // @optimization - @include while-transition() { + #Main.right-column-animating & { pointer-events: none; } } diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index b2d49e342..b644b6977 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -31,7 +31,7 @@ transition: none; } - &:not(.middle-column-open) { + &.left-column-open { transform: translate3d(100vw, 0, 0) !important; } } diff --git a/src/components/middle/composer/SymbolMenu.tsx b/src/components/middle/composer/SymbolMenu.tsx index 4348c3e8c..7cb502939 100644 --- a/src/components/middle/composer/SymbolMenu.tsx +++ b/src/components/middle/composer/SymbolMenu.tsx @@ -211,7 +211,7 @@ const SymbolMenu: FC = ({ const className = buildClassName( 'SymbolMenu mobile-menu', transitionClassNames, - !isLeftColumnShown && 'middle-column-open', + isLeftColumnShown && 'left-column-open', ); return ( diff --git a/src/components/middle/hooks/useStickyDates.ts b/src/components/middle/hooks/useStickyDates.ts index 84135b875..93ecd9b70 100644 --- a/src/components/middle/hooks/useStickyDates.ts +++ b/src/components/middle/hooks/useStickyDates.ts @@ -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(); diff --git a/src/components/right/RightColumn.scss b/src/components/right/RightColumn.scss index ed9a1fa77..c2a44eb7e 100644 --- a/src/components/right/RightColumn.scss +++ b/src/components/right/RightColumn.scss @@ -114,8 +114,7 @@ } } - #Main.right-column-open &, - body.animating-right-column & { + #Main.right-column-shown & { visibility: visible; } diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index 97af28423..dafdb5751 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -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 = ({ useEffect(() => { setTimeout(() => { setShouldSkipTransition(!isOpen); - }, CLOSE_ANIMATION_DURATION); + }, ANIMATION_DURATION); }, [isOpen]); useEffect(() => { @@ -300,7 +300,6 @@ const RightColumn: FC = ({ profileState={profileState} managementScreen={managementScreen} onClose={close} - shouldSkipAnimation={shouldSkipTransition || shouldSkipHistoryAnimations} onScreenSelect={setManagementScreen} /> 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 = ({ messageSearchQuery, stickerSearchQuery, gifSearchQuery, - shouldSkipAnimation, isEditingInvite, canViewStatistics, currentInviteInfo, + shouldSkipHistoryAnimations, }) => { const { setLocalTextSearchQuery, @@ -179,7 +180,7 @@ const RightHeader: FC = ({ useEffect(() => { setTimeout(() => { setShouldSkipTransition(!isColumnOpen); - }, COLUMN_CLOSE_DELAY_MS); + }, COLUMN_ANIMATION_DURATION); }, [isColumnOpen]); const lang = useLang(); @@ -436,7 +437,7 @@ const RightHeader: FC = ({ 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 = ({
{renderHeaderContent()} @@ -495,6 +496,7 @@ export default memo(withGlobal( gifSearchQuery, isEditingInvite, currentInviteInfo, + shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, }; }, )(RightHeader)); diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts deleted file mode 100644 index 1ea87deff..000000000 --- a/src/hooks/useDebounce.ts +++ /dev/null @@ -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]); -} diff --git a/src/hooks/useDebouncedCallback.ts b/src/hooks/useDebouncedCallback.ts new file mode 100644 index 000000000..517a00a99 --- /dev/null +++ b/src/hooks/useDebouncedCallback.ts @@ -0,0 +1,18 @@ +import { useCallback, useMemo } from '../lib/teact/teact'; + +import { debounce } from '../util/schedulers'; + +export default function useDebouncedCallback( + 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]); +} diff --git a/src/hooks/useDebouncedMemo.ts b/src/hooks/useDebouncedMemo.ts index 168e08926..1767840de 100644 --- a/src/hooks/useDebouncedMemo.ts +++ b/src/hooks/useDebouncedMemo.ts @@ -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( resolverFn: () => R, ms: number, dependencies: D, ): R | undefined { - const runDebounced = useDebounce(ms, true); const [value, setValue] = useState(); - 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( 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() { +} diff --git a/src/hooks/useRunDebounced.ts b/src/hooks/useRunDebounced.ts new file mode 100644 index 000000000..432c464b2 --- /dev/null +++ b/src/hooks/useRunDebounced.ts @@ -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); +} diff --git a/src/hooks/useRunThrottled.ts b/src/hooks/useRunThrottled.ts new file mode 100644 index 000000000..f9c343c86 --- /dev/null +++ b/src/hooks/useRunThrottled.ts @@ -0,0 +1,7 @@ +import useThrottledCallback from './useThrottledCallback'; + +export default function useRunThrottled(ms: number, noFirst?: boolean) { + return useThrottledCallback((cb: NoneToVoidFunction) => { + cb(); + }, [], ms, noFirst); +} diff --git a/src/hooks/useShowTransition.ts b/src/hooks/useShowTransition.ts index 5e18259c1..b890bcba0 100644 --- a/src/hooks/useShowTransition.ts +++ b/src/hooks/useShowTransition.ts @@ -60,6 +60,8 @@ const useShowTransition = ( return { shouldRender, transitionClassNames, + hasShownClass: shouldRender, + hasOpenClass: shouldHaveOpenClassName, }; }; diff --git a/src/hooks/useThrottle.ts b/src/hooks/useThrottle.ts deleted file mode 100644 index 87442755c..000000000 --- a/src/hooks/useThrottle.ts +++ /dev/null @@ -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; diff --git a/src/hooks/useThrottledCallback.ts b/src/hooks/useThrottledCallback.ts new file mode 100644 index 000000000..f9f651ea6 --- /dev/null +++ b/src/hooks/useThrottledCallback.ts @@ -0,0 +1,17 @@ +import { useCallback, useMemo } from '../lib/teact/teact'; + +import { throttle } from '../util/schedulers'; + +export default function useThrottledCallback( + 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]); +} diff --git a/src/hooks/useThrottledMemo.ts b/src/hooks/useThrottledMemo.ts deleted file mode 100644 index a849b8fa4..000000000 --- a/src/hooks/useThrottledMemo.ts +++ /dev/null @@ -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( - resolverFn: () => R, ms: number, dependencies: D, -): R | undefined { - const runThrottled = useThrottle(ms, true); - const [value, setValue] = useState(); - const [isFrozen, freeze, unfreeze] = useFlag(); - - useHeavyAnimationCheck(freeze, unfreeze); - - useOnChange(() => { - if (isFrozen) { - return; - } - - runThrottled(() => { - setValue(resolverFn()); - }); - }, [...dependencies, isFrozen]); - - return value; -} diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index a42ea8f8d..da0e3b964 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -464,9 +464,7 @@ export function setTarget($element: VirtualElement, target: Node) { } } -export function useState(): [T, StateHookSetter]; -export function useState(initial: T): [T, StateHookSetter]; -export function useState(initial?: T): [T, StateHookSetter] { +export function useState(initial?: T, debugKey?: string): [T, StateHookSetter] { const { cursor, byCursor } = renderingInstance.hooks.state; if (byCursor[cursor] === undefined) { @@ -501,7 +499,9 @@ export function useState(initial?: T): [T, StateHookSetter] { 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, ); } diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 57f3bcb40..c983c97e6 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -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; } } diff --git a/src/util/findInViewport.ts b/src/util/findInViewport.ts index 9d81f132d..a44a094bf 100644 --- a/src/util/findInViewport.ts +++ b/src/util/findInViewport.ts @@ -1,6 +1,6 @@ export default function findInViewport( container: HTMLElement, - selectorOrElements: string | NodeListOf, + selectorOrElements: string | NodeListOf | HTMLElement[], margin = 0, isDense = false, shouldContainBottom = false,