Message List: Fixes and refactoring of scroll animation

This commit is contained in:
Alexander Zinchuk 2023-04-23 18:33:12 +04:00
parent 0ab0c13d87
commit 83557863b8
22 changed files with 219 additions and 188 deletions

View File

@ -23,9 +23,9 @@ import {
} from '../../config'; } from '../../config';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import fastSmoothScroll from '../../util/fastSmoothScroll'; import animateScroll from '../../util/animateScroll';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal'; import animateHorizontalScroll from '../../util/animateHorizontalScroll';
import { pickTruthy, unique } from '../../util/iteratees'; import { pickTruthy, unique } from '../../util/iteratees';
import { isSameReaction } from '../../global/helpers'; import { isSameReaction } from '../../global/helpers';
import { import {
@ -275,13 +275,13 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2); const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2);
fastSmoothScrollHorizontal(header, newLeft); animateHorizontalScroll(header, newLeft);
}, [areAddedLoaded, activeSetIndex]); }, [areAddedLoaded, activeSetIndex]);
const selectStickerSet = useCallback((index: number) => { const selectStickerSet = useCallback((index: number) => {
setActiveSetIndex(index); setActiveSetIndex(index);
const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!; const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!;
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE); animateScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
}, [idPrefix]); }, [idPrefix]);
const handleEmojiSelect = useCallback((emoji: ApiSticker) => { const handleEmojiSelect = useCallback((emoji: ApiSticker) => {

View File

@ -8,7 +8,7 @@ import type { ApiLimitType, GlobalState } from '../../../global/types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
@ -185,7 +185,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
const index = PREMIUM_FEATURE_SECTIONS.indexOf(initialSection); const index = PREMIUM_FEATURE_SECTIONS.indexOf(initialSection);
setCurrentSlideIndex(index); setCurrentSlideIndex(index);
startScrolling(); startScrolling();
fastSmoothScrollHorizontal(scrollContainer, scrollContainer.clientWidth * index, 0) animateHorizontalScroll(scrollContainer, scrollContainer.clientWidth * index, 0)
.then(stopScrolling); .then(stopScrolling);
}, [currentSlideIndex, initialSection, prevInitialSection, startScrolling, stopScrolling]); }, [currentSlideIndex, initialSection, prevInitialSection, startScrolling, stopScrolling]);
@ -196,7 +196,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
setCurrentSlideIndex(index); setCurrentSlideIndex(index);
startScrolling(); startScrolling();
await fastSmoothScrollHorizontal(scrollContainer, scrollContainer.clientWidth * index, 800); await animateHorizontalScroll(scrollContainer, scrollContainer.clientWidth * index, 800);
stopScrolling(); stopScrolling();
}, [startScrolling, stopScrolling]); }, [startScrolling, stopScrolling]);

View File

@ -18,7 +18,6 @@ import {
selectChat, selectChat,
selectTopicFromMessage, selectTopicFromMessage,
selectTabState, selectTabState,
selectCurrentMessageIds,
} from '../../global/selectors'; } from '../../global/selectors';
import { getMessageHtmlId, isChatChannel } from '../../global/helpers'; import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -46,6 +45,7 @@ type OwnProps = {
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
isEmbedded?: boolean; isEmbedded?: boolean;
appearanceOrder?: number; appearanceOrder?: number;
isJustAdded?: boolean;
isLastInList?: boolean; isLastInList?: boolean;
isInsideTopic?: boolean; isInsideTopic?: boolean;
memoFirstUnreadIdRef?: { current: number | undefined }; memoFirstUnreadIdRef?: { current: number | undefined };
@ -63,7 +63,6 @@ type StateProps = {
topic?: ApiTopic; topic?: ApiTopic;
focusDirection?: FocusDirection; focusDirection?: FocusDirection;
noFocusHighlight?: boolean; noFocusHighlight?: boolean;
viewportIds?: number[];
premiumGiftSticker?: ApiSticker; premiumGiftSticker?: ApiSticker;
}; };
@ -73,6 +72,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
message, message,
isEmbedded, isEmbedded,
appearanceOrder = 0, appearanceOrder = 0,
isJustAdded,
isLastInList, isLastInList,
usersById, usersById,
senderUser, senderUser,
@ -83,7 +83,6 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
isFocused, isFocused,
focusDirection, focusDirection,
noFocusHighlight, noFocusHighlight,
viewportIds,
premiumGiftSticker, premiumGiftSticker,
isInsideTopic, isInsideTopic,
topic, topic,
@ -102,7 +101,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
useOnIntersect(ref, observeIntersectionForReading); useOnIntersect(ref, observeIntersectionForReading);
useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage); useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage);
useFocusMessage(ref, message.id, message.chatId, isFocused, focusDirection, noFocusHighlight, viewportIds); useFocusMessage(ref, message.id, message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded);
useEffect(() => { useEffect(() => {
if (!message.isPinned) return undefined; if (!message.isPinned) return undefined;
@ -252,7 +251,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { message, threadId, messageListType }): StateProps => { (global, { message, threadId }): StateProps => {
const { const {
chatId, senderId, replyToMessageId, content, chatId, senderId, replyToMessageId, content,
} = message; } = message;
@ -291,9 +290,6 @@ export default memo(withGlobal<OwnProps>(
...(isFocused && { ...(isFocused && {
focusDirection, focusDirection,
noFocusHighlight, noFocusHighlight,
viewportIds: threadId && messageListType
? selectCurrentMessageIds(global, chatId, threadId, messageListType)
: undefined,
}), }),
}; };
}, },

View File

@ -9,7 +9,7 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { selectChat, selectCurrentMessageList } from '../../global/selectors'; import { selectChat, selectCurrentMessageList } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import fastSmoothScroll from '../../util/fastSmoothScroll'; import animateScroll from '../../util/animateScroll';
import ScrollDownButton from './ScrollDownButton'; import ScrollDownButton from './ScrollDownButton';
@ -79,7 +79,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
return; return;
} }
fastSmoothScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN); animateScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN);
} }
}, [isShown, messageListType, focusNextReply]); }, [isShown, messageListType, focusNextReply]);

View File

@ -125,6 +125,17 @@
opacity: 0; opacity: 0;
} }
body.animation-level-0 & {
opacity: 1;
transform: none;
display: flex !important;
transition: none !important;
}
&.is-just-added:not(.own) {
transform: none;
}
// Restore stacking context // Restore stacking context
// https://developer.mozilla.org/ru/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context // https://developer.mozilla.org/ru/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
&.open.shown { &.open.shown {

View File

@ -5,6 +5,7 @@ import React, {
useMemo, useMemo,
useRef, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
import { requestForcedReflow, forceMeasure, requestMeasure } from '../../lib/fasterdom/fasterdom'; import { requestForcedReflow, forceMeasure, requestMeasure } from '../../lib/fasterdom/fasterdom';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
@ -58,7 +59,7 @@ import buildClassName from '../../util/buildClassName';
import { groupMessages } from './helpers/groupMessages'; import { groupMessages } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import resetScroll from '../../util/resetScroll'; import resetScroll from '../../util/resetScroll';
import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import { useStateRef } from '../../hooks/useStateRef'; import { useStateRef } from '../../hooks/useStateRef';
@ -397,40 +398,50 @@ const MessageList: FC<OwnProps & StateProps> = ({
// Handles updated message list, takes care of scroll repositioning // Handles updated message list, takes care of scroll repositioning
useLayoutEffectWithPrevDeps(([prevMessageIds, prevIsViewportNewest]) => { useLayoutEffectWithPrevDeps(([prevMessageIds, prevIsViewportNewest]) => {
if (process.env.APP_ENV === 'perf') {
// eslint-disable-next-line no-console
console.time('scrollTop');
}
const containerHeight = getContainerHeight(); const containerHeight = getContainerHeight();
const prevContainerHeight = prevContainerHeightRef.current; const prevContainerHeight = prevContainerHeightRef.current;
prevContainerHeightRef.current = containerHeight; prevContainerHeightRef.current = containerHeight;
const container = containerRef.current!;
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
const hasLastMessageChanged = (
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
);
const hasViewportShifted = (
messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1)
);
const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted;
// Add extra height when few messages to allow scroll animation
if (
isViewportNewest
&& wasMessageAdded
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
&& !container.parentElement!.classList.contains('force-messages-scroll')
&& forceMeasure(() => (
(container.firstElementChild as HTMLDivElement)!.clientHeight <= container.offsetHeight * 2
))
) {
addExtraClass(container.parentElement!, 'force-messages-scroll');
container.parentElement!.classList.add('force-messages-scroll');
setTimeout(() => {
if (container.parentElement) {
removeExtraClass(container.parentElement!, 'force-messages-scroll');
}
}, MESSAGE_ANIMATION_DURATION);
}
requestForcedReflow(() => { requestForcedReflow(() => {
const container = containerRef.current!; const { scrollTop, scrollHeight, offsetHeight } = container;
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
const hasLastMessageChanged = (
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
);
const hasViewportShifted = (
messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1)
);
const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted;
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
// Add extra height when few messages to allow smooth scroll animation. Uses assumption that `parentElement`
// is a Transition slide and its CSS class can not be reset in a declarative way.
const shouldForceScroll = (
isViewportNewest
&& wasMessageAdded
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
&& !container.parentElement!.classList.contains('force-messages-scroll')
&& (container.firstElementChild as HTMLDivElement)!.clientHeight <= container.offsetHeight * 2
);
const {
scrollTop,
scrollHeight,
offsetHeight,
} = container;
const scrollOffset = scrollOffsetRef.current; const scrollOffset = scrollOffsetRef.current;
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight); let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
if (wasMessageAdded) { if (wasMessageAdded) {
@ -441,34 +452,19 @@ const MessageList: FC<OwnProps & StateProps> = ({
bottomOffset -= lastItemHeight; bottomOffset -= lastItemHeight;
} }
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD; const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
let newScrollTop!: number; // Animate incoming message
if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) { if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) {
if (lastItemElement) { // Break out of `forceLayout`
// Break out of `forceLayout` requestMeasure(() => {
requestMeasure(() => { animateScroll(
fastSmoothScroll( container,
container, lastItemElement!,
lastItemElement, 'end',
'end', BOTTOM_FOCUS_MARGIN,
BOTTOM_FOCUS_MARGIN, );
); });
});
}
newScrollTop = scrollHeight - offsetHeight;
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
// Scroll still needs to be restored after container resize
if (!shouldForceScroll) {
return undefined;
}
}
if (process.env.APP_ENV === 'perf') {
// eslint-disable-next-line no-console
console.time('scrollTop');
} }
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight; const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight;
@ -483,6 +479,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`) && container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
); );
let newScrollTop!: number;
if (isAtBottom && isResized) { if (isAtBottom && isResized) {
newScrollTop = scrollHeight - offsetHeight; newScrollTop = scrollHeight - offsetHeight;
} else if (anchor) { } else if (anchor) {
@ -498,17 +495,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
} }
return () => { return () => {
if (shouldForceScroll) {
container.parentElement!.classList.add('force-messages-scroll');
setTimeout(() => {
if (container.parentElement) {
container.parentElement.classList.remove('force-messages-scroll');
}
}, MESSAGE_ANIMATION_DURATION);
}
resetScroll(container, Math.ceil(newScrollTop)); resetScroll(container, Math.ceil(newScrollTop));
restartCurrentScrollAnimation();
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
if (!memoFocusingIdRef.current) { if (!memoFocusingIdRef.current) {
isScrollTopJustUpdatedRef.current = true; isScrollTopJustUpdatedRef.current = true;
@ -518,8 +508,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
}); });
} }
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
if (process.env.APP_ENV === 'perf') { if (process.env.APP_ENV === 'perf') {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.timeEnd('scrollTop'); console.timeEnd('scrollTop');

View File

@ -21,6 +21,7 @@ import { isAlbum } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import useScrollHooks from './hooks/useScrollHooks'; import useScrollHooks from './hooks/useScrollHooks';
import useMessageObservers from './hooks/useMessageObservers'; import useMessageObservers from './hooks/useMessageObservers';
import usePrevious from '../../hooks/usePrevious';
import Message from './message/Message'; import Message from './message/Message';
import SponsoredMessage from './message/SponsoredMessage'; import SponsoredMessage from './message/SponsoredMessage';
@ -114,12 +115,16 @@ const MessageListContent: FC<OwnProps> = ({
<span>{lang('UnreadMessages')}</span> <span>{lang('UnreadMessages')}</span>
</div> </div>
); );
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
return acc + messageGroup.senderGroups.flat().length; return acc + messageGroup.senderGroups.flat().length;
}, 0); }, 0);
let appearanceIndex = 0; let appearanceIndex = 0;
const prevMessageIds = usePrevious(messageIds);
const isNewMessage = Boolean(
messageIds && prevMessageIds && messageIds[messageIds.length - 2] === prevMessageIds[prevMessageIds.length - 1],
);
const dateGroups = messageGroups.map(( const dateGroups = messageGroups.map((
dateGroup: MessageDateGroup, dateGroup: MessageDateGroup,
dateGroupIndex: number, dateGroupIndex: number,
@ -155,6 +160,7 @@ const MessageListContent: FC<OwnProps> = ({
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
memoFirstUnreadIdRef={memoFirstUnreadIdRef} memoFirstUnreadIdRef={memoFirstUnreadIdRef}
appearanceOrder={messageCountToAnimate - ++appearanceIndex} appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isJustAdded={isLastInList && isNewMessage}
isLastInList={isLastInList} isLastInList={isLastInList}
onPinnedIntersectionChange={onPinnedIntersectionChange} onPinnedIntersectionChange={onPinnedIntersectionChange}
/>, />,
@ -219,6 +225,7 @@ const MessageListContent: FC<OwnProps> = ({
noComments={noComments} noComments={noComments}
noReplies={!noComments || threadId !== MAIN_THREAD_ID} noReplies={!noComments || threadId !== MAIN_THREAD_ID}
appearanceOrder={messageCountToAnimate - ++appearanceIndex} appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isJustAdded={position.isLastInList && isNewMessage}
isFirstInGroup={position.isFirstInGroup} isFirstInGroup={position.isFirstInGroup}
isLastInGroup={position.isLastInGroup} isLastInGroup={position.isLastInGroup}
isFirstInDocumentGroup={position.isFirstInDocumentGroup} isFirstInDocumentGroup={position.isFirstInDocumentGroup}

View File

@ -15,10 +15,10 @@ import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config'
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { uncompressEmoji } from '../../../util/emoji'; import { uncompressEmoji } from '../../../util/emoji';
import fastSmoothScroll from '../../../util/fastSmoothScroll'; import animateScroll from '../../../util/animateScroll';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
@ -122,7 +122,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
const newLeft = activeCategoryIndex * HEADER_BUTTON_WIDTH - header.offsetWidth / 2 + HEADER_BUTTON_WIDTH / 2; const newLeft = activeCategoryIndex * HEADER_BUTTON_WIDTH - header.offsetWidth / 2 + HEADER_BUTTON_WIDTH / 2;
fastSmoothScrollHorizontal(header, newLeft); animateHorizontalScroll(header, newLeft);
}, [categories, activeCategoryIndex]); }, [categories, activeCategoryIndex]);
const lang = useLang(); const lang = useLang();
@ -165,7 +165,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
setActiveCategoryIndex(index); setActiveCategoryIndex(index);
const categoryEl = containerRef.current!.closest<HTMLElement>('.SymbolMenu-main')! const categoryEl = containerRef.current!.closest<HTMLElement>('.SymbolMenu-main')!
.querySelector(`#emoji-category-${index}`)! as HTMLElement; .querySelector(`#emoji-category-${index}`)! as HTMLElement;
fastSmoothScroll(containerRef.current!, categoryEl, 'start', FOCUS_MARGIN, SMOOTH_SCROLL_DISTANCE); animateScroll(containerRef.current!, categoryEl, 'start', FOCUS_MARGIN, SMOOTH_SCROLL_DISTANCE);
}, []); }, []);
const handleEmojiSelect = useCallback((emoji: string, name: string) => { const handleEmojiSelect = useCallback((emoji: string, name: string) => {

View File

@ -8,7 +8,7 @@ import type { FC } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import findInViewport from '../../../util/findInViewport'; import findInViewport from '../../../util/findInViewport';
import isFullyVisible from '../../../util/isFullyVisible'; import isFullyVisible from '../../../util/isFullyVisible';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
@ -51,7 +51,7 @@ function setItemVisible(index: number, containerRef: Record<string, any>) {
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end'; const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
const newLeft = position === 'start' ? index * EMOJI_BUTTON_WIDTH : 0; const newLeft = position === 'start' ? index * EMOJI_BUTTON_WIDTH : 0;
fastSmoothScrollHorizontal(container, newLeft); animateHorizontalScroll(container, newLeft);
} }
} }

View File

@ -17,9 +17,9 @@ import {
} from '../../../config'; } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import fastSmoothScroll from '../../../util/fastSmoothScroll'; import animateScroll from '../../../util/animateScroll';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import { pickTruthy, uniqueByField } from '../../../util/iteratees'; import { pickTruthy, uniqueByField } from '../../../util/iteratees';
import { selectChat, selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors'; import { selectChat, selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
@ -212,13 +212,13 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2); const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2);
fastSmoothScrollHorizontal(header, newLeft); animateHorizontalScroll(header, newLeft);
}, [areAddedLoaded, activeSetIndex]); }, [areAddedLoaded, activeSetIndex]);
const selectStickerSet = useCallback((index: number) => { const selectStickerSet = useCallback((index: number) => {
setActiveSetIndex(index); setActiveSetIndex(index);
const stickerSetEl = document.getElementById(`sticker-set-${index}`)!; const stickerSetEl = document.getElementById(`sticker-set-${index}`)!;
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE); animateScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
}, []); }, []);
const handleStickerSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => { const handleStickerSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => {

View File

@ -21,7 +21,7 @@ import { useDebouncedSignal } from '../../../hooks/useAsyncResolvers';
const FAB_THRESHOLD = 50; const FAB_THRESHOLD = 50;
const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect
const CONTAINER_HEIGHT_DEBOUNCE = 100; const CONTAINER_HEIGHT_DEBOUNCE = 100;
const TOOLS_FREEZE_TIMEOUT = 250; // Approximate message sending animation duration const TOOLS_FREEZE_TIMEOUT = 350; // Approximate message sending animation duration
export default function useScrollHooks( export default function useScrollHooks(
type: MessageListType, type: MessageListType,

View File

@ -67,7 +67,7 @@ import {
selectTopicFromMessage, selectTopicFromMessage,
selectTabState, selectTabState,
selectChatTranslations, selectChatTranslations,
selectRequestedTranslationLanguage, selectCurrentMessageIds, selectRequestedTranslationLanguage,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { import {
getMessageContent, getMessageContent,
@ -109,7 +109,7 @@ import renderText from '../../common/helpers/renderText';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { isElementInViewport } from '../../../util/isElementInViewport'; import { isElementInViewport } from '../../../util/isElementInViewport';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji'; import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import { isAnimatingScroll } from '../../../util/fastSmoothScroll'; import { isAnimatingScroll } from '../../../util/animateScroll';
import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
@ -186,6 +186,7 @@ type OwnProps =
noComments: boolean; noComments: boolean;
noReplies: boolean; noReplies: boolean;
appearanceOrder: number; appearanceOrder: number;
isJustAdded: boolean;
memoFirstUnreadIdRef: { current: number | undefined }; memoFirstUnreadIdRef: { current: number | undefined };
onPinnedIntersectionChange: PinnedIntersectionChangedCallback; onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
} }
@ -212,7 +213,6 @@ type StateProps = {
focusDirection?: FocusDirection; focusDirection?: FocusDirection;
noFocusHighlight?: boolean; noFocusHighlight?: boolean;
isResizingContainer?: boolean; isResizingContainer?: boolean;
viewportIds?: number[];
isForwarding?: boolean; isForwarding?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isRepliesChat?: boolean; isRepliesChat?: boolean;
@ -293,6 +293,7 @@ const Message: FC<OwnProps & StateProps> = ({
noComments, noComments,
noReplies, noReplies,
appearanceOrder, appearanceOrder,
isJustAdded,
isFirstInGroup, isFirstInGroup,
isPremium, isPremium,
isLastInGroup, isLastInGroup,
@ -320,7 +321,6 @@ const Message: FC<OwnProps & StateProps> = ({
focusDirection, focusDirection,
noFocusHighlight, noFocusHighlight,
isResizingContainer, isResizingContainer,
viewportIds,
isForwarding, isForwarding,
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
@ -412,7 +412,12 @@ const Message: FC<OwnProps & StateProps> = ({
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY); setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
}, [appearanceOrder, markShown, noAppearanceAnimation]); }, [appearanceOrder, markShown, noAppearanceAnimation]);
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false); const { transitionClassNames } = useShowTransition(
isShown || isJustAdded,
undefined,
noAppearanceAnimation && !isJustAdded,
false,
);
const { const {
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError,
@ -576,6 +581,7 @@ const Message: FC<OwnProps & StateProps> = ({
Boolean(message.inlineButtons) && 'has-inline-buttons', Boolean(message.inlineButtons) && 'has-inline-buttons',
isSwiped && 'is-swiped', isSwiped && 'is-swiped',
transitionClassNames, transitionClassNames,
isJustAdded && 'is-just-added',
(Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction', (Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction',
); );
@ -658,7 +664,7 @@ const Message: FC<OwnProps & StateProps> = ({
); );
useFocusMessage( useFocusMessage(
ref, messageId, chatId, isFocused, focusDirection, noFocusHighlight, viewportIds, isResizingContainer, ref, messageId, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded,
); );
const signature = (isChannel && message.postAuthorTitle) const signature = (isChannel && message.postAuthorTitle)
@ -1480,7 +1486,6 @@ export default memo(withGlobal<OwnProps>(
focusDirection, focusDirection,
noFocusHighlight, noFocusHighlight,
isResizingContainer, isResizingContainer,
viewportIds: selectCurrentMessageIds(global, chatId, threadId, messageListType),
}), }),
}; };
}, },

View File

@ -1,8 +1,9 @@
import { useLayoutEffect, useMemo } from '../../../../lib/teact/teact'; import { useLayoutEffect, useRef } from '../../../../lib/teact/teact';
import { requestForcedReflow, requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
import type { FocusDirection } from '../../../../types'; import type { FocusDirection } from '../../../../types';
import fastSmoothScroll from '../../../../util/fastSmoothScroll'; import animateScroll from '../../../../util/animateScroll';
// This is used when the viewport was replaced. // This is used when the viewport was replaced.
const BOTTOM_FOCUS_OFFSET = 500; const BOTTOM_FOCUS_OFFSET = 500;
@ -16,38 +17,42 @@ export default function useFocusMessage(
isFocused?: boolean, isFocused?: boolean,
focusDirection?: FocusDirection, focusDirection?: FocusDirection,
noFocusHighlight?: boolean, noFocusHighlight?: boolean,
viewportIds?: number[],
isResizingContainer?: boolean, isResizingContainer?: boolean,
isJustAdded?: boolean,
) { ) {
const viewportIndex = useMemo(() => { const isRelocatedRef = useRef(!isJustAdded);
if (!viewportIds) {
return 0;
}
const index = viewportIds.indexOf(messageId);
return Math.min(index, viewportIds.length - index - 1);
}, [messageId, viewportIds]);
useLayoutEffect(() => { useLayoutEffect(() => {
const isRelocated = isRelocatedRef.current;
isRelocatedRef.current = false;
if (isFocused && elementRef.current) { if (isFocused && elementRef.current) {
const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList')!; const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList')!;
// `noFocusHighlight` is always called with “scroll-to-bottom” buttons // `noFocusHighlight` is always called with “scroll-to-bottom” buttons
const isToBottom = noFocusHighlight; const isToBottom = noFocusHighlight;
fastSmoothScroll( const exec = () => animateScroll(
messagesContainer, messagesContainer,
elementRef.current, elementRef.current!,
isToBottom ? 'end' : 'centerOrTop', isToBottom ? 'end' : 'centerOrTop',
FOCUS_MARGIN, FOCUS_MARGIN,
focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined, focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined,
focusDirection, focusDirection,
undefined, undefined,
isResizingContainer, isResizingContainer,
// We need this to override scroll setting from Message List layout effect
true, true,
); );
if (isRelocated) {
// We need this to override scroll setting from Message List layout effect
requestForcedReflow(exec);
} else {
requestMeasure(() => {
requestMutation(exec()!);
});
}
} }
}, [ }, [
elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, viewportIndex, elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer,
]); ]);
} }

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect } from '../../../lib/teact/teact';
import { ProfileState } from '../../../types'; import { ProfileState } from '../../../types';
import fastSmoothScroll from '../../../util/fastSmoothScroll'; import animateScroll from '../../../util/animateScroll';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
@ -27,7 +27,7 @@ export default function useProfileState(
if (container.scrollTop < tabsEl.offsetTop) { if (container.scrollTop < tabsEl.offsetTop) {
onProfileStateChange(tabType === 'members' ? ProfileState.MemberList : ProfileState.SharedMedia); onProfileStateChange(tabType === 'members' ? ProfileState.MemberList : ProfileState.SharedMedia);
isScrollingProgrammatically = true; isScrollingProgrammatically = true;
fastSmoothScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION); animateScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION);
setTimeout(() => { setTimeout(() => {
isScrollingProgrammatically = false; isScrollingProgrammatically = false;
}, PROGRAMMATIC_SCROLL_TIMEOUT_MS); }, PROGRAMMATIC_SCROLL_TIMEOUT_MS);
@ -52,7 +52,7 @@ export default function useProfileState(
} }
isScrollingProgrammatically = true; isScrollingProgrammatically = true;
fastSmoothScroll( animateScroll(
container, container,
container.firstElementChild as HTMLElement, container.firstElementChild as HTMLElement,
'start', 'start',

View File

@ -1,6 +1,6 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import type { FC, TeactNode } from '../../lib/teact/teact'; import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { useEffect, useRef } from '../../lib/teact/teact'; import React, { useCallback, useEffect, useRef } from '../../lib/teact/teact';
import type { TextPart } from '../../types'; import type { TextPart } from '../../types';
@ -78,9 +78,19 @@ const Modal: FC<OwnProps & StateProps> = ({
return enableDirectTextInput; return enableDirectTextInput;
}, [isOpen]); }, [isOpen]);
useEffect(() => (isOpen const handleEnter = useCallback((e: KeyboardEvent) => {
? captureKeyboardListeners({ onEsc: onClose, onEnter }) if (!onEnter) {
: undefined), [isOpen, onClose, onEnter]); return false;
}
e.preventDefault();
onEnter();
return true;
}, [onEnter]);
useEffect(() => (
isOpen ? captureKeyboardListeners({ onEsc: onClose, onEnter: handleEnter }) : undefined
), [isOpen, onClose, handleEnter]);
useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]); useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]);
useHistoryBack({ useHistoryBack({

View File

@ -3,7 +3,7 @@ import React, { memo, useRef, useEffect } from '../../lib/teact/teact';
import { ALL_FOLDER_ID } from '../../config'; import { ALL_FOLDER_ID } from '../../config';
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment'; import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal'; import animateHorizontalScroll from '../../util/animateHorizontalScroll';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import useHorizontalScroll from '../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../hooks/useHorizontalScroll';
@ -63,7 +63,7 @@ const TabList: FC<OwnProps> = ({
return; return;
} }
fastSmoothScrollHorizontal(container, newLeft, SCROLL_DURATION); animateHorizontalScroll(container, newLeft, SCROLL_DURATION);
}, [activeTab]); }, [activeTab]);
const lang = useLang(); const lang = useLang();

View File

@ -144,7 +144,7 @@ export const TMP_CHAT_ID = '0';
export const ANIMATION_END_DELAY = 100; export const ANIMATION_END_DELAY = 100;
export const FAST_SMOOTH_MIN_DURATION = 250; export const FAST_SMOOTH_MIN_DURATION = 300;
export const FAST_SMOOTH_MAX_DURATION = 600; export const FAST_SMOOTH_MAX_DURATION = 600;
export const FAST_SMOOTH_MAX_DISTANCE = 750; export const FAST_SMOOTH_MAX_DISTANCE = 750;
export const FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE = 300; // px export const FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE = 300; // px

View File

@ -89,7 +89,24 @@ export function useIntersectionObserver({
function initController() { function initController() {
const callbacks = new Map(); const callbacks = new Map();
const entriesAccumulator = new Map<Element, IntersectionObserverEntry>(); const entriesAccumulator = new Map<Element, IntersectionObserverEntry>();
const observerCallbackSync = () => {
let observerCallback: typeof observerCallbackSync;
if (typeof throttleScheduler === 'function') {
observerCallback = throttleWith(throttleScheduler, observerCallbackSync);
} else if (throttleMs) {
observerCallback = throttle(observerCallbackSync, throttleMs, !shouldSkipFirst);
} else if (debounceMs) {
observerCallback = debounce(observerCallbackSync, debounceMs, !shouldSkipFirst);
} else {
observerCallback = observerCallbackSync;
}
function observerCallbackSync() {
if (freezeFlagsRef.current) {
onUnfreezeRef.current = observerCallback;
return;
}
const entries = Array.from(entriesAccumulator.values()); const entries = Array.from(entriesAccumulator.values());
entries.forEach((entry: IntersectionObserverEntry) => { entries.forEach((entry: IntersectionObserverEntry) => {
@ -104,17 +121,6 @@ export function useIntersectionObserver({
} }
entriesAccumulator.clear(); entriesAccumulator.clear();
};
let observerCallback: typeof observerCallbackSync;
if (typeof throttleScheduler === 'function') {
observerCallback = throttleWith(throttleScheduler, observerCallbackSync);
} else if (throttleMs) {
observerCallback = throttle(observerCallbackSync, throttleMs, !shouldSkipFirst);
} else if (debounceMs) {
observerCallback = debounce(observerCallbackSync, debounceMs, !shouldSkipFirst);
} else {
observerCallback = observerCallbackSync;
} }
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
@ -124,9 +130,7 @@ export function useIntersectionObserver({
}); });
if (freezeFlagsRef.current) { if (freezeFlagsRef.current) {
onUnfreezeRef.current = () => { onUnfreezeRef.current = observerCallback;
observerCallback();
};
} else { } else {
observerCallback(); observerCallback();
} }

View File

@ -8,7 +8,7 @@ const DEFAULT_DURATION = 300;
const stopById: Map<string, VoidFunction> = new Map(); const stopById: Map<string, VoidFunction> = new Map();
export default function fastSmoothScrollHorizontal(container: HTMLElement, left: number, duration = DEFAULT_DURATION) { export default function animateHorizontalScroll(container: HTMLElement, left: number, duration = DEFAULT_DURATION) {
if (getGlobal().settings.byKey.animationLevel === ANIMATION_LEVEL_MIN) { if (getGlobal().settings.byKey.animationLevel === ANIMATION_LEVEL_MIN) {
duration = 0; duration = 0;
} }

View File

@ -1,3 +1,4 @@
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
import { getGlobal } from '../global'; import { getGlobal } from '../global';
import { FocusDirection } from '../types'; import { FocusDirection } from '../types';
@ -11,41 +12,40 @@ import {
} from '../config'; } from '../config';
import { IS_ANDROID } from './windowEnvironment'; import { IS_ANDROID } from './windowEnvironment';
import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck';
import { animateSingle } from './animation'; import { animateSingle, cancelSingleAnimation } from './animation';
import { requestForcedReflow, requestMutation } from '../lib/fasterdom/fasterdom';
type Params = Parameters<typeof createMutateFunction>;
let isAnimating = false; let isAnimating = false;
let currentArgs: Parameters<typeof createMutateFunction> | undefined;
export default function fastSmoothScroll( export default function animateScroll(...args: Params | [...Params, boolean]) {
container: HTMLElement, currentArgs = args.slice(0, 8) as Params;
element: HTMLElement,
position: ScrollLogicalPosition | 'centerOrTop',
margin = 0,
maxDistance = FAST_SMOOTH_MAX_DISTANCE,
forceDirection?: FocusDirection,
forceDuration?: number,
forceNormalContainerHeight?: boolean,
withForcedReflow = false,
) {
const args = [
container,
element,
position,
margin,
maxDistance,
forceDirection,
forceDuration,
forceNormalContainerHeight,
] as const;
if (withForcedReflow) { const mutate = createMutateFunction(...currentArgs);
requestForcedReflow(() => measure(...args));
} else { const shouldReturnMutationFn = args[8];
requestMutation(measure(...args)); if (shouldReturnMutationFn) {
return mutate;
} }
requestMutation(mutate);
return undefined;
} }
function measure( export function restartCurrentScrollAnimation() {
if (!isAnimating) {
return;
}
cancelSingleAnimation();
requestMeasure(() => {
requestMutation(createMutateFunction(...currentArgs!));
});
}
function createMutateFunction(
container: HTMLElement, container: HTMLElement,
element: HTMLElement, element: HTMLElement,
position: ScrollLogicalPosition | 'centerOrTop', position: ScrollLogicalPosition | 'centerOrTop',
@ -88,12 +88,7 @@ function measure(
const scrollFrom = calculateScrollFrom(container, scrollTo, maxDistance, forceDirection); const scrollFrom = calculateScrollFrom(container, scrollTo, maxDistance, forceDirection);
if (currentScrollTop !== scrollFrom) {
container.scrollTop = scrollFrom;
}
let path = scrollTo - scrollFrom; let path = scrollTo - scrollFrom;
if (path < 0) { if (path < 0) {
const remainingPath = -scrollFrom; const remainingPath = -scrollFrom;
path = Math.max(path, remainingPath); path = Math.max(path, remainingPath);
@ -102,12 +97,14 @@ function measure(
path = Math.min(path, remainingPath); path = Math.min(path, remainingPath);
} }
return () => { const absPath = Math.abs(path);
if (currentScrollTop !== scrollFrom) {
container.scrollTop = scrollFrom; return () => {
} if (absPath < 1) {
if (currentScrollTop !== scrollFrom) {
container.scrollTop = scrollFrom;
}
if (path === 0) {
return; return;
} }
@ -120,7 +117,6 @@ function measure(
isAnimating = true; isAnimating = true;
const absPath = Math.abs(path);
const transition = absPath <= FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition; const transition = absPath <= FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition;
const duration = forceDuration || ( const duration = forceDuration || (
FAST_SMOOTH_MIN_DURATION FAST_SMOOTH_MIN_DURATION
@ -132,12 +128,14 @@ function measure(
animateSingle(() => { animateSingle(() => {
const t = Math.min((Date.now() - startAt) / duration, 1); const t = Math.min((Date.now() - startAt) / duration, 1);
const currentPath = path * (1 - transition(t)); const currentPath = path * (1 - transition(t));
const newScrollTop = Math.round(target - currentPath);
container.scrollTop = Math.round(target - currentPath); container.scrollTop = newScrollTop;
isAnimating = t < 1; isAnimating = t < 1 && newScrollTop !== target;
if (!isAnimating) { if (!isAnimating) {
currentArgs = undefined;
onHeavyAnimationStop(); onHeavyAnimationStop();
} }
@ -176,7 +174,7 @@ function calculateScrollFrom(
} }
function shortTransition(t: number) { function shortTransition(t: number) {
return 1 - ((1 - t) ** 3); return 1 - ((1 - t) ** 3.5);
} }
function longTransition(t: number) { function longTransition(t: number) {

View File

@ -24,6 +24,13 @@ export function animateSingle(tick: Function, schedulerFn: Scheduler, instance?:
} }
} }
export function cancelSingleAnimation() {
const dumbScheduler = (cb: AnyFunction) => cb;
const dumbCb = () => undefined;
animateSingle(dumbCb, dumbScheduler);
}
export function animate(tick: Function, schedulerFn: Scheduler) { export function animate(tick: Function, schedulerFn: Scheduler) {
schedulerFn(() => { schedulerFn(() => {
if (tick()) { if (tick()) {

View File

@ -1,6 +1,6 @@
import findInViewport from './findInViewport'; import findInViewport from './findInViewport';
import isFullyVisible from './isFullyVisible'; import isFullyVisible from './isFullyVisible';
import fastSmoothScroll from './fastSmoothScroll'; import animateScroll from './animateScroll';
const VIEWPORT_MARGIN = 8; const VIEWPORT_MARGIN = 8;
const SCROLL_MARGIN = 10; const SCROLL_MARGIN = 10;
@ -25,6 +25,6 @@ export default function setTooltipItemVisible(selector: string, index: number, c
if (!visibleIndexes.includes(index) if (!visibleIndexes.includes(index)
|| (index === first && !isFullyVisible(container, allElements[first]))) { || (index === first && !isFullyVisible(container, allElements[first]))) {
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end'; const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
fastSmoothScroll(container, allElements[index], position, SCROLL_MARGIN); animateScroll(container, allElements[index], position, SCROLL_MARGIN);
} }
} }