TelegramPWA/src/components/middle/MessageList.tsx
2021-05-11 23:17:56 +03:00

786 lines
26 KiB
TypeScript

import React, {
FC, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../lib/teact/teactn';
import { ApiMessage, ApiRestrictionReason, MAIN_THREAD_ID } from '../../api/types';
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_IOS, IS_MOBILE_SCREEN } from '../../util/environment';
import {
selectChatMessages,
selectIsViewportNewest,
selectFirstUnreadId,
selectFocusedMessageId,
selectChat,
selectIsInSelectMode,
selectIsChatWithSelf,
selectChatBot,
selectIsChatBotNotStarted,
selectScrollOffset,
selectThreadTopMessageId,
selectFirstMessageId,
selectScheduledMessages, selectCurrentMessageIds,
} from '../../modules/selectors';
import {
getMessageOriginalId,
isActionMessage,
isChatChannel,
isChatPrivate,
isOwnMessage,
getCanPostInChat,
} from '../../modules/helpers';
import {
compact,
flatten,
orderBy,
pick,
} from '../../util/iteratees';
import {
fastRaf, debounce, throttleWithTickEnd, onTickEnd,
} from '../../util/schedulers';
import { formatHumanDate } from '../../util/dateFormat';
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
import buildClassName from '../../util/buildClassName';
import { groupMessages, MessageDateGroup, isAlbum } from './helpers/groupMessages';
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 Loading from '../ui/Loading';
import MessageScroll from './MessageScroll';
import Message from './message/Message';
import ActionMessage from './ActionMessage';
import './MessageList.scss';
type OwnProps = {
chatId: number;
threadId: number;
type: MessageListType;
onFabToggle: (show: boolean) => void;
hasTools?: boolean;
bottomOffset: 'none' | 'small' | 'big';
};
type StateProps = {
isChatLoaded?: boolean;
isChannelChat?: boolean;
canPost?: boolean;
isChatWithSelf?: boolean;
messageIds?: number[];
messagesById?: Record<number, ApiMessage>;
firstUnreadId?: number;
isViewportNewest?: boolean;
isRestricted?: boolean;
restrictionReason?: ApiRestrictionReason;
focusingId?: number;
isSelectModeActive?: boolean;
animationLevel?: number;
lastMessage?: ApiMessage;
botDescription?: string;
threadTopMessageId?: number;
threadFirstMessageId?: number;
hasLinkedChat?: boolean;
};
type DispatchProps = Pick<GlobalActions, (
'loadViewportMessages' | 'markMessageListRead' | 'markMessagesRead' | 'setScrollOffset'
)>;
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_MOBILE_SCREEN ? 300 : 500;
const FOCUSING_DURATION = 1000;
const BOTTOM_FOCUS_MARGIN = 20;
const SELECT_MODE_ANIMATION_DURATION = 200;
const FOCUSING_FADE_ANIMATION_DURATION = 200;
const UNREAD_DIVIDER_CLASS = 'unread-divider';
const runDebouncedForScroll = debounce((cb) => cb(), SCROLL_DEBOUNCE, false);
const runThrottledOnTickEnd = throttleWithTickEnd((cb) => cb());
const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
chatId,
threadId,
type,
hasTools,
onFabToggle,
isChatLoaded,
isChannelChat,
canPost,
bottomOffset,
isChatWithSelf,
messageIds,
messagesById,
firstUnreadId,
isViewportNewest,
threadFirstMessageId,
isRestricted,
restrictionReason,
focusingId,
isSelectModeActive,
animationLevel,
loadViewportMessages,
markMessageListRead,
markMessagesRead,
setScrollOffset,
lastMessage,
botDescription,
threadTopMessageId,
hasLinkedChat,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// We update local cached `scrollOffsetRef` when opening chat.
// Then we update global version every second on scrolling.
const scrollOffsetRef = useRef<number>((type === 'thread' && selectScrollOffset(getGlobal(), chatId, threadId)) || 0);
const anchorIdRef = useRef<string>();
const anchorTopRef = useRef<number>();
const listItemElementsRef = useRef<HTMLDivElement[]>();
const memoUnreadDividerBeforeIdRef = useRef<number | undefined>();
// Updated every time (to be used from intersection callback closure)
const memoFirstUnreadIdRef = useRef<number>();
const memoFocusingIdRef = useRef<number>();
const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(!messageIds);
const [containerHeight, setContainerHeight] = useState<number | undefined>();
const [hasFocusing, setHasFocusing] = useState<boolean>(Boolean(focusingId));
const areMessagesLoaded = Boolean(messageIds);
useOnChange(() => {
// We only need it first time when message list appears
if (areMessagesLoaded) {
onTickEnd(() => {
shouldAnimateAppearanceRef.current = false;
});
}
}, [areMessagesLoaded]);
useOnChange(() => {
memoFirstUnreadIdRef.current = firstUnreadId;
// Updated only once (to preserve divider even after messages are read)
if (!memoUnreadDividerBeforeIdRef.current) {
memoUnreadDividerBeforeIdRef.current = firstUnreadId;
}
}, [firstUnreadId]);
const {
observe: observeIntersectionForMedia, freeze: freezeForMedia, unfreeze: unfreezeForMedia,
} = 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 });
}
});
useOnChange(() => {
memoFocusingIdRef.current = focusingId;
if (focusingId) {
freezeForMedia();
freezeForReading();
} else {
unfreezeForReading();
unfreezeForMedia();
}
}, [focusingId]);
const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
});
useEffect(() => {
if (focusingId) {
setHasFocusing(true);
} else {
setTimeout(() => {
setHasFocusing(false);
}, FOCUSING_FADE_ANIMATION_DURATION);
}
}, [focusingId]);
const messageGroups = useMemo(() => {
if (!messageIds || !messagesById) {
return undefined;
}
const viewportIds = threadTopMessageId && (!messageIds[0] || threadFirstMessageId === messageIds[0])
? [threadTopMessageId, ...messageIds]
: messageIds;
if (!viewportIds.length) {
return undefined;
}
const listedMessages = viewportIds.map((id) => messagesById[id]);
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),
] : []),
// eslint-disable-next-line react-hooks/exhaustive-deps
[loadViewportMessages, messageIds],
);
const { isScrolled, updateStickyDates } = useStickyDates();
const handleScroll = useCallback(() => {
if (isScrollTopJustUpdatedRef.current) {
isScrollTopJustUpdatedRef.current = false;
return;
}
const container = containerRef.current!;
if (!memoFocusingIdRef.current) {
updateStickyDates(container, hasTools);
}
runDebouncedForScroll(() => {
fastRaf(() => {
if (!container.parentElement) {
return;
}
scrollOffsetRef.current = container.scrollHeight - container.scrollTop;
if (type === 'thread') {
setScrollOffset({ chatId, threadId, scrollOffset: scrollOffsetRef.current });
}
});
});
}, [updateStickyDates, hasTools, type, setScrollOffset, chatId, threadId]);
// Container resize observer (caused by Composer reply/webpage panels)
useEffect(() => {
if (!('ResizeObserver' in window) || process.env.APP_ENV === 'perf') {
return undefined;
}
const observer = new ResizeObserver(([entry]) => {
// During animation
if (!(entry.target as HTMLDivElement).offsetParent) {
return;
}
setContainerHeight(entry.contentRect.height);
});
observer.observe(containerRef.current!);
return () => {
observer.disconnect();
};
}, []);
useLayoutEffect(() => {
containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight);
}, []);
// Workaround for an iOS bug when animated stickers sometimes disappear
useLayoutEffect(() => {
if (!IS_IOS) {
return;
}
runThrottledOnTickEnd(() => {
if (!(containerRef.current as HTMLDivElement).querySelector('.AnimatedSticker.is-playing')) {
return;
}
const style = (containerRef.current as HTMLDivElement).style as any;
style.webkitOverflowScrolling = style.webkitOverflowScrolling === 'auto' ? '' : 'auto';
});
});
// Initial message loading
useEffect(() => {
if (!loadMoreAround || !isChatLoaded || isRestricted || focusingId) {
return;
}
const container = containerRef.current!;
if (!messageIds || (
messageIds.length < MESSAGE_LIST_SLICE / 2
&& (container.firstElementChild as HTMLDivElement).clientHeight <= container.offsetHeight
)) {
loadMoreAround();
}
}, [isChatLoaded, messageIds, loadMoreAround, focusingId, isRestricted]);
// Handles updated message list, takes care of scroll repositioning
useLayoutEffectWithPrevDeps(([
prevMessageIds, prevIsViewportNewest, prevContainerHeight,
]: [
typeof messageIds, typeof isViewportNewest, typeof containerHeight
]) => {
const container = containerRef.current!;
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
// During animation
if (!container.offsetParent) {
return;
}
// Add extra height when few messages to allow smooth scroll animation. Uses assumption that `parentElement`
// is a Transition slide and its CSS class can not be reset in a declarative way.
const shouldForceScroll = (
isViewportNewest
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
&& !container.parentElement!.classList.contains('force-messages-scroll')
&& (container.firstElementChild as HTMLDivElement)!.clientHeight <= container.offsetHeight * 2
);
if (shouldForceScroll) {
container.parentElement!.classList.add('force-messages-scroll');
setTimeout(() => {
if (container.parentElement) {
container.parentElement.classList.remove('force-messages-scroll');
}
}, FOCUSING_DURATION);
}
const { scrollTop, scrollHeight, offsetHeight } = container;
const scrollOffset = scrollOffsetRef.current!;
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
// If two messages come at once (e.g. via Quiz Bot) then the first message will update `scrollOffset`
// right away (before animation) which creates inconsistency until the animation completes.
// To workaround that, we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0;
const isAtBottom = isViewportNewest && prevIsViewportNewest && (
scrollOffset - (prevContainerHeight || offsetHeight) - lastItemHeight <= BOTTOM_THRESHOLD
);
let newScrollTop!: number;
const hasFirstMessageChanged = messageIds && prevMessageIds && messageIds[0] !== prevMessageIds[0];
const hasLastMessageChanged = (
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
);
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
if (isAtBottom && hasLastMessageChanged && !hasFirstMessageChanged && !isAlreadyFocusing) {
if (lastItemElement) {
fastRaf(() => {
fastSmoothScroll(
container,
lastItemElement,
'end',
BOTTOM_FOCUS_MARGIN,
undefined,
undefined,
undefined,
true,
);
});
}
newScrollTop = scrollHeight - offsetHeight;
scrollOffsetRef.current = Math.max(scrollHeight - newScrollTop, offsetHeight);
// Scroll still needs to be restored after container resize
if (!shouldForceScroll) {
return;
}
}
if (process.env.APP_ENV === 'perf') {
// eslint-disable-next-line no-console
console.time('scrollTop');
}
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight;
const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`);
const unreadDivider = (
!anchor
&& memoUnreadDividerBeforeIdRef.current
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
);
if (isAtBottom && isResized) {
if (isAnimatingScroll()) {
return;
}
newScrollTop = scrollHeight - offsetHeight;
} else if (anchor) {
const newAnchorTop = anchor.getBoundingClientRect().top;
newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0));
} else if (unreadDivider) {
newScrollTop = unreadDivider.offsetTop - (hasTools ? UNREAD_DIVIDER_TOP_WITH_TOOLS : UNREAD_DIVIDER_TOP);
} else {
newScrollTop = scrollHeight - scrollOffset;
}
resetScroll(container, newScrollTop);
if (!memoFocusingIdRef.current) {
isScrollTopJustUpdatedRef.current = true;
fastRaf(() => {
isScrollTopJustUpdatedRef.current = false;
});
}
scrollOffsetRef.current = Math.max(scrollHeight - newScrollTop, offsetHeight);
if (process.env.APP_ENV === 'perf') {
// eslint-disable-next-line no-console
console.timeEnd('scrollTop');
}
}, [messageIds, isViewportNewest, containerHeight, hasTools]);
useEffect(() => {
if (!animationLevel || animationLevel > 0) {
dispatchHeavyAnimationEvent(SELECT_MODE_ANIMATION_DURATION + ANIMATION_END_DELAY);
}
}, [animationLevel, isSelectModeActive]);
const lang = useLang();
const isPrivate = Boolean(chatId && isChatPrivate(chatId));
const withUsers = Boolean((!isPrivate && !isChannelChat) || isChatWithSelf);
const className = buildClassName(
'MessageList custom-scroll',
!withUsers && 'no-avatars',
isChannelChat && 'no-avatars',
(!canPost || bottomOffset !== 'none') && 'bottom-padding',
(bottomOffset !== 'none') && `bottom-padding-${bottomOffset}`,
isSelectModeActive && 'select-mode-active',
hasFocusing && 'has-focusing',
isScrolled && 'scrolled',
);
return (
<div ref={containerRef} className={className} onScroll={handleScroll}>
{isRestricted ? (
<div className="empty">
<span>
{restrictionReason ? restrictionReason.text : `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
</span>
</div>
) : botDescription ? (
<div className="empty rich"><span>{renderText(lang(botDescription), ['br', 'emoji', 'links'])}</span></div>
) : messageIds && !messageGroups ? (
<div className="empty"><span>{lang('NoMessages')}</span></div>
) : ((messageIds && messageGroups) || lastMessage) ? (
<MessageScroll
containerRef={containerRef}
className="messages-container"
messageIds={messageIds || [lastMessage!.id]}
containerHeight={containerHeight}
listItemElementsRef={listItemElementsRef}
focusingId={focusingId}
anchorIdRef={anchorIdRef}
anchorTopRef={anchorTopRef}
loadMoreForwards={loadMoreForwards}
loadMoreBackwards={loadMoreBackwards}
isViewportNewest={isViewportNewest}
firstUnreadId={firstUnreadId}
onFabToggle={onFabToggle}
>
{renderMessages(
lang,
messageGroups || groupMessages([lastMessage!]),
observeIntersectionForReading,
observeIntersectionForMedia,
observeIntersectionForAnimatedStickers,
withUsers,
anchorIdRef,
memoUnreadDividerBeforeIdRef,
threadId,
type,
threadTopMessageId,
threadFirstMessageId,
hasLinkedChat,
messageGroups ? type === 'scheduled' : false,
!messageGroups || !shouldAnimateAppearanceRef.current,
)}
</MessageScroll>
) : (
<Loading color="white" />
)}
</div>
);
};
function renderMessages(
lang: LangFn,
messageGroups: MessageDateGroup[],
observeIntersectionForReading: ObserveFn,
observeIntersectionForMedia: ObserveFn,
observeIntersectionForAnimatedStickers: ObserveFn,
withUsers: boolean,
currentAnchorIdRef: { current: string | undefined },
memoFirstUnreadIdRef: { current: number | undefined },
threadId: number,
type: MessageListType,
threadTopMessageId?: number,
threadFirstMessageId?: number,
hasLinkedChat?: boolean,
isSchedule = false,
noAppearanceAnimation = false,
) {
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];
return compact([
message.id === memoFirstUnreadIdRef.current && unreadDivider,
<ActionMessage
key={message.id}
message={message}
observeIntersection={observeIntersectionForReading}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
/>,
]);
}
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 shouldRenderUnreadDivider = (
(message.id === memoFirstUnreadIdRef.current && memoFirstUnreadIdRef.current !== threadFirstMessageId)
|| (message.id === threadTopMessageId && memoFirstUnreadIdRef.current === threadFirstMessageId)
);
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([
shouldRenderUnreadDivider && unreadDivider,
<Message
key={key}
message={message}
observeIntersectionForBottom={observeIntersectionForReading}
observeIntersectionForMedia={observeIntersectionForMedia}
observeIntersectionForAnimatedStickers={observeIntersectionForAnimatedStickers}
album={album}
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}
teactFastList
>
<div className="sticky-date" key="date-header">
<span>
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledUntilOnline')
)}
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledOn', formatHumanDate(dateGroup.datetime, undefined, true))
)}
{!isSchedule && formatHumanDate(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);
if (!chat) {
return {};
}
const messageIds = selectCurrentMessageIds(global, chatId, threadId, type);
const messagesById = type === 'scheduled'
? selectScheduledMessages(global, chatId)
: selectChatMessages(global, chatId);
const threadTopMessageId = selectThreadTopMessageId(global, chatId, threadId);
if (
threadId !== MAIN_THREAD_ID
&& !(messagesById && threadTopMessageId && messagesById[threadTopMessageId])
) {
return {};
}
const { isRestricted, restrictionReason, lastMessage } = chat;
const focusingId = selectFocusedMessageId(global, chatId);
const withLastMessageWhenPreloading = (
threadId === MAIN_THREAD_ID
&& !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId
);
let botDescription: string | undefined;
if (selectIsChatBotNotStarted(global, chatId)) {
const chatBot = selectChatBot(global, chatId)!;
if (chatBot.fullInfo) {
botDescription = chatBot.fullInfo.botDescription || 'NoMessages';
} else {
botDescription = 'Updating bot info...';
}
}
return {
isChatLoaded: true,
isRestricted,
restrictionReason,
isChannelChat: isChatChannel(chat),
canPost: getCanPostInChat(chat, threadId),
isChatWithSelf: selectIsChatWithSelf(global, chatId),
messageIds,
messagesById,
firstUnreadId: selectFirstUnreadId(global, chatId, threadId),
isViewportNewest: type !== 'thread' || selectIsViewportNewest(global, chatId, threadId),
threadFirstMessageId: selectFirstMessageId(global, chatId, threadId),
focusingId,
isSelectModeActive: selectIsInSelectMode(global),
animationLevel: global.settings.byKey.animationLevel,
...(withLastMessageWhenPreloading && { lastMessage }),
botDescription,
threadTopMessageId,
hasLinkedChat: chat.fullInfo && ('linkedChatId' in chat.fullInfo)
? Boolean(chat.fullInfo.linkedChatId)
: undefined,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'loadViewportMessages',
'markMessageListRead',
'markMessagesRead',
'setScrollOffset',
]),
)(MessageList));