[Perf] [Refactoring] Extract messages rendering to a memoized component
This commit is contained in:
parent
b3e2eda5e6
commit
45a0bca14b
@ -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',
|
||||
]),
|
||||
|
||||
241
src/components/middle/MessageListContent.tsx
Normal file
241
src/components/middle/MessageListContent.tsx
Normal 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);
|
||||
80
src/components/middle/hooks/useMessageObservers.ts
Normal file
80
src/components/middle/hooks/useMessageObservers.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user