import type { FC } from '@teact'; import { beginHeavyAnimation, memo, useEffect, useMemo, useRef } from '@teact'; import { addExtraClass, removeExtraClass } from '@teact/teact-dom'; import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic } from '../../api/types'; import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import { MAIN_THREAD_ID } from '../../api/types'; import { LoadMoreDirection, type MessageListType, type ThreadId } from '../../types'; import { ANIMATION_END_DELAY, ANONYMOUS_USER_ID, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID, } from '../../config'; import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom'; import { getIsSavedDialog, getMessageHtmlId, isAnonymousForwardsChat, isChatChannel, isChatGroup, isChatMonoforum, isSystemBot, } from '../../global/helpers'; import { selectBot, selectCanTranslateChat, selectChat, selectChatFullInfo, selectChatLastMessage, selectChatMessages, selectChatScheduledMessages, selectCurrentMessageIds, selectFirstUnreadId, selectFocusedMessageId, selectIsChatProtected, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsCurrentUserPremium, selectIsInSelectMode, selectIsViewportNewest, selectLastScrollOffset, selectMonoforumChannel, selectPerformanceSettingsValue, selectScrollOffset, selectTabState, selectThreadInfo, selectTopic, selectTranslationLanguage, selectUserFullInfo, } from '../../global/selectors'; import { selectIsChatRestricted } from '../../global/selectors/chats'; import { selectActiveRestrictionReasons } from '../../global/selectors/messages'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; import buildClassName from '../../util/buildClassName'; import { isUserId } from '../../util/entities/ids'; import { orderBy } from '../../util/iteratees'; import { isLocalMessageId } from '../../util/keys/messageKey'; import resetScroll from '../../util/resetScroll'; import { debounce, onTickEnd } from '../../util/schedulers'; import getOffsetToContainer from '../../util/visibility/getOffsetToContainer'; import { groupMessages } from './helpers/groupMessages'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import useInterval from '../../hooks/schedulers/useInterval'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useLastCallback from '../../hooks/useLastCallback'; import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps'; import useNativeCopySelectedMessages from '../../hooks/useNativeCopySelectedMessages'; import { useStateRef } from '../../hooks/useStateRef'; import useSyncEffect from '../../hooks/useSyncEffect'; import { isBackgroundModeActive } from '../../hooks/window/useBackgroundMode'; import useContainerHeight from './hooks/useContainerHeight'; import useStickyDates from './hooks/useStickyDates'; import Loading from '../ui/Loading'; import Transition from '../ui/Transition.tsx'; import ContactGreeting from './ContactGreeting'; import MessageListAccountInfo from './MessageListAccountInfo'; import MessageListContent from './MessageListContent'; import NoMessages from './NoMessages'; import RequirementToContactMessage from './RequirementToContactMessage'; import './MessageList.scss'; type OwnProps = { chatId: string; threadId: ThreadId; type: MessageListType; isComments?: boolean; canPost: boolean; isReady: boolean; withBottomShift?: boolean; withDefaultBg: boolean; isContactRequirePremium?: boolean; paidMessagesStars?: number; onScrollDownToggle: BooleanToVoidFunction; onNotchToggle: BooleanToVoidFunction; onIntersectPinnedMessage: OnIntersectPinnedMessage; }; type StateProps = { isChatLoaded?: boolean; isChannelChat?: boolean; isGroupChat?: boolean; isChatMonoforum?: boolean; isChatWithSelf?: boolean; isSystemBotChat?: boolean; isAnonymousForwards?: boolean; isCreator?: boolean; isChannelWithAvatars?: boolean; isBot?: boolean; isNonContact?: boolean; nameChangeDate?: number; photoChangeDate?: number; isSynced?: boolean; messageIds?: number[]; messagesById?: Record; firstUnreadId?: number; isViewportNewest?: boolean; isRestricted?: boolean; restrictionReasons?: ApiRestrictionReason[]; focusingId?: number; isSelectModeActive?: boolean; lastMessage?: ApiMessage; hasLinkedChat?: boolean; topic?: ApiTopic; noMessageSendingAnimation?: boolean; isServiceNotificationsChat?: boolean; isEmptyThread?: boolean; isForum?: boolean; currentUserId: string; isAccountFrozen?: boolean; areAdsEnabled?: boolean; channelJoinInfo?: ApiChatFullInfo['joinInfo']; isChatProtected?: boolean; hasCustomGreeting?: boolean; isAppConfigLoaded?: boolean; monoforumChannelId?: string; canTranslate?: boolean; translationLanguage?: string; shouldAutoTranslate?: boolean; }; enum Content { // eslint-disable-next-line @typescript-eslint/no-shadow Loading, Restricted, StarsRequired, PremiumRequired, AccountInfo, // eslint-disable-next-line @typescript-eslint/no-shadow ContactGreeting, // eslint-disable-next-line @typescript-eslint/no-shadow NoMessages, MessageList, } const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000; const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000; const BOTTOM_THRESHOLD = 50; const UNREAD_DIVIDER_TOP = 10; const SCROLL_DEBOUNCE = 200; const MESSAGE_ANIMATION_DURATION = 500; const BOTTOM_FOCUS_MARGIN = 20; const SELECT_MODE_ANIMATION_DURATION = 200; const UNREAD_DIVIDER_CLASS = 'unread-divider'; const runDebouncedForScroll = debounce((cb) => cb(), SCROLL_DEBOUNCE, false); const MessageList: FC = ({ chatId, threadId, type, isChatLoaded, isForum, isChannelChat, isGroupChat, isChannelWithAvatars, canPost, isSynced, // eslint-disable-next-line @typescript-eslint/no-shadow isChatMonoforum, isReady, isChatWithSelf, isSystemBotChat, isAnonymousForwards, isCreator, isBot, isNonContact, nameChangeDate, photoChangeDate, messageIds, messagesById, firstUnreadId, isComments, isViewportNewest, isRestricted, restrictionReasons, isEmptyThread, focusingId, isSelectModeActive, lastMessage, hasLinkedChat, withBottomShift, withDefaultBg, topic, noMessageSendingAnimation, isServiceNotificationsChat, currentUserId, isContactRequirePremium, paidMessagesStars, areAdsEnabled, channelJoinInfo, isChatProtected, isAccountFrozen, hasCustomGreeting, monoforumChannelId, isAppConfigLoaded, canTranslate, translationLanguage, shouldAutoTranslate, onIntersectPinnedMessage, onScrollDownToggle, onNotchToggle, }) => { const { loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, loadMessageViews, loadPeerStoriesByIds, loadFactChecks, requestChatTranslation, } = getActions(); const containerRef = useRef(); // We update local cached `scrollOffsetRef` when opening chat. // Then we update global version every second on scrolling. const scrollOffsetRef = useRef( (type === 'thread' && selectScrollOffset(getGlobal(), chatId, threadId)) || selectLastScrollOffset(getGlobal(), chatId, threadId) || 0, ); const anchorIdRef = useRef(); const anchorTopRef = useRef(); const listItemElementsRef = useRef(); const memoFirstUnreadIdRef = useRef(); const memoUnreadDividerBeforeIdRef = useRef(); const memoFocusingIdRef = useRef(); const isScrollTopJustUpdatedRef = useRef(false); const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage)); const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId); const hasOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID; const areMessagesLoaded = Boolean(messageIds); const isPrivate = isUserId(chatId); const withUsers = Boolean((!isPrivate && !isChannelChat) || isChatWithSelf || isSystemBotChat || isAnonymousForwards || isChannelWithAvatars); useSyncEffect(() => { // We only need it first time when message list appears if (areMessagesLoaded) { onTickEnd(() => { shouldAnimateAppearanceRef.current = false; }); } }, [areMessagesLoaded]); // Updated every time (to be used from intersection callback closure) useSyncEffect(() => { memoFirstUnreadIdRef.current = firstUnreadId; }, [firstUnreadId]); useEffect(() => { const canHaveAds = isChannelChat || isBot; if (areAdsEnabled && canHaveAds && isSynced && isReady && isAppConfigLoaded) { loadSponsoredMessages({ peerId: chatId }); } }, [chatId, isSynced, isReady, isChannelChat, isBot, areAdsEnabled, isAppConfigLoaded]); // Updated only once when messages are loaded (as we want the unread divider to keep its position) useSyncEffect(() => { if (areMessagesLoaded) { memoUnreadDividerBeforeIdRef.current = memoFirstUnreadIdRef.current; } }, [areMessagesLoaded]); useSyncEffect(() => { memoFocusingIdRef.current = focusingId; }, [focusingId]); // Enable auto translation for the chat if it's available useEffect(() => { if (!shouldAutoTranslate || !canTranslate) return; requestChatTranslation({ chatId, toLanguageCode: translationLanguage }); }, [shouldAutoTranslate, canTranslate, translationLanguage, chatId]); useNativeCopySelectedMessages(copyMessagesByIds); const messageGroups = useMemo(() => { if (!messageIds?.length || !messagesById) { return undefined; } const listedMessages: ApiMessage[] = []; messageIds.forEach((id, index, arr) => { const prevMessage = listedMessages[listedMessages.length - 1]; const message = messagesById[id]; if (!message) { return; } const { shouldAppendJoinMessage, shouldAppendJoinMessageAfterCurrent } = (() => { if (!channelJoinInfo || type !== 'thread') return undefined; if (prevMessage && prevMessage.date < channelJoinInfo.joinedDate && channelJoinInfo.joinedDate <= message.date) { return { shouldAppendJoinMessage: true, shouldAppendJoinMessageAfterCurrent: false }; } if (index === arr.length - 1 && message.date < channelJoinInfo.joinedDate) { return { shouldAppendJoinMessage: true, shouldAppendJoinMessageAfterCurrent: true, }; } return undefined; })() || {}; if (shouldAppendJoinMessageAfterCurrent) { listedMessages.push(message); } if (shouldAppendJoinMessage) { const lastMessageId = shouldAppendJoinMessageAfterCurrent ? message.id : (prevMessage?.id || (message.id - 1)); listedMessages.push({ id: generateChannelJoinMessageId(lastMessageId), chatId: message.chatId, date: channelJoinInfo!.joinedDate, isOutgoing: false, content: { action: { mediaType: 'action', type: 'channelJoined', inviterId: channelJoinInfo?.inviterId, isViaRequest: channelJoinInfo?.isViaRequest || undefined, }, }, } satisfies ApiMessage); } if (!shouldAppendJoinMessageAfterCurrent) { listedMessages.push(message); } }); // Service notifications have local IDs which may be not in sync with real message history const orderRule: (keyof ApiMessage)[] = type === 'scheduled' || isServiceNotificationsChat ? ['date', 'id'] : ['id']; return listedMessages.length ? groupMessages( orderBy(listedMessages, orderRule), memoUnreadDividerBeforeIdRef.current, !isForum ? Number(threadId) : undefined, isChatWithSelf, withUsers, ) : undefined; }, [withUsers, messageIds, messagesById, type, isServiceNotificationsChat, isForum, threadId, isChatWithSelf, channelJoinInfo]); useInterval(() => { if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen) return; if (!isChannelChat && !isGroupChat) return; const ids = messageIds.filter((id) => { const message = messagesById[id]; return message && message.reactions && !message.content.action; }); if (!ids.length) return; loadMessageReactions({ chatId, ids }); }, MESSAGE_REACTIONS_POLLING_INTERVAL); useInterval(() => { if (!messageIds || !messagesById || type === 'scheduled') { return; } const storyDataList = messageIds.map((id) => messagesById[id]?.content.storyData).filter(Boolean); if (!storyDataList.length) return; const storiesByPeerIds = storyDataList.reduce((acc, storyData) => { const { peerId, id } = storyData; if (!acc[peerId]) { acc[peerId] = []; } acc[peerId].push(id); return acc; }, {} as Record); Object.entries(storiesByPeerIds).forEach(([peerId, storyIds]) => { loadPeerStoriesByIds({ peerId, storyIds }); }); }, MESSAGE_STORY_POLLING_INTERVAL); useInterval(() => { if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') { return; } const global = getGlobal(); const ids = messageIds.filter((id) => selectThreadInfo(global, chatId, id)?.isCommentsInfo || messagesById[id]?.viewsCount !== undefined); if (!ids.length) return; loadMessageViews({ chatId, ids }); }, MESSAGE_COMMENTS_POLLING_INTERVAL, true); useInterval(() => { if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') { return; } const ids = messageIds.filter((id) => messagesById[id]?.factCheck?.shouldFetch); if (!ids.length) return; loadFactChecks({ chatId, ids }); }, MESSAGE_FACT_CHECK_UPDATE_INTERVAL); const loadMoreAround = useMemo(() => { if (type !== 'thread') { return undefined; } return debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false); // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps }, [loadViewportMessages, messageIds]); const { isScrolled, updateStickyDates } = useStickyDates(); const handleScroll = useLastCallback(() => { if (isScrollTopJustUpdatedRef.current) { isScrollTopJustUpdatedRef.current = false; return; } const container = containerRef.current; if (!container) { return; } if (!memoFocusingIdRef.current) { updateStickyDates(container); } runDebouncedForScroll(() => { const global = getGlobal(); const isFocusing = Boolean(selectTabState(global).focusedMessage?.chatId); if (!isFocusing) { onIntersectPinnedMessage({ shouldCancelWaiting: true }); } if (!container.parentElement) { return; } scrollOffsetRef.current = container.scrollHeight - container.scrollTop; if (type === 'thread') { setScrollOffset({ chatId, threadId, scrollOffset: scrollOffsetRef.current }); } }); }); const [getContainerHeight, prevContainerHeightRef] = useContainerHeight(containerRef, canPost && !isSelectModeActive); // Initial message loading useEffect(() => { if (!loadMoreAround || !isChatLoaded || isRestricted || focusingId) { return; } // Loading history while sending a message can return the same message and cause ambiguity const isLastMessageLocal = messageIds && isLocalMessageId(messageIds[messageIds.length - 1]); if (isLastMessageLocal) { return; } const container = containerRef.current!; if (!messageIds || messageIds.length === 1 || ( messageIds.length < MESSAGE_LIST_SLICE / 2 && (container.firstElementChild as HTMLDivElement).clientHeight <= container.offsetHeight )) { loadMoreAround(); } }, [isChatLoaded, messageIds, loadMoreAround, focusingId, isRestricted]); const rememberScrollPositionRef = useStateRef(() => { if (!messageIds || !listItemElementsRef.current) { return; } const preservedItemElements = listItemElementsRef.current .filter((element) => messageIds.includes(Number(element.dataset.messageId))); // We avoid the very first item as it may be a partly-loaded album // and also because it may be removed when messages limit is reached const anchor = preservedItemElements[1] || preservedItemElements[0]; if (!anchor) { return; } anchorIdRef.current = anchor.id; anchorTopRef.current = anchor.getBoundingClientRect().top; }); useSyncEffect( () => forceMeasure(() => rememberScrollPositionRef.current()), // This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below [messageIds, isViewportNewest, rememberScrollPositionRef], ); useEffect( () => rememberScrollPositionRef.current(), // This is only needed to react on signal updates [getContainerHeight, rememberScrollPositionRef], ); // Handles updated message list, takes care of scroll repositioning useLayoutEffectWithPrevDeps(([prevMessageIds, prevIsViewportNewest]) => { if (process.env.APP_ENV === 'perf') { // eslint-disable-next-line no-console console.time('scrollTop'); } const containerHeight = getContainerHeight(); const prevContainerHeight = prevContainerHeightRef.current; prevContainerHeightRef.current = containerHeight; // Skip initial resize observer callback if ( messageIds === prevMessageIds && isViewportNewest === prevIsViewportNewest && containerHeight !== prevContainerHeight && prevContainerHeight === undefined ) { return; } const container = containerRef.current!; listItemElementsRef.current = Array.from(container.querySelectorAll('.message-list-item')); const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1]; const firstUnreadElement = memoFirstUnreadIdRef.current ? container.querySelector(`#${getMessageHtmlId(memoFirstUnreadIdRef.current)}`) : undefined; const hasLastMessageChanged = ( messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1] ); const hasViewportShifted = ( messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1) ); const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted; // Add extra height when few messages to allow scroll animation if ( isViewportNewest && wasMessageAdded && (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2) && !container.parentElement!.classList.contains('force-messages-scroll') && forceMeasure(() => ( (container.firstElementChild as HTMLDivElement).clientHeight <= container.offsetHeight * 2 )) ) { addExtraClass(container.parentElement!, 'force-messages-scroll'); container.parentElement!.classList.add('force-messages-scroll'); setTimeout(() => { if (container.parentElement) { removeExtraClass(container.parentElement, 'force-messages-scroll'); } }, MESSAGE_ANIMATION_DURATION); } requestForcedReflow(() => { const { scrollTop, scrollHeight, offsetHeight } = container; const scrollOffset = scrollOffsetRef.current; let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight); if (wasMessageAdded) { // If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset` // right away (before animation) which creates inconsistency until the animation completes. To work around that, // we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate). const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0; bottomOffset -= lastItemHeight; } const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD; const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1]; // Animate incoming message, but if app is in background mode, scroll to the first unread if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) { // Break out of `forceLayout` requestMeasure(() => { const shouldScrollToBottom = !isBackgroundModeActive() || !firstUnreadElement; animateScroll({ container, element: shouldScrollToBottom ? lastItemElement : firstUnreadElement, position: shouldScrollToBottom ? 'end' : 'start', margin: BOTTOM_FOCUS_MARGIN, forceDuration: noMessageSendingAnimation ? 0 : undefined, }); }); } const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight; if (isResized && isAnimatingScroll()) { return undefined; } const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`); const unreadDivider = ( !anchor && memoUnreadDividerBeforeIdRef.current && container.querySelector(`.${UNREAD_DIVIDER_CLASS}`) ); let newScrollTop!: number; if (isAtBottom && isResized) { newScrollTop = scrollHeight - offsetHeight; } else if (anchor) { const newAnchorTop = anchor.getBoundingClientRect().top; newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0)); } else if (unreadDivider) { newScrollTop = Math.min( getOffsetToContainer(unreadDivider, container).top - UNREAD_DIVIDER_TOP, scrollHeight - scrollOffset, ); } else { newScrollTop = scrollHeight - scrollOffset; } return () => { resetScroll(container, Math.ceil(newScrollTop)); restartCurrentScrollAnimation(); scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight); if (!memoFocusingIdRef.current) { isScrollTopJustUpdatedRef.current = true; requestMeasure(() => { isScrollTopJustUpdatedRef.current = false; }); } if (process.env.APP_ENV === 'perf') { // eslint-disable-next-line no-console console.timeEnd('scrollTop'); } }; }); // This should match deps for `useSyncEffect` above }, [messageIds, isViewportNewest, getContainerHeight, prevContainerHeightRef, noMessageSendingAnimation]); useEffectWithPrevDeps(([prevIsSelectModeActive]) => { if (prevIsSelectModeActive !== undefined) { beginHeavyAnimation(SELECT_MODE_ANIMATION_DURATION + ANIMATION_END_DELAY); } }, [isSelectModeActive]); const noAvatars = Boolean(!withUsers || (isChannelChat && !isChannelWithAvatars)); const shouldRenderGreeting = isUserId(chatId) && !isChatWithSelf && !isBot && !isAnonymousForwards && type === 'thread' && ( ( !messageGroups && !lastMessage && messageIds // Used to avoid flickering when deleting a greeting that has just been sent && (!listItemElementsRef.current || listItemElementsRef.current.length === 0) ) || (messageIds?.length === 1 && messagesById?.[messageIds[0]]?.content.action?.type === 'contactSignUp') || (lastMessage?.content?.action?.type === 'contactSignUp') ); const isGroupChatJustCreated = isGroupChat && isCreator && messageIds?.length === 1 && messagesById?.[messageIds[0]]?.content.action?.type === 'chatCreate'; const isEmptyTopic = messageIds?.length === 1 && messagesById?.[messageIds[0]]?.content.action?.type === 'topicCreate'; const className = buildClassName( 'MessageList custom-scroll', noAvatars && 'no-avatars', !canPost && 'no-composer', type === 'pinned' && 'type-pinned', withBottomShift && 'with-bottom-shift', withDefaultBg && 'with-default-bg', isSelectModeActive && 'select-mode-active', isScrolled && 'scrolled', !isReady && 'is-animating', hasOpenChatButton && 'saved-dialog', isChatProtected && 'hide-on-print', ); const hasMessages = Boolean((messageIds && messageGroups) || lastMessage); useEffect(() => { if (hasMessages) return; onScrollDownToggle(false); }, [hasMessages, onScrollDownToggle]); const activeKey = isRestricted ? ( Content.Restricted ) : paidMessagesStars && !hasMessages && !hasCustomGreeting ? ( Content.StarsRequired ) : isContactRequirePremium && !hasMessages ? ( Content.PremiumRequired ) : (isBot || isNonContact) && !hasMessages ? ( Content.AccountInfo ) : shouldRenderGreeting ? ( Content.ContactGreeting ) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? ( Content.NoMessages ) : hasMessages ? ( Content.MessageList ) : ( Content.Loading ); function renderContent() { return activeKey === Content.Restricted ? (
{restrictionReasons?.[0]?.text || `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
) : activeKey === Content.StarsRequired ? ( ) : activeKey === Content.PremiumRequired ? ( ) : activeKey === Content.AccountInfo ? ( ) : activeKey === Content.ContactGreeting ? ( ) : activeKey === Content.NoMessages ? ( ) : activeKey === Content.MessageList ? ( ) : ( ); } return ( {renderContent()} ); }; export default memo(withGlobal( (global, { chatId, threadId, type }): Complete => { const currentUserId = global.currentUserId!; const chat = selectChat(global, chatId); const userFullInfo = selectUserFullInfo(global, chatId); if (!chat) { return { currentUserId } as Complete; } const messageIds = selectCurrentMessageIds(global, chatId, threadId, type); const messagesById = type === 'scheduled' ? selectChatScheduledMessages(global, chatId) : selectChatMessages(global, chatId); const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId); if ( threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum && !(messagesById && threadId && messagesById[Number(threadId)]) ) { return { currentUserId } as Complete; } const isRestricted = selectIsChatRestricted(global, chatId); const restrictionReasons = selectActiveRestrictionReasons(global, chat?.restrictionReasons); const lastMessage = selectChatLastMessage(global, chatId, isSavedDialog ? 'saved' : 'all'); const focusingId = selectFocusedMessageId(global, chatId); const withLastMessageWhenPreloading = ( threadId === MAIN_THREAD_ID && !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId ); const chatBot = selectBot(global, chatId); const isNonContact = Boolean(userFullInfo?.settings?.canAddContact); const nameChangeDate = userFullInfo?.settings?.nameChangeDate; const photoChangeDate = userFullInfo?.settings?.photoChangeDate; const topic = selectTopic(global, chatId, threadId); const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount; const isCurrentUserPremium = selectIsCurrentUserPremium(global); const areAdsEnabled = !isCurrentUserPremium || selectUserFullInfo(global, currentUserId)?.areAdsEnabled; const isAccountFrozen = selectIsCurrentUserFrozen(global); const hasCustomGreeting = Boolean(userFullInfo?.businessIntro); const isAppConfigLoaded = global.isAppConfigLoaded; const monoforumChannelId = selectMonoforumChannel(global, chatId)?.id; const canTranslate = selectCanTranslateChat(global, chatId) && !chatFullInfo?.isTranslationDisabled; const shouldAutoTranslate = chat?.hasAutoTranslation; const translationLanguage = selectTranslationLanguage(global); return { areAdsEnabled, isChatLoaded: true, isRestricted, restrictionReasons, isChannelChat: isChatChannel(chat), isChatMonoforum: isChatMonoforum(chat), isGroupChat: isChatGroup(chat), isChannelWithAvatars: chat.areProfilesShown, isCreator: chat.isCreator, isChatWithSelf: selectIsChatWithSelf(global, chatId), isSystemBotChat: isSystemBot(chatId), isAnonymousForwards: isAnonymousForwardsChat(chatId), isBot: Boolean(chatBot), isNonContact, nameChangeDate, photoChangeDate, isSynced: global.isSynced, messageIds, messagesById, firstUnreadId: selectFirstUnreadId(global, chatId, threadId), isViewportNewest: type !== 'thread' || selectIsViewportNewest(global, chatId, threadId), focusingId, isSelectModeActive: selectIsInSelectMode(global), hasLinkedChat: chatFullInfo ? Boolean(chatFullInfo.linkedChatId) : undefined, channelJoinInfo: chatFullInfo?.joinInfo, topic, noMessageSendingAnimation: !selectPerformanceSettingsValue(global, 'messageSendingAnimations'), isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID, isForum: chat.isForum, isEmptyThread, currentUserId, isChatProtected: selectIsChatProtected(global, chatId), lastMessage: withLastMessageWhenPreloading ? lastMessage : undefined, isAccountFrozen, hasCustomGreeting, isAppConfigLoaded, monoforumChannelId, canTranslate, translationLanguage, shouldAutoTranslate, }; }, )(MessageList)); function generateChannelJoinMessageId(lastMessageId: number) { return lastMessageId + 10e-7; // Smaller than smallest possible id with `getNextLocalMessageId` }