[Perf] [Refactoring] Extract messages rendering to a memoized component

This commit is contained in:
Alexander Zinchuk 2021-07-28 17:09:50 +03:00
parent b3e2eda5e6
commit 45a0bca14b
4 changed files with 387 additions and 337 deletions

View File

@ -7,8 +7,7 @@ import { ApiMessage, ApiRestrictionReason, MAIN_THREAD_ID } from '../../api/type
import { GlobalActions, MessageListType } from '../../global/types';
import { LoadMoreDirection } from '../../types';
import { ANIMATION_END_DELAY, MESSAGE_LIST_SLICE, SCHEDULED_WHEN_ONLINE } from '../../config';
import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { ANIMATION_END_DELAY, MESSAGE_LIST_SLICE } from '../../config';
import {
selectChatMessages,
selectIsViewportNewest,
@ -25,42 +24,24 @@ import {
selectScheduledMessages,
selectCurrentMessageIds,
} from '../../modules/selectors';
import {
getMessageOriginalId,
isActionMessage,
isChatChannel,
isChatPrivate,
isOwnMessage,
} from '../../modules/helpers';
import {
compact,
flatten,
orderBy,
pick,
} from '../../util/iteratees';
import {
fastRaf, debounce, onTickEnd,
} from '../../util/schedulers';
import { formatHumanDate } from '../../util/dateFormat';
import { isChatChannel, isChatPrivate } from '../../modules/helpers';
import { orderBy, pick } from '../../util/iteratees';
import { fastRaf, debounce, onTickEnd } from '../../util/schedulers';
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
import buildClassName from '../../util/buildClassName';
import { groupMessages, MessageDateGroup, isAlbum } from './helpers/groupMessages';
import { groupMessages } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import { ObserveFn, useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useOnChange from '../../hooks/useOnChange';
import useStickyDates from './hooks/useStickyDates';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import resetScroll from '../../util/resetScroll';
import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll';
import renderText from '../common/helpers/renderText';
import useLang, { LangFn } from '../../hooks/useLang';
import useLang from '../../hooks/useLang';
import useWindowSize from '../../hooks/useWindowSize';
import useBackgroundMode from '../../hooks/useBackgroundMode';
import Loading from '../ui/Loading';
import MessageScroll from './MessageScroll';
import Message from './message/Message';
import ActionMessage from './ActionMessage';
import MessageListContent from './MessageListContent';
import './MessageList.scss';
@ -96,16 +77,12 @@ type StateProps = {
hasLinkedChat?: boolean;
};
type DispatchProps = Pick<GlobalActions, (
'loadViewportMessages' | 'markMessageListRead' | 'markMessagesRead' | 'setScrollOffset' | 'openHistoryCalendar'
)>;
type DispatchProps = Pick<GlobalActions, 'loadViewportMessages' | 'setScrollOffset' | 'openHistoryCalendar'>;
const BOTTOM_THRESHOLD = 100;
const UNREAD_DIVIDER_TOP = 10;
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
const SCROLL_DEBOUNCE = 200;
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_MEDIA = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
const FOCUSING_DURATION = 1000;
const BOTTOM_FOCUS_MARGIN = 20;
const SELECT_MODE_ANIMATION_DURATION = 200;
@ -137,8 +114,6 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
isSelectModeActive,
animationLevel,
loadViewportMessages,
markMessageListRead,
markMessagesRead,
setScrollOffset,
lastMessage,
botDescription,
@ -184,65 +159,10 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
}
}, [firstUnreadId]);
const {
observe: observeIntersectionForMedia,
} = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
margin: INTERSECTION_MARGIN_FOR_MEDIA,
});
const {
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
} = useIntersectionObserver({
rootRef: containerRef,
}, (entries) => {
if (type !== 'thread') {
return;
}
let maxId = 0;
const mentionIds: number[] = [];
entries.forEach((entry) => {
const { isIntersecting, target } = entry;
if (!isIntersecting) {
return;
}
const { dataset } = target as HTMLDivElement;
const messageId = Number(dataset.lastMessageId || dataset.messageId);
if (messageId > maxId) {
maxId = messageId;
}
if (dataset.hasUnreadMention) {
mentionIds.push(messageId);
}
});
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
markMessageListRead({ maxId });
}
if (mentionIds.length) {
markMessagesRead({ messageIds: mentionIds });
}
});
useBackgroundMode(freezeForReading, unfreezeForReading);
useOnChange(() => {
memoFocusingIdRef.current = focusingId;
}, [focusingId]);
const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
});
const messageGroups = useMemo(() => {
if (!messageIds || !messagesById) {
return undefined;
@ -260,15 +180,14 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current);
}, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]);
const [loadMoreBackwards, loadMoreForwards, loadMoreAround] = useMemo(
() => (type === 'thread' ? [
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Backwards }), 1000, true, false),
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Forwards }), 1000, true, false),
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false),
] : []),
const loadMoreAround = useMemo(() => {
if (type !== 'thread') {
return undefined;
}
return debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
[loadViewportMessages, messageIds],
);
}, [loadViewportMessages, messageIds]);
const { isScrolled, updateStickyDates } = useStickyDates();
@ -535,37 +454,26 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
) : messageIds && !messageGroups ? (
<div className="empty"><span>{lang('NoMessages')}</span></div>
) : ((messageIds && messageGroups) || lastMessage) ? (
<MessageScroll
containerRef={containerRef}
className="messages-container"
<MessageListContent
messageIds={messageIds || [lastMessage!.id]}
loadMoreForwards={loadMoreForwards}
loadMoreBackwards={loadMoreBackwards}
isViewportNewest={isViewportNewest}
firstUnreadId={firstUnreadId}
messageGroups={messageGroups || groupMessages([lastMessage!])}
isViewportNewest={Boolean(isViewportNewest)}
isUnread={Boolean(firstUnreadId)}
withUsers={withUsers}
noAvatars={noAvatars}
containerRef={containerRef}
anchorIdRef={anchorIdRef}
memoFirstUnreadIdRef={memoUnreadDividerBeforeIdRef}
threadId={threadId}
type={type}
threadTopMessageId={threadTopMessageId}
hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onFabToggle={onFabToggle}
onNotchToggle={onNotchToggle}
>
{renderMessages(
lang,
messageGroups || groupMessages([lastMessage!]),
observeIntersectionForReading,
observeIntersectionForMedia,
observeIntersectionForAnimatedStickers,
withUsers,
noAvatars,
anchorIdRef,
memoUnreadDividerBeforeIdRef,
threadId,
type,
threadTopMessageId,
threadFirstMessageId,
hasLinkedChat,
messageGroups ? type === 'scheduled' : false,
!messageGroups || !shouldAnimateAppearanceRef.current,
openHistoryCalendar,
)}
</MessageScroll>
openHistoryCalendar={openHistoryCalendar}
/>
) : (
<Loading color="white" />
)}
@ -573,166 +481,6 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
);
};
function renderMessages(
lang: LangFn,
messageGroups: MessageDateGroup[],
observeIntersectionForReading: ObserveFn,
observeIntersectionForMedia: ObserveFn,
observeIntersectionForAnimatedStickers: ObserveFn,
withUsers: boolean,
noAvatars: boolean,
currentAnchorIdRef: { current: string | undefined },
memoFirstUnreadIdRef: { current: number | undefined },
threadId: number,
type: MessageListType,
threadTopMessageId: number | undefined,
threadFirstMessageId: number | undefined,
hasLinkedChat: boolean | undefined,
isSchedule: boolean,
noAppearanceAnimation: boolean,
openHistoryCalendar: Function,
) {
const unreadDivider = (
<div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
<span>{lang('UnreadMessages')}</span>
</div>
);
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
return acc + flatten(messageGroup.senderGroups).length;
}, 0);
let appearanceIndex = 0;
const dateGroups = messageGroups.map((
dateGroup: MessageDateGroup,
dateGroupIndex: number,
dateGroupsArray: MessageDateGroup[],
) => {
const senderGroups = dateGroup.senderGroups.map((
senderGroup,
senderGroupIndex,
senderGroupsArray,
) => {
if (senderGroup.length === 1 && !isAlbum(senderGroup[0]) && isActionMessage(senderGroup[0])) {
const message = senderGroup[0];
const isLastInList = (
senderGroupIndex === senderGroupsArray.length - 1
&& dateGroupIndex === dateGroupsArray.length - 1
);
return compact([
message.id === memoFirstUnreadIdRef.current && unreadDivider,
<ActionMessage
key={message.id}
message={message}
observeIntersection={observeIntersectionForReading}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isLastInList={isLastInList}
/>,
]);
}
let currentDocumentGroupId: string | undefined;
return flatten(senderGroup.map((
messageOrAlbum,
messageIndex,
) => {
const message = isAlbum(messageOrAlbum) ? messageOrAlbum.mainMessage : messageOrAlbum;
const album = isAlbum(messageOrAlbum) ? messageOrAlbum : undefined;
const isOwn = isOwnMessage(message);
const isMessageAlbum = isAlbum(messageOrAlbum);
const nextMessage = senderGroup[messageIndex + 1];
if (message.previousLocalId && currentAnchorIdRef.current === `message${message.previousLocalId}`) {
currentAnchorIdRef.current = `message${message.id}`;
}
const documentGroupId = !isMessageAlbum && message.groupedId ? message.groupedId : undefined;
const nextDocumentGroupId = nextMessage && !isAlbum(nextMessage) ? nextMessage.groupedId : undefined;
const position = {
isFirstInGroup: messageIndex === 0,
isLastInGroup: messageIndex === senderGroup.length - 1,
isFirstInDocumentGroup: Boolean(documentGroupId && documentGroupId !== currentDocumentGroupId),
isLastInDocumentGroup: Boolean(documentGroupId && documentGroupId !== nextDocumentGroupId),
isLastInList: (
messageIndex === senderGroup.length - 1
&& senderGroupIndex === senderGroupsArray.length - 1
&& dateGroupIndex === dateGroupsArray.length - 1
),
};
currentDocumentGroupId = documentGroupId;
const originalId = getMessageOriginalId(message);
// Scheduled messages can have local IDs in the middle of the list,
// and keys should be ordered, so we prefix it with a date.
// However, this may lead to issues if server date is not synchronized with the local one.
const key = type !== 'scheduled' ? originalId : `${message.date}_${originalId}`;
return compact([
message.id === memoFirstUnreadIdRef.current ? unreadDivider : undefined,
<Message
key={key}
message={message}
observeIntersectionForBottom={observeIntersectionForReading}
observeIntersectionForMedia={observeIntersectionForMedia}
observeIntersectionForAnimatedStickers={observeIntersectionForAnimatedStickers}
album={album}
noAvatars={noAvatars}
withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)}
withSenderName={position.isFirstInGroup && withUsers && !isOwn}
threadId={threadId}
messageListType={type}
noComments={hasLinkedChat === false}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isFirstInGroup={position.isFirstInGroup}
isLastInGroup={position.isLastInGroup}
isFirstInDocumentGroup={position.isFirstInDocumentGroup}
isLastInDocumentGroup={position.isLastInDocumentGroup}
isLastInList={position.isLastInList}
/>,
message.id === threadTopMessageId && (
<div className="local-action-message" key="discussion-started">
<span>{lang('DiscussionStarted')}</span>
</div>
),
]);
}));
});
return (
<div
className="message-date-group"
key={dateGroup.datetime}
onMouseDown={preventMessageInputBlur}
teactFastList
>
<div
className={buildClassName('sticky-date', !isSchedule && 'interactive')}
key="date-header"
onMouseDown={preventMessageInputBlur}
onClick={!isSchedule ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
>
<span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledUntilOnline')
)}
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledOn', formatHumanDate(lang, dateGroup.datetime, undefined, true))
)}
{!isSchedule && formatHumanDate(lang, dateGroup.datetime)}
</span>
</div>
{flatten(senderGroups)}
</div>
);
});
return flatten(dateGroups);
}
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): StateProps => {
const chat = selectChat(global, chatId);
@ -797,8 +545,6 @@ export default memo(withGlobal<OwnProps>(
},
(setGlobal, actions): DispatchProps => pick(actions, [
'loadViewportMessages',
'markMessageListRead',
'markMessagesRead',
'setScrollOffset',
'openHistoryCalendar',
]),

View File

@ -0,0 +1,241 @@
import { RefObject } from 'react';
import React, { FC, memo } from '../../lib/teact/teact';
import { MessageListType } from '../../global/types';
import { SCHEDULED_WHEN_ONLINE } from '../../config';
import buildClassName from '../../util/buildClassName';
import { compact, flatten } from '../../util/iteratees';
import { formatHumanDate } from '../../util/dateFormat';
import { getMessageOriginalId, isActionMessage, isOwnMessage } from '../../modules/helpers';
import useLang from '../../hooks/useLang';
import { isAlbum, MessageDateGroup } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import useScrollHooks from './hooks/useScrollHooks';
import useMessageObservers from './hooks/useMessageObservers';
import Message from './message/Message';
import ActionMessage from './ActionMessage';
interface OwnProps {
messageIds: number[];
messageGroups: MessageDateGroup[];
isViewportNewest: boolean;
isUnread: boolean;
withUsers: boolean;
noAvatars: boolean;
containerRef: RefObject<HTMLDivElement>;
anchorIdRef: { current: string | undefined };
memoFirstUnreadIdRef: { current: number | undefined };
threadId: number;
type: MessageListType;
threadTopMessageId: number | undefined;
hasLinkedChat: boolean | undefined;
isSchedule: boolean;
noAppearanceAnimation: boolean;
onFabToggle: AnyToVoidFunction;
onNotchToggle: AnyToVoidFunction;
openHistoryCalendar: Function;
}
const UNREAD_DIVIDER_CLASS = 'unread-divider';
const MessageListContent: FC<OwnProps> = ({
messageIds,
messageGroups,
isViewportNewest,
isUnread,
withUsers,
noAvatars,
containerRef,
anchorIdRef,
memoFirstUnreadIdRef,
threadId,
type,
threadTopMessageId,
hasLinkedChat,
isSchedule,
noAppearanceAnimation,
onFabToggle,
onNotchToggle,
openHistoryCalendar,
}) => {
const {
observeIntersectionForMedia,
observeIntersectionForReading,
observeIntersectionForAnimatedStickers,
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef);
const {
backwardsTriggerRef,
forwardsTriggerRef,
fabTriggerRef,
} = useScrollHooks(
type,
containerRef,
messageIds,
isViewportNewest,
isUnread,
onFabToggle,
onNotchToggle,
);
const lang = useLang();
const unreadDivider = (
<div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
<span>{lang('UnreadMessages')}</span>
</div>
);
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
return acc + flatten(messageGroup.senderGroups).length;
}, 0);
let appearanceIndex = 0;
const dateGroups = messageGroups.map((
dateGroup: MessageDateGroup,
dateGroupIndex: number,
dateGroupsArray: MessageDateGroup[],
) => {
const senderGroups = dateGroup.senderGroups.map((
senderGroup,
senderGroupIndex,
senderGroupsArray,
) => {
if (senderGroup.length === 1 && !isAlbum(senderGroup[0]) && isActionMessage(senderGroup[0])) {
const message = senderGroup[0];
const isLastInList = (
senderGroupIndex === senderGroupsArray.length - 1
&& dateGroupIndex === dateGroupsArray.length - 1
);
return compact([
message.id === memoFirstUnreadIdRef.current && unreadDivider,
<ActionMessage
key={message.id}
message={message}
observeIntersection={observeIntersectionForReading}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isLastInList={isLastInList}
/>,
]);
}
let currentDocumentGroupId: string | undefined;
return flatten(senderGroup.map((
messageOrAlbum,
messageIndex,
) => {
const message = isAlbum(messageOrAlbum) ? messageOrAlbum.mainMessage : messageOrAlbum;
const album = isAlbum(messageOrAlbum) ? messageOrAlbum : undefined;
const isOwn = isOwnMessage(message);
const isMessageAlbum = isAlbum(messageOrAlbum);
const nextMessage = senderGroup[messageIndex + 1];
if (message.previousLocalId && anchorIdRef.current === `message${message.previousLocalId}`) {
anchorIdRef.current = `message${message.id}`;
}
const documentGroupId = !isMessageAlbum && message.groupedId ? message.groupedId : undefined;
const nextDocumentGroupId = nextMessage && !isAlbum(nextMessage) ? nextMessage.groupedId : undefined;
const position = {
isFirstInGroup: messageIndex === 0,
isLastInGroup: messageIndex === senderGroup.length - 1,
isFirstInDocumentGroup: Boolean(documentGroupId && documentGroupId !== currentDocumentGroupId),
isLastInDocumentGroup: Boolean(documentGroupId && documentGroupId !== nextDocumentGroupId),
isLastInList: (
messageIndex === senderGroup.length - 1
&& senderGroupIndex === senderGroupsArray.length - 1
&& dateGroupIndex === dateGroupsArray.length - 1
),
};
currentDocumentGroupId = documentGroupId;
const originalId = getMessageOriginalId(message);
// Scheduled messages can have local IDs in the middle of the list,
// and keys should be ordered, so we prefix it with a date.
// However, this may lead to issues if server date is not synchronized with the local one.
const key = type !== 'scheduled' ? originalId : `${message.date}_${originalId}`;
return compact([
message.id === memoFirstUnreadIdRef.current ? unreadDivider : undefined,
<Message
key={key}
message={message}
observeIntersectionForBottom={observeIntersectionForReading}
observeIntersectionForMedia={observeIntersectionForMedia}
observeIntersectionForAnimatedStickers={observeIntersectionForAnimatedStickers}
album={album}
noAvatars={noAvatars}
withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)}
withSenderName={position.isFirstInGroup && withUsers && !isOwn}
threadId={threadId}
messageListType={type}
noComments={hasLinkedChat === false}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isFirstInGroup={position.isFirstInGroup}
isLastInGroup={position.isLastInGroup}
isFirstInDocumentGroup={position.isFirstInDocumentGroup}
isLastInDocumentGroup={position.isLastInDocumentGroup}
isLastInList={position.isLastInList}
/>,
message.id === threadTopMessageId && (
<div className="local-action-message" key="discussion-started">
<span>{lang('DiscussionStarted')}</span>
</div>
),
]);
}));
});
return (
<div
className="message-date-group"
key={dateGroup.datetime}
onMouseDown={preventMessageInputBlur}
teactFastList
>
<div
className={buildClassName('sticky-date', !isSchedule && 'interactive')}
key="date-header"
onMouseDown={preventMessageInputBlur}
onClick={!isSchedule ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
>
<span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledUntilOnline')
)}
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledOn', formatHumanDate(lang, dateGroup.datetime, undefined, true))
)}
{!isSchedule && formatHumanDate(lang, dateGroup.datetime)}
</span>
</div>
{flatten(senderGroups)}
</div>
);
});
return (
<div className="messages-container" teactFastList>
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
{flatten(dateGroups)}
<div
ref={forwardsTriggerRef}
key="forwards-trigger"
className="forwards-trigger"
/>
<div
ref={fabTriggerRef}
key="fab-trigger"
className="fab-trigger"
/>
</div>
);
};
export default memo(MessageListContent);

View File

@ -0,0 +1,80 @@
import { RefObject } from 'react';
import { getDispatch } from '../../../lib/teact/teactn';
import { MessageListType } from '../../../global/types';
import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useBackgroundMode from '../../../hooks/useBackgroundMode';
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_MEDIA = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
export default function useMessageObservers(
type: MessageListType,
containerRef: RefObject<HTMLDivElement>,
memoFirstUnreadIdRef: { current: number | undefined },
) {
const { markMessageListRead, markMessagesRead } = getDispatch();
const {
observe: observeIntersectionForMedia,
} = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
margin: INTERSECTION_MARGIN_FOR_MEDIA,
});
const {
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
} = useIntersectionObserver({
rootRef: containerRef,
}, (entries) => {
if (type !== 'thread') {
return;
}
let maxId = 0;
const mentionIds: number[] = [];
entries.forEach((entry) => {
const { isIntersecting, target } = entry;
if (!isIntersecting) {
return;
}
const { dataset } = target as HTMLDivElement;
const messageId = Number(dataset.lastMessageId || dataset.messageId);
if (messageId > maxId) {
maxId = messageId;
}
if (dataset.hasUnreadMention) {
mentionIds.push(messageId);
}
});
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
markMessageListRead({ maxId });
}
if (mentionIds.length) {
markMessagesRead({ messageIds: mentionIds });
}
});
useBackgroundMode(freezeForReading, unfreezeForReading);
const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
});
return {
observeIntersectionForMedia,
observeIntersectionForReading,
observeIntersectionForAnimatedStickers,
};
}

View File

@ -1,39 +1,39 @@
import { MutableRefObject } from 'react';
import React, { FC, useCallback, useRef } from '../../lib/teact/teact';
import { RefObject } from 'react';
import { getDispatch } from '../../../lib/teact/teactn';
import { useCallback, useMemo, useRef } from '../../../lib/teact/teact';
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../config';
import resetScroll from '../../util/resetScroll';
import { useIntersectionObserver, useOnIntersect } from '../../hooks/useIntersectionObserver';
import useOnChange from '../../hooks/useOnChange';
import { LoadMoreDirection } from '../../../types';
import { MessageListType } from '../../../global/types';
type OwnProps = {
containerRef: MutableRefObject<HTMLDivElement | null>;
className: string;
messageIds: number[];
loadMoreForwards?: NoneToVoidFunction;
loadMoreBackwards?: NoneToVoidFunction;
isViewportNewest?: boolean;
firstUnreadId?: number;
onFabToggle: AnyToVoidFunction;
onNotchToggle: AnyToVoidFunction;
children: any;
};
import { debounce } from '../../../util/schedulers';
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../config';
import resetScroll from '../../../util/resetScroll';
import useOnChange from '../../../hooks/useOnChange';
const FAB_THRESHOLD = 50;
const TOOLS_FREEZE_TIMEOUT = 100;
const MessageScroll: FC<OwnProps> = ({
containerRef,
className,
messageIds,
loadMoreForwards,
loadMoreBackwards,
isViewportNewest,
firstUnreadId,
onFabToggle,
onNotchToggle,
children,
}) => {
export default function useScrollHooks(
type: MessageListType,
containerRef: RefObject<HTMLDivElement>,
messageIds: number[],
isViewportNewest: boolean,
isUnread: boolean,
onFabToggle: AnyToVoidFunction,
onNotchToggle: AnyToVoidFunction,
) {
const { loadViewportMessages } = getDispatch();
const [loadMoreBackwards, loadMoreForwards] = useMemo(
() => (type === 'thread' ? [
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Backwards }), 1000, true, false),
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Forwards }), 1000, true, false),
] : []),
// eslint-disable-next-line react-hooks/exhaustive-deps
[loadViewportMessages, messageIds],
);
// eslint-disable-next-line no-null/no-null
const backwardsTriggerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
@ -59,9 +59,9 @@ const MessageScroll: FC<OwnProps> = ({
const isNearBottom = scrollBottom <= FAB_THRESHOLD;
const isAtBottom = scrollBottom <= 0;
onFabToggle(firstUnreadId ? !isAtBottom : !isNearBottom);
onFabToggle(isUnread ? !isAtBottom : !isNearBottom);
onNotchToggle(!isAtBottom);
}, [messageIds, isViewportNewest, containerRef, onFabToggle, firstUnreadId, onNotchToggle]);
}, [messageIds, isViewportNewest, containerRef, onFabToggle, isUnread, onNotchToggle]);
const {
observe: observeIntersection,
@ -124,22 +124,5 @@ const MessageScroll: FC<OwnProps> = ({
}, TOOLS_FREEZE_TIMEOUT);
}, [messageIds]);
return (
<div className={className} teactFastList>
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
{children}
<div
ref={forwardsTriggerRef}
key="forwards-trigger"
className="forwards-trigger"
/>
<div
ref={fabTriggerRef}
key="fab-trigger"
className="fab-trigger"
/>
</div>
);
};
export default MessageScroll;
return { backwardsTriggerRef, forwardsTriggerRef, fabTriggerRef };
}