Message List: Fixes and refactoring of scroll animation
This commit is contained in:
parent
0ab0c13d87
commit
83557863b8
@ -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) => {
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
@ -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()) {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user