Message List: Fixes and refactoring of scroll animation
This commit is contained in:
parent
0ab0c13d87
commit
83557863b8
@ -23,9 +23,9 @@ import {
|
||||
} from '../../config';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
||||
import animateScroll from '../../util/animateScroll';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
import { pickTruthy, unique } from '../../util/iteratees';
|
||||
import { isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
@ -275,13 +275,13 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2);
|
||||
|
||||
fastSmoothScrollHorizontal(header, newLeft);
|
||||
animateHorizontalScroll(header, newLeft);
|
||||
}, [areAddedLoaded, activeSetIndex]);
|
||||
|
||||
const selectStickerSet = useCallback((index: number) => {
|
||||
setActiveSetIndex(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]);
|
||||
|
||||
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||
|
||||
@ -8,7 +8,7 @@ import type { ApiLimitType, GlobalState } from '../../../global/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
@ -185,7 +185,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
const index = PREMIUM_FEATURE_SECTIONS.indexOf(initialSection);
|
||||
setCurrentSlideIndex(index);
|
||||
startScrolling();
|
||||
fastSmoothScrollHorizontal(scrollContainer, scrollContainer.clientWidth * index, 0)
|
||||
animateHorizontalScroll(scrollContainer, scrollContainer.clientWidth * index, 0)
|
||||
.then(stopScrolling);
|
||||
}, [currentSlideIndex, initialSection, prevInitialSection, startScrolling, stopScrolling]);
|
||||
|
||||
@ -196,7 +196,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
setCurrentSlideIndex(index);
|
||||
|
||||
startScrolling();
|
||||
await fastSmoothScrollHorizontal(scrollContainer, scrollContainer.clientWidth * index, 800);
|
||||
await animateHorizontalScroll(scrollContainer, scrollContainer.clientWidth * index, 800);
|
||||
stopScrolling();
|
||||
}, [startScrolling, stopScrolling]);
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
selectChat,
|
||||
selectTopicFromMessage,
|
||||
selectTabState,
|
||||
selectCurrentMessageIds,
|
||||
} from '../../global/selectors';
|
||||
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -46,6 +45,7 @@ type OwnProps = {
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
isEmbedded?: boolean;
|
||||
appearanceOrder?: number;
|
||||
isJustAdded?: boolean;
|
||||
isLastInList?: boolean;
|
||||
isInsideTopic?: boolean;
|
||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||
@ -63,7 +63,6 @@ type StateProps = {
|
||||
topic?: ApiTopic;
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
viewportIds?: number[];
|
||||
premiumGiftSticker?: ApiSticker;
|
||||
};
|
||||
|
||||
@ -73,6 +72,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
isEmbedded,
|
||||
appearanceOrder = 0,
|
||||
isJustAdded,
|
||||
isLastInList,
|
||||
usersById,
|
||||
senderUser,
|
||||
@ -83,7 +83,6 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
viewportIds,
|
||||
premiumGiftSticker,
|
||||
isInsideTopic,
|
||||
topic,
|
||||
@ -102,7 +101,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useOnIntersect(ref, observeIntersectionForReading);
|
||||
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(() => {
|
||||
if (!message.isPinned) return undefined;
|
||||
@ -252,7 +251,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, threadId, messageListType }): StateProps => {
|
||||
(global, { message, threadId }): StateProps => {
|
||||
const {
|
||||
chatId, senderId, replyToMessageId, content,
|
||||
} = message;
|
||||
@ -291,9 +290,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
...(isFocused && {
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
viewportIds: threadId && messageListType
|
||||
? selectCurrentMessageIds(global, chatId, threadId, messageListType)
|
||||
: undefined,
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@ import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import { selectChat, selectCurrentMessageList } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
||||
import animateScroll from '../../util/animateScroll';
|
||||
|
||||
import ScrollDownButton from './ScrollDownButton';
|
||||
|
||||
@ -79,7 +79,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
fastSmoothScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN);
|
||||
animateScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN);
|
||||
}
|
||||
}, [isShown, messageListType, focusNextReply]);
|
||||
|
||||
|
||||
@ -125,6 +125,17 @@
|
||||
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
|
||||
// https://developer.mozilla.org/ru/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
|
||||
&.open.shown {
|
||||
|
||||
@ -5,6 +5,7 @@ import React, {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
|
||||
import { requestForcedReflow, forceMeasure, requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
@ -58,7 +59,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { groupMessages } from './helpers/groupMessages';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
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 { useStateRef } from '../../hooks/useStateRef';
|
||||
@ -397,40 +398,50 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
|
||||
// Handles updated message list, takes care of scroll repositioning
|
||||
useLayoutEffectWithPrevDeps(([prevMessageIds, prevIsViewportNewest]) => {
|
||||
if (process.env.APP_ENV === 'perf') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.time('scrollTop');
|
||||
}
|
||||
|
||||
const containerHeight = getContainerHeight();
|
||||
const prevContainerHeight = prevContainerHeightRef.current;
|
||||
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(() => {
|
||||
const container = containerRef.current!;
|
||||
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 { scrollTop, scrollHeight, offsetHeight } = container;
|
||||
const scrollOffset = scrollOffsetRef.current;
|
||||
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
|
||||
|
||||
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
|
||||
if (wasMessageAdded) {
|
||||
@ -441,34 +452,19 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
bottomOffset -= lastItemHeight;
|
||||
}
|
||||
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 (lastItemElement) {
|
||||
// Break out of `forceLayout`
|
||||
requestMeasure(() => {
|
||||
fastSmoothScroll(
|
||||
container,
|
||||
lastItemElement,
|
||||
'end',
|
||||
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');
|
||||
// Break out of `forceLayout`
|
||||
requestMeasure(() => {
|
||||
animateScroll(
|
||||
container,
|
||||
lastItemElement!,
|
||||
'end',
|
||||
BOTTOM_FOCUS_MARGIN,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight;
|
||||
@ -483,6 +479,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
|
||||
);
|
||||
|
||||
let newScrollTop!: number;
|
||||
if (isAtBottom && isResized) {
|
||||
newScrollTop = scrollHeight - offsetHeight;
|
||||
} else if (anchor) {
|
||||
@ -498,17 +495,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
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));
|
||||
restartCurrentScrollAnimation();
|
||||
|
||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||
|
||||
if (!memoFocusingIdRef.current) {
|
||||
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') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.timeEnd('scrollTop');
|
||||
|
||||
@ -21,6 +21,7 @@ import { isAlbum } from './helpers/groupMessages';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
import useScrollHooks from './hooks/useScrollHooks';
|
||||
import useMessageObservers from './hooks/useMessageObservers';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
|
||||
import Message from './message/Message';
|
||||
import SponsoredMessage from './message/SponsoredMessage';
|
||||
@ -114,12 +115,16 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
<span>{lang('UnreadMessages')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
|
||||
return acc + messageGroup.senderGroups.flat().length;
|
||||
}, 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((
|
||||
dateGroup: MessageDateGroup,
|
||||
dateGroupIndex: number,
|
||||
@ -155,6 +160,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||
isJustAdded={isLastInList && isNewMessage}
|
||||
isLastInList={isLastInList}
|
||||
onPinnedIntersectionChange={onPinnedIntersectionChange}
|
||||
/>,
|
||||
@ -219,6 +225,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
noComments={noComments}
|
||||
noReplies={!noComments || threadId !== MAIN_THREAD_ID}
|
||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||
isJustAdded={position.isLastInList && isNewMessage}
|
||||
isFirstInGroup={position.isFirstInGroup}
|
||||
isLastInGroup={position.isLastInGroup}
|
||||
isFirstInDocumentGroup={position.isFirstInDocumentGroup}
|
||||
|
||||
@ -15,10 +15,10 @@ import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config'
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { uncompressEmoji } from '../../../util/emoji';
|
||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||
import animateScroll from '../../../util/animateScroll';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
|
||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
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;
|
||||
|
||||
fastSmoothScrollHorizontal(header, newLeft);
|
||||
animateHorizontalScroll(header, newLeft);
|
||||
}, [categories, activeCategoryIndex]);
|
||||
|
||||
const lang = useLang();
|
||||
@ -165,7 +165,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
setActiveCategoryIndex(index);
|
||||
const categoryEl = containerRef.current!.closest<HTMLElement>('.SymbolMenu-main')!
|
||||
.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) => {
|
||||
|
||||
@ -8,7 +8,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import findInViewport from '../../../util/findInViewport';
|
||||
import isFullyVisible from '../../../util/isFullyVisible';
|
||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
|
||||
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
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 newLeft = position === 'start' ? index * EMOJI_BUTTON_WIDTH : 0;
|
||||
|
||||
fastSmoothScrollHorizontal(container, newLeft);
|
||||
animateHorizontalScroll(container, newLeft);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,9 +17,9 @@ import {
|
||||
} from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||
import animateScroll from '../../../util/animateScroll';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
|
||||
import { pickTruthy, uniqueByField } from '../../../util/iteratees';
|
||||
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);
|
||||
|
||||
fastSmoothScrollHorizontal(header, newLeft);
|
||||
animateHorizontalScroll(header, newLeft);
|
||||
}, [areAddedLoaded, activeSetIndex]);
|
||||
|
||||
const selectStickerSet = useCallback((index: number) => {
|
||||
setActiveSetIndex(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) => {
|
||||
|
||||
@ -21,7 +21,7 @@ import { useDebouncedSignal } from '../../../hooks/useAsyncResolvers';
|
||||
const FAB_THRESHOLD = 50;
|
||||
const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect
|
||||
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(
|
||||
type: MessageListType,
|
||||
|
||||
@ -67,7 +67,7 @@ import {
|
||||
selectTopicFromMessage,
|
||||
selectTabState,
|
||||
selectChatTranslations,
|
||||
selectRequestedTranslationLanguage, selectCurrentMessageIds,
|
||||
selectRequestedTranslationLanguage,
|
||||
} from '../../../global/selectors';
|
||||
import {
|
||||
getMessageContent,
|
||||
@ -109,7 +109,7 @@ import renderText from '../../common/helpers/renderText';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
||||
import { isAnimatingScroll } from '../../../util/fastSmoothScroll';
|
||||
import { isAnimatingScroll } from '../../../util/animateScroll';
|
||||
|
||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
@ -186,6 +186,7 @@ type OwnProps =
|
||||
noComments: boolean;
|
||||
noReplies: boolean;
|
||||
appearanceOrder: number;
|
||||
isJustAdded: boolean;
|
||||
memoFirstUnreadIdRef: { current: number | undefined };
|
||||
onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
|
||||
}
|
||||
@ -212,7 +213,6 @@ type StateProps = {
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
isResizingContainer?: boolean;
|
||||
viewportIds?: number[];
|
||||
isForwarding?: boolean;
|
||||
isChatWithSelf?: boolean;
|
||||
isRepliesChat?: boolean;
|
||||
@ -293,6 +293,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
noComments,
|
||||
noReplies,
|
||||
appearanceOrder,
|
||||
isJustAdded,
|
||||
isFirstInGroup,
|
||||
isPremium,
|
||||
isLastInGroup,
|
||||
@ -320,7 +321,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
isResizingContainer,
|
||||
viewportIds,
|
||||
isForwarding,
|
||||
isChatWithSelf,
|
||||
isRepliesChat,
|
||||
@ -412,7 +412,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
|
||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||
|
||||
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
|
||||
const { transitionClassNames } = useShowTransition(
|
||||
isShown || isJustAdded,
|
||||
undefined,
|
||||
noAppearanceAnimation && !isJustAdded,
|
||||
false,
|
||||
);
|
||||
|
||||
const {
|
||||
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError,
|
||||
@ -576,6 +581,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
Boolean(message.inlineButtons) && 'has-inline-buttons',
|
||||
isSwiped && 'is-swiped',
|
||||
transitionClassNames,
|
||||
isJustAdded && 'is-just-added',
|
||||
(Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction',
|
||||
);
|
||||
|
||||
@ -658,7 +664,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
|
||||
useFocusMessage(
|
||||
ref, messageId, chatId, isFocused, focusDirection, noFocusHighlight, viewportIds, isResizingContainer,
|
||||
ref, messageId, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded,
|
||||
);
|
||||
|
||||
const signature = (isChannel && message.postAuthorTitle)
|
||||
@ -1480,7 +1486,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
isResizingContainer,
|
||||
viewportIds: selectCurrentMessageIds(global, chatId, threadId, messageListType),
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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 fastSmoothScroll from '../../../../util/fastSmoothScroll';
|
||||
import animateScroll from '../../../../util/animateScroll';
|
||||
|
||||
// This is used when the viewport was replaced.
|
||||
const BOTTOM_FOCUS_OFFSET = 500;
|
||||
@ -16,38 +17,42 @@ export default function useFocusMessage(
|
||||
isFocused?: boolean,
|
||||
focusDirection?: FocusDirection,
|
||||
noFocusHighlight?: boolean,
|
||||
viewportIds?: number[],
|
||||
isResizingContainer?: boolean,
|
||||
isJustAdded?: boolean,
|
||||
) {
|
||||
const viewportIndex = useMemo(() => {
|
||||
if (!viewportIds) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = viewportIds.indexOf(messageId);
|
||||
return Math.min(index, viewportIds.length - index - 1);
|
||||
}, [messageId, viewportIds]);
|
||||
const isRelocatedRef = useRef(!isJustAdded);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const isRelocated = isRelocatedRef.current;
|
||||
isRelocatedRef.current = false;
|
||||
|
||||
if (isFocused && elementRef.current) {
|
||||
const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList')!;
|
||||
// `noFocusHighlight` is always called with “scroll-to-bottom” buttons
|
||||
const isToBottom = noFocusHighlight;
|
||||
|
||||
fastSmoothScroll(
|
||||
const exec = () => animateScroll(
|
||||
messagesContainer,
|
||||
elementRef.current,
|
||||
elementRef.current!,
|
||||
isToBottom ? 'end' : 'centerOrTop',
|
||||
FOCUS_MARGIN,
|
||||
focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined,
|
||||
focusDirection,
|
||||
undefined,
|
||||
isResizingContainer,
|
||||
// We need this to override scroll setting from Message List layout effect
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useEffect } from '../../../lib/teact/teact';
|
||||
|
||||
import { ProfileState } from '../../../types';
|
||||
|
||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||
import animateScroll from '../../../util/animateScroll';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
|
||||
@ -27,7 +27,7 @@ export default function useProfileState(
|
||||
if (container.scrollTop < tabsEl.offsetTop) {
|
||||
onProfileStateChange(tabType === 'members' ? ProfileState.MemberList : ProfileState.SharedMedia);
|
||||
isScrollingProgrammatically = true;
|
||||
fastSmoothScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION);
|
||||
animateScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION);
|
||||
setTimeout(() => {
|
||||
isScrollingProgrammatically = false;
|
||||
}, PROGRAMMATIC_SCROLL_TIMEOUT_MS);
|
||||
@ -52,7 +52,7 @@ export default function useProfileState(
|
||||
}
|
||||
|
||||
isScrollingProgrammatically = true;
|
||||
fastSmoothScroll(
|
||||
animateScroll(
|
||||
container,
|
||||
container.firstElementChild as HTMLElement,
|
||||
'start',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { RefObject } from 'react';
|
||||
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';
|
||||
|
||||
@ -78,9 +78,19 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
return enableDirectTextInput;
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => (isOpen
|
||||
? captureKeyboardListeners({ onEsc: onClose, onEnter })
|
||||
: undefined), [isOpen, onClose, onEnter]);
|
||||
const handleEnter = useCallback((e: KeyboardEvent) => {
|
||||
if (!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]);
|
||||
|
||||
useHistoryBack({
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo, useRef, useEffect } from '../../lib/teact/teact';
|
||||
|
||||
import { ALL_FOLDER_ID } from '../../config';
|
||||
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
|
||||
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
@ -63,7 +63,7 @@ const TabList: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
fastSmoothScrollHorizontal(container, newLeft, SCROLL_DURATION);
|
||||
animateHorizontalScroll(container, newLeft, SCROLL_DURATION);
|
||||
}, [activeTab]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -144,7 +144,7 @@ export const TMP_CHAT_ID = '0';
|
||||
|
||||
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_DISTANCE = 750;
|
||||
export const FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE = 300; // px
|
||||
|
||||
@ -89,7 +89,24 @@ export function useIntersectionObserver({
|
||||
function initController() {
|
||||
const callbacks = new Map();
|
||||
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());
|
||||
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
@ -104,17 +121,6 @@ export function useIntersectionObserver({
|
||||
}
|
||||
|
||||
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(
|
||||
@ -124,9 +130,7 @@ export function useIntersectionObserver({
|
||||
});
|
||||
|
||||
if (freezeFlagsRef.current) {
|
||||
onUnfreezeRef.current = () => {
|
||||
observerCallback();
|
||||
};
|
||||
onUnfreezeRef.current = observerCallback;
|
||||
} else {
|
||||
observerCallback();
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ const DEFAULT_DURATION = 300;
|
||||
|
||||
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) {
|
||||
duration = 0;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
import { getGlobal } from '../global';
|
||||
|
||||
import { FocusDirection } from '../types';
|
||||
@ -11,41 +12,40 @@ import {
|
||||
} from '../config';
|
||||
import { IS_ANDROID } from './windowEnvironment';
|
||||
import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck';
|
||||
import { animateSingle } from './animation';
|
||||
import { requestForcedReflow, requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
import { animateSingle, cancelSingleAnimation } from './animation';
|
||||
|
||||
type Params = Parameters<typeof createMutateFunction>;
|
||||
|
||||
let isAnimating = false;
|
||||
let currentArgs: Parameters<typeof createMutateFunction> | undefined;
|
||||
|
||||
export default function fastSmoothScroll(
|
||||
container: HTMLElement,
|
||||
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;
|
||||
export default function animateScroll(...args: Params | [...Params, boolean]) {
|
||||
currentArgs = args.slice(0, 8) as Params;
|
||||
|
||||
if (withForcedReflow) {
|
||||
requestForcedReflow(() => measure(...args));
|
||||
} else {
|
||||
requestMutation(measure(...args));
|
||||
const mutate = createMutateFunction(...currentArgs);
|
||||
|
||||
const shouldReturnMutationFn = args[8];
|
||||
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,
|
||||
element: HTMLElement,
|
||||
position: ScrollLogicalPosition | 'centerOrTop',
|
||||
@ -88,12 +88,7 @@ function measure(
|
||||
|
||||
const scrollFrom = calculateScrollFrom(container, scrollTo, maxDistance, forceDirection);
|
||||
|
||||
if (currentScrollTop !== scrollFrom) {
|
||||
container.scrollTop = scrollFrom;
|
||||
}
|
||||
|
||||
let path = scrollTo - scrollFrom;
|
||||
|
||||
if (path < 0) {
|
||||
const remainingPath = -scrollFrom;
|
||||
path = Math.max(path, remainingPath);
|
||||
@ -102,12 +97,14 @@ function measure(
|
||||
path = Math.min(path, remainingPath);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentScrollTop !== scrollFrom) {
|
||||
container.scrollTop = scrollFrom;
|
||||
}
|
||||
const absPath = Math.abs(path);
|
||||
|
||||
return () => {
|
||||
if (absPath < 1) {
|
||||
if (currentScrollTop !== scrollFrom) {
|
||||
container.scrollTop = scrollFrom;
|
||||
}
|
||||
|
||||
if (path === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -120,7 +117,6 @@ function measure(
|
||||
|
||||
isAnimating = true;
|
||||
|
||||
const absPath = Math.abs(path);
|
||||
const transition = absPath <= FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition;
|
||||
const duration = forceDuration || (
|
||||
FAST_SMOOTH_MIN_DURATION
|
||||
@ -132,12 +128,14 @@ function measure(
|
||||
animateSingle(() => {
|
||||
const t = Math.min((Date.now() - startAt) / duration, 1);
|
||||
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) {
|
||||
currentArgs = undefined;
|
||||
onHeavyAnimationStop();
|
||||
}
|
||||
|
||||
@ -176,7 +174,7 @@ function calculateScrollFrom(
|
||||
}
|
||||
|
||||
function shortTransition(t: number) {
|
||||
return 1 - ((1 - t) ** 3);
|
||||
return 1 - ((1 - t) ** 3.5);
|
||||
}
|
||||
|
||||
function longTransition(t: number) {
|
||||
@ -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) {
|
||||
schedulerFn(() => {
|
||||
if (tick()) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import findInViewport from './findInViewport';
|
||||
import isFullyVisible from './isFullyVisible';
|
||||
import fastSmoothScroll from './fastSmoothScroll';
|
||||
import animateScroll from './animateScroll';
|
||||
|
||||
const VIEWPORT_MARGIN = 8;
|
||||
const SCROLL_MARGIN = 10;
|
||||
@ -25,6 +25,6 @@ export default function setTooltipItemVisible(selector: string, index: number, c
|
||||
if (!visibleIndexes.includes(index)
|
||||
|| (index === first && !isFullyVisible(container, allElements[first]))) {
|
||||
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
|
||||
fastSmoothScroll(container, allElements[index], position, SCROLL_MARGIN);
|
||||
animateScroll(container, allElements[index], position, SCROLL_MARGIN);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user