[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 { GlobalActions, MessageListType } from '../../global/types';
import { LoadMoreDirection } from '../../types'; import { LoadMoreDirection } from '../../types';
import { ANIMATION_END_DELAY, MESSAGE_LIST_SLICE, SCHEDULED_WHEN_ONLINE } from '../../config'; import { ANIMATION_END_DELAY, MESSAGE_LIST_SLICE } from '../../config';
import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { import {
selectChatMessages, selectChatMessages,
selectIsViewportNewest, selectIsViewportNewest,
@ -25,42 +24,24 @@ import {
selectScheduledMessages, selectScheduledMessages,
selectCurrentMessageIds, selectCurrentMessageIds,
} from '../../modules/selectors'; } from '../../modules/selectors';
import { import { isChatChannel, isChatPrivate } from '../../modules/helpers';
getMessageOriginalId, import { orderBy, pick } from '../../util/iteratees';
isActionMessage, import { fastRaf, debounce, onTickEnd } from '../../util/schedulers';
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 useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps'; import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { groupMessages, MessageDateGroup, isAlbum } from './helpers/groupMessages'; import { groupMessages } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import { ObserveFn, useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useOnChange from '../../hooks/useOnChange'; import useOnChange from '../../hooks/useOnChange';
import useStickyDates from './hooks/useStickyDates'; import useStickyDates from './hooks/useStickyDates';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import resetScroll from '../../util/resetScroll'; import resetScroll from '../../util/resetScroll';
import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import useLang, { LangFn } from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useWindowSize from '../../hooks/useWindowSize'; import useWindowSize from '../../hooks/useWindowSize';
import useBackgroundMode from '../../hooks/useBackgroundMode';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import MessageScroll from './MessageScroll'; import MessageListContent from './MessageListContent';
import Message from './message/Message';
import ActionMessage from './ActionMessage';
import './MessageList.scss'; import './MessageList.scss';
@ -96,16 +77,12 @@ type StateProps = {
hasLinkedChat?: boolean; hasLinkedChat?: boolean;
}; };
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, 'loadViewportMessages' | 'setScrollOffset' | 'openHistoryCalendar'>;
'loadViewportMessages' | 'markMessageListRead' | 'markMessagesRead' | 'setScrollOffset' | 'openHistoryCalendar'
)>;
const BOTTOM_THRESHOLD = 100; const BOTTOM_THRESHOLD = 100;
const UNREAD_DIVIDER_TOP = 10; const UNREAD_DIVIDER_TOP = 10;
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60; const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
const SCROLL_DEBOUNCE = 200; 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 FOCUSING_DURATION = 1000;
const BOTTOM_FOCUS_MARGIN = 20; const BOTTOM_FOCUS_MARGIN = 20;
const SELECT_MODE_ANIMATION_DURATION = 200; const SELECT_MODE_ANIMATION_DURATION = 200;
@ -137,8 +114,6 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
isSelectModeActive, isSelectModeActive,
animationLevel, animationLevel,
loadViewportMessages, loadViewportMessages,
markMessageListRead,
markMessagesRead,
setScrollOffset, setScrollOffset,
lastMessage, lastMessage,
botDescription, botDescription,
@ -184,65 +159,10 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
} }
}, [firstUnreadId]); }, [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(() => { useOnChange(() => {
memoFocusingIdRef.current = focusingId; memoFocusingIdRef.current = focusingId;
}, [focusingId]); }, [focusingId]);
const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
});
const messageGroups = useMemo(() => { const messageGroups = useMemo(() => {
if (!messageIds || !messagesById) { if (!messageIds || !messagesById) {
return undefined; return undefined;
@ -260,15 +180,14 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current); return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current);
}, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]); }, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]);
const [loadMoreBackwards, loadMoreForwards, loadMoreAround] = useMemo( const loadMoreAround = useMemo(() => {
() => (type === 'thread' ? [ if (type !== 'thread') {
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Backwards }), 1000, true, false), return undefined;
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Forwards }), 1000, true, false), }
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false),
] : []), return debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[loadViewportMessages, messageIds], }, [loadViewportMessages, messageIds]);
);
const { isScrolled, updateStickyDates } = useStickyDates(); const { isScrolled, updateStickyDates } = useStickyDates();
@ -535,37 +454,26 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
) : messageIds && !messageGroups ? ( ) : messageIds && !messageGroups ? (
<div className="empty"><span>{lang('NoMessages')}</span></div> <div className="empty"><span>{lang('NoMessages')}</span></div>
) : ((messageIds && messageGroups) || lastMessage) ? ( ) : ((messageIds && messageGroups) || lastMessage) ? (
<MessageScroll <MessageListContent
containerRef={containerRef}
className="messages-container"
messageIds={messageIds || [lastMessage!.id]} messageIds={messageIds || [lastMessage!.id]}
loadMoreForwards={loadMoreForwards} messageGroups={messageGroups || groupMessages([lastMessage!])}
loadMoreBackwards={loadMoreBackwards} isViewportNewest={Boolean(isViewportNewest)}
isViewportNewest={isViewportNewest} isUnread={Boolean(firstUnreadId)}
firstUnreadId={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} onFabToggle={onFabToggle}
onNotchToggle={onNotchToggle} onNotchToggle={onNotchToggle}
> openHistoryCalendar={openHistoryCalendar}
{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>
) : ( ) : (
<Loading color="white" /> <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>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): StateProps => { (global, { chatId, threadId, type }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -797,8 +545,6 @@ export default memo(withGlobal<OwnProps>(
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [
'loadViewportMessages', 'loadViewportMessages',
'markMessageListRead',
'markMessagesRead',
'setScrollOffset', 'setScrollOffset',
'openHistoryCalendar', '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 { RefObject } from 'react';
import React, { FC, useCallback, useRef } from '../../lib/teact/teact'; import { getDispatch } from '../../../lib/teact/teactn';
import { useCallback, useMemo, useRef } from '../../../lib/teact/teact';
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../config'; import { LoadMoreDirection } from '../../../types';
import resetScroll from '../../util/resetScroll'; import { MessageListType } from '../../../global/types';
import { useIntersectionObserver, useOnIntersect } from '../../hooks/useIntersectionObserver';
import useOnChange from '../../hooks/useOnChange';
type OwnProps = { import { debounce } from '../../../util/schedulers';
containerRef: MutableRefObject<HTMLDivElement | null>; import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
className: string; import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../config';
messageIds: number[]; import resetScroll from '../../../util/resetScroll';
loadMoreForwards?: NoneToVoidFunction; import useOnChange from '../../../hooks/useOnChange';
loadMoreBackwards?: NoneToVoidFunction;
isViewportNewest?: boolean;
firstUnreadId?: number;
onFabToggle: AnyToVoidFunction;
onNotchToggle: AnyToVoidFunction;
children: any;
};
const FAB_THRESHOLD = 50; const FAB_THRESHOLD = 50;
const TOOLS_FREEZE_TIMEOUT = 100; const TOOLS_FREEZE_TIMEOUT = 100;
const MessageScroll: FC<OwnProps> = ({ export default function useScrollHooks(
containerRef, type: MessageListType,
className, containerRef: RefObject<HTMLDivElement>,
messageIds, messageIds: number[],
loadMoreForwards, isViewportNewest: boolean,
loadMoreBackwards, isUnread: boolean,
isViewportNewest, onFabToggle: AnyToVoidFunction,
firstUnreadId, onNotchToggle: AnyToVoidFunction,
onFabToggle, ) {
onNotchToggle, const { loadViewportMessages } = getDispatch();
children,
}) => { 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 // eslint-disable-next-line no-null/no-null
const backwardsTriggerRef = useRef<HTMLDivElement>(null); const backwardsTriggerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -59,9 +59,9 @@ const MessageScroll: FC<OwnProps> = ({
const isNearBottom = scrollBottom <= FAB_THRESHOLD; const isNearBottom = scrollBottom <= FAB_THRESHOLD;
const isAtBottom = scrollBottom <= 0; const isAtBottom = scrollBottom <= 0;
onFabToggle(firstUnreadId ? !isAtBottom : !isNearBottom); onFabToggle(isUnread ? !isAtBottom : !isNearBottom);
onNotchToggle(!isAtBottom); onNotchToggle(!isAtBottom);
}, [messageIds, isViewportNewest, containerRef, onFabToggle, firstUnreadId, onNotchToggle]); }, [messageIds, isViewportNewest, containerRef, onFabToggle, isUnread, onNotchToggle]);
const { const {
observe: observeIntersection, observe: observeIntersection,
@ -124,22 +124,5 @@ const MessageScroll: FC<OwnProps> = ({
}, TOOLS_FREEZE_TIMEOUT); }, TOOLS_FREEZE_TIMEOUT);
}, [messageIds]); }, [messageIds]);
return ( return { backwardsTriggerRef, forwardsTriggerRef, fabTriggerRef };
<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;