285 lines
9.9 KiB
TypeScript
285 lines
9.9 KiB
TypeScript
import type { RefObject } from 'react';
|
|
import type { FC } from '../../lib/teact/teact';
|
|
import React, { memo } from '../../lib/teact/teact';
|
|
import { getActions } from '../../global';
|
|
|
|
import type { MessageListType } from '../../global/types';
|
|
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
|
|
|
|
import { SCHEDULED_WHEN_ONLINE } from '../../config';
|
|
import { MAIN_THREAD_ID } from '../../api/types';
|
|
import buildClassName from '../../util/buildClassName';
|
|
import { compact } from '../../util/iteratees';
|
|
import { formatHumanDate } from '../../util/dateFormat';
|
|
import {
|
|
getMessageHtmlId, getMessageOriginalId, isActionMessage, isOwnMessage, isServiceNotificationMessage,
|
|
} from '../../global/helpers';
|
|
import useLang from '../../hooks/useLang';
|
|
import type { MessageDateGroup } from './helpers/groupMessages';
|
|
import { isAlbum } from './helpers/groupMessages';
|
|
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
|
import useScrollHooks from './hooks/useScrollHooks';
|
|
import useMessageObservers from './hooks/useMessageObservers';
|
|
|
|
import Message from './message/Message';
|
|
import SponsoredMessage from './message/SponsoredMessage';
|
|
import ActionMessage from './ActionMessage';
|
|
|
|
interface OwnProps {
|
|
isCurrentUserPremium?: boolean;
|
|
chatId: string;
|
|
threadId: number;
|
|
messageIds: number[];
|
|
messageGroups: MessageDateGroup[];
|
|
isViewportNewest: boolean;
|
|
isUnread: boolean;
|
|
withUsers: boolean;
|
|
isChannelChat: boolean | undefined;
|
|
isComments?: boolean;
|
|
noAvatars: boolean;
|
|
containerRef: RefObject<HTMLDivElement>;
|
|
anchorIdRef: { current: string | undefined };
|
|
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
|
memoFirstUnreadIdRef: { current: number | undefined };
|
|
type: MessageListType;
|
|
isReady: boolean;
|
|
threadTopMessageId: number | undefined;
|
|
hasLinkedChat: boolean | undefined;
|
|
isSchedule: boolean;
|
|
noAppearanceAnimation: boolean;
|
|
onFabToggle: AnyToVoidFunction;
|
|
onNotchToggle: AnyToVoidFunction;
|
|
onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
|
|
}
|
|
|
|
const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
|
|
|
const MessageListContent: FC<OwnProps> = ({
|
|
isCurrentUserPremium,
|
|
chatId,
|
|
threadId,
|
|
messageIds,
|
|
messageGroups,
|
|
isViewportNewest,
|
|
isUnread,
|
|
isComments,
|
|
withUsers,
|
|
isChannelChat,
|
|
noAvatars,
|
|
containerRef,
|
|
anchorIdRef,
|
|
memoUnreadDividerBeforeIdRef,
|
|
memoFirstUnreadIdRef,
|
|
type,
|
|
isReady,
|
|
threadTopMessageId,
|
|
hasLinkedChat,
|
|
isSchedule,
|
|
noAppearanceAnimation,
|
|
onFabToggle,
|
|
onNotchToggle,
|
|
onPinnedIntersectionChange,
|
|
}) => {
|
|
const { openHistoryCalendar } = getActions();
|
|
|
|
const {
|
|
observeIntersectionForReading,
|
|
observeIntersectionForLoading,
|
|
observeIntersectionForPlaying,
|
|
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onPinnedIntersectionChange);
|
|
|
|
const {
|
|
backwardsTriggerRef,
|
|
forwardsTriggerRef,
|
|
fabTriggerRef,
|
|
} = useScrollHooks(
|
|
type,
|
|
containerRef,
|
|
messageIds,
|
|
isViewportNewest,
|
|
isUnread,
|
|
onFabToggle,
|
|
onNotchToggle,
|
|
isReady,
|
|
);
|
|
|
|
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 + messageGroup.senderGroups.flat().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])
|
|
&& !senderGroup[0].content.action?.phoneCall
|
|
) {
|
|
const message = senderGroup[0]!;
|
|
const isLastInList = (
|
|
senderGroupIndex === senderGroupsArray.length - 1
|
|
&& dateGroupIndex === dateGroupsArray.length - 1
|
|
);
|
|
|
|
return compact([
|
|
message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
|
|
<ActionMessage
|
|
key={message.id}
|
|
message={message}
|
|
threadId={threadId}
|
|
messageListType={type}
|
|
isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID)}
|
|
observeIntersectionForReading={observeIntersectionForReading}
|
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
|
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
|
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
|
isLastInList={isLastInList}
|
|
onPinnedIntersectionChange={onPinnedIntersectionChange}
|
|
/>,
|
|
]);
|
|
}
|
|
|
|
let currentDocumentGroupId: string | undefined;
|
|
|
|
return 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 === getMessageHtmlId(message.previousLocalId)) {
|
|
anchorIdRef.current = getMessageHtmlId(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);
|
|
// Service notifications saved in cache in previous versions may share the same `previousLocalId`
|
|
const key = isServiceNotificationMessage(message) ? `${message.date}_${originalId}` : originalId;
|
|
|
|
const noComments = hasLinkedChat === false || !isChannelChat;
|
|
|
|
const isTopicTopMessage = message.id === threadTopMessageId;
|
|
|
|
return compact([
|
|
message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
|
|
<Message
|
|
key={key}
|
|
message={message}
|
|
observeIntersectionForBottom={observeIntersectionForReading}
|
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
|
album={album}
|
|
noAvatars={noAvatars}
|
|
withAvatar={position.isLastInGroup && withUsers && !isOwn && (!isTopicTopMessage || !isComments)}
|
|
withSenderName={position.isFirstInGroup && withUsers && !isOwn}
|
|
threadId={threadId}
|
|
messageListType={type}
|
|
noComments={noComments}
|
|
noReplies={!noComments || threadId !== MAIN_THREAD_ID}
|
|
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
|
isFirstInGroup={position.isFirstInGroup}
|
|
isLastInGroup={position.isLastInGroup}
|
|
isFirstInDocumentGroup={position.isFirstInDocumentGroup}
|
|
isLastInDocumentGroup={position.isLastInDocumentGroup}
|
|
isLastInList={position.isLastInList}
|
|
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
|
onPinnedIntersectionChange={onPinnedIntersectionChange}
|
|
/>,
|
|
message.id === threadTopMessageId && (
|
|
<div className="local-action-message" key="discussion-started">
|
|
<span>{lang('DiscussionStarted')}</span>
|
|
</div>
|
|
),
|
|
]);
|
|
}).flat();
|
|
});
|
|
|
|
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>
|
|
{senderGroups.flat()}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<div className="messages-container" teactFastList>
|
|
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
|
|
{dateGroups.flat()}
|
|
{!isCurrentUserPremium && isViewportNewest && (
|
|
<SponsoredMessage key={chatId} chatId={chatId} containerRef={containerRef} />
|
|
)}
|
|
<div
|
|
ref={forwardsTriggerRef}
|
|
key="forwards-trigger"
|
|
className="forwards-trigger"
|
|
/>
|
|
<div
|
|
ref={fabTriggerRef}
|
|
key="fab-trigger"
|
|
className="fab-trigger"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(MessageListContent);
|