diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 89a6ebfd2..59ebd3d97 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -3,7 +3,7 @@ import { getIsHeavyAnimating, memo } from '../../lib/teact/teact'; import { getActions, getGlobal } from '../../global'; import type { ApiMessage } from '../../api/types'; -import type { IAlbum, MessageListType, ThreadId } from '../../types'; +import type { IAlbum, IDocumentGroup, MessageListType, ThreadId } from '../../types'; import type { Signal } from '../../util/signals'; import type { MessageDateGroup } from './helpers/groupMessages'; import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; @@ -26,7 +26,7 @@ import { formatHumanDate, formatScheduledDateTime } from '../../util/dates/oldDa import { convertTonFromNanos } from '../../util/formatCurrency'; import { compact } from '../../util/iteratees'; import { formatStarsAsText, formatTonAsText } from '../../util/localization/format'; -import { isAlbum } from './helpers/groupMessages'; +import { isAlbum, isDocumentGroup } from './helpers/groupMessages'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import { renderPeerLink } from './message/helpers/messageActions'; @@ -130,6 +130,7 @@ const MessageListContent = ({ const areDatesClickable = !isSavedDialog && !isSchedule; const shouldRenderSponsoredMessage = canShowAds && isViewportNewest; + const shouldHideComments = hasLinkedChat === false || !isChannelChat || Boolean(isChatMonoforum); const { observeIntersectionForReading, @@ -270,7 +271,9 @@ const MessageListContent = ({ }; const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { - return acc + messageGroup.senderGroups.flat().length; + return acc + messageGroup.senderGroups.flat().reduce((innerAcc, messageOrAlbum) => { + return innerAcc + (isDocumentGroup(messageOrAlbum) ? messageOrAlbum.messages.length : 1); + }, 0); }, 0); let appearanceIndex = 0; @@ -290,6 +293,7 @@ const MessageListContent = ({ if ( senderGroup.length === 1 && !isAlbum(senderGroup[0]) + && !isDocumentGroup(senderGroup[0]) && isActionMessage(senderGroup[0]) && senderGroup[0].content.action?.type !== 'phoneCall' ) { @@ -318,31 +322,115 @@ const MessageListContent = ({ ]); } - let currentDocumentGroupId: string | undefined; + let currentDocumentGroupHasThreadTop = false; const senderGroupElements = senderGroup.map(( messageOrAlbum, messageIndex, ) => { + function renderMessageElement( + message: ApiMessage, + position: { + isFirstInGroup: boolean; + isLastInGroup: boolean; + isFirstInDocumentGroup: boolean; + isLastInDocumentGroup: boolean; + isLastInList: boolean; + }, + isThreadTopMessage: boolean, + album?: IAlbum, + documentGroup?: IDocumentGroup, + ) { + const isOwn = isOwnMessage(message); + const originalId = getMessageOriginalId(message); + const key = isServiceNotificationMessage(message) + ? `${message.date}_${originalId}` : originalId; + + return compact([ + message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider, + message.paidMessageStars && !withUsers && renderPaidMessageAction(message, album), + message.suggestedPostInfo && renderSuggestedPostInfoAction(message), + , + ]); + } + + if (isDocumentGroup(messageOrAlbum)) { + const documentGroup = messageOrAlbum; + return documentGroup.messages.map((docMessage, docIndex) => { + const isFirstInDocGroup = docIndex === 0; + const isLastInDocGroup = docIndex === documentGroup.messages.length - 1; + + if (docMessage.previousLocalId && anchorIdRef.current === getMessageHtmlId(docMessage.previousLocalId)) { + anchorIdRef.current = getMessageHtmlId(docMessage.id); + } + + if (isFirstInDocGroup && docMessage.id === threadId) { + currentDocumentGroupHasThreadTop = true; + } + + const isThreadTopMessage = docMessage.id === threadId || currentDocumentGroupHasThreadTop; + + if (isLastInDocGroup) { + currentDocumentGroupHasThreadTop = false; + } + + const position = { + isFirstInGroup: messageIndex === 0 && isFirstInDocGroup, + isLastInGroup: messageIndex === senderGroup.length - 1 && isLastInDocGroup, + isFirstInDocumentGroup: isFirstInDocGroup, + isLastInDocumentGroup: isLastInDocGroup, + isLastInList: ( + messageIndex === senderGroup.length - 1 + && isLastInDocGroup + && senderGroupIndex === senderGroupsArray.length - 1 + && dateGroupIndex === dateGroupsArray.length - 1 + ), + }; + + return renderMessageElement(docMessage, position, isThreadTopMessage, undefined, documentGroup); + }).flat(); + } + 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 isThreadTopMessage = message.id === threadId; const position = { isFirstInGroup: messageIndex === 0, isLastInGroup: messageIndex === senderGroup.length - 1, - isFirstInDocumentGroup: Boolean(documentGroupId && documentGroupId !== currentDocumentGroupId), - isLastInDocumentGroup: Boolean(documentGroupId && documentGroupId !== nextDocumentGroupId), + isFirstInDocumentGroup: false, + isLastInDocumentGroup: false, isLastInList: ( messageIndex === senderGroup.length - 1 && senderGroupIndex === senderGroupsArray.length - 1 @@ -350,60 +438,33 @@ const MessageListContent = ({ ), }; - 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 || Boolean(isChatMonoforum); - - return compact([ - message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider, - message.paidMessageStars && !withUsers && renderPaidMessageAction(message, album), - message.suggestedPostInfo && renderSuggestedPostInfoAction(message), - , - ]); + return renderMessageElement(message, position, isThreadTopMessage, album); }).flat(); if (!withUsers) return senderGroupElements; - const lastMessageOrAlbum = senderGroup[senderGroup.length - 1]; - const lastMessage = isAlbum(lastMessageOrAlbum) ? lastMessageOrAlbum.mainMessage : lastMessageOrAlbum; + const lastItem = senderGroup[senderGroup.length - 1]; + const lastMessage = isAlbum(lastItem) + ? lastItem.mainMessage + : isDocumentGroup(lastItem) + ? lastItem.messages[lastItem.messages.length - 1] + : lastItem; const lastMessageId = getMessageOriginalId(lastMessage); const lastAppearanceOrder = messageCountToAnimate - appearanceIndex; - const isThreadTopMessage = lastMessage.id === threadId; const isOwn = isOwnMessage(lastMessage); - const firstMessageOrAlbum = senderGroup[0]; - const firstMessage = isAlbum(firstMessageOrAlbum) ? firstMessageOrAlbum.mainMessage : firstMessageOrAlbum; + const firstItem = senderGroup[0]; + const firstMessage = isAlbum(firstItem) + ? firstItem.mainMessage + : isDocumentGroup(firstItem) + ? firstItem.messages[0] + : firstItem; const firstMessageId = getMessageOriginalId(firstMessage); + const isThreadTopMessage = lastMessage.id === threadId + || (firstMessage.id === threadId && Boolean(firstMessage.groupedId)); + const key = `${firstMessageId}-${lastMessageId}`; const id = (firstMessageId === lastMessageId) ? `message-group-${firstMessageId}` : `message-group-${firstMessageId}-${lastMessageId}`; diff --git a/src/components/middle/helpers/groupMessages.ts b/src/components/middle/helpers/groupMessages.ts index 701127a8c..976eb71b1 100644 --- a/src/components/middle/helpers/groupMessages.ts +++ b/src/components/middle/helpers/groupMessages.ts @@ -1,10 +1,10 @@ import type { ApiMessage } from '../../../api/types'; -import type { IAlbum } from '../../../types'; +import type { IAlbum, IDocumentGroup } from '../../../types'; import { isActionMessage } from '../../../global/helpers'; import { getDayStartAt } from '../../../util/dates/oldDateFormat'; -type SenderGroup = (ApiMessage | IAlbum)[]; +type SenderGroup = (ApiMessage | IAlbum | IDocumentGroup)[]; const GROUP_INTERVAL_SECONDS = 600; // 10 minutes @@ -14,10 +14,16 @@ export type MessageDateGroup = { senderGroups: SenderGroup[]; }; -export function isAlbum(messageOrAlbum: ApiMessage | IAlbum): messageOrAlbum is IAlbum { +export function isAlbum(messageOrAlbum: ApiMessage | IAlbum | IDocumentGroup): messageOrAlbum is IAlbum { return 'albumId' in messageOrAlbum; } +export function isDocumentGroup( + messageOrAlbum: ApiMessage | IAlbum | IDocumentGroup, +): messageOrAlbum is IDocumentGroup { + return 'documentGroupId' in messageOrAlbum; +} + export function groupMessages( messages: ApiMessage[], firstUnreadId?: number, topMessageId?: number, isChatWithSelf?: boolean, withUsers?: boolean, ) { @@ -27,6 +33,7 @@ export function groupMessages( senderGroups: [[]], }; let currentAlbum: IAlbum | undefined; + let currentDocumentGroup: IDocumentGroup | undefined; const dateGroups: MessageDateGroup[] = [initDateGroup]; @@ -40,6 +47,8 @@ export function groupMessages( messages: [message], mainMessage: message, hasMultipleCaptions: false, + commentsMessage: message.hasComments ? message : undefined, + captionMessage: message.content.text ? message : undefined, } satisfies IAlbum; } else { currentAlbum.messages.push(message); @@ -63,6 +72,20 @@ export function groupMessages( hasMultipleCaptions: false, isPaidMedia: true, } satisfies IAlbum); + } else if (message.groupedId) { + if (!currentDocumentGroup) { + currentDocumentGroup = { + documentGroupId: message.groupedId, + messages: [message], + firstMessageId: message.id, + commentsMessage: message.hasComments ? message : undefined, + } satisfies IDocumentGroup; + } else { + currentDocumentGroup.messages.push(message); + if (message.hasComments) { + currentDocumentGroup.commentsMessage = message; + } + } } else { currentSenderGroup.push(message); } @@ -77,8 +100,16 @@ export function groupMessages( currentAlbum = undefined; } + if ( + currentDocumentGroup + && (!nextMessage || !nextMessage.groupedId || nextMessage.groupedId !== currentDocumentGroup.documentGroupId) + ) { + currentSenderGroup.push(currentDocumentGroup); + currentDocumentGroup = undefined; + } + const lastMessageInSenderGroup = currentSenderGroup[currentSenderGroup.length - 1]; - if (nextMessage && !currentAlbum) { + if (nextMessage && !currentAlbum && !currentDocumentGroup) { const nextMessageDayStartsAt = getDayStartAt(nextMessage.date * 1000); if (currentDateGroup.datetime !== nextMessageDayStartsAt) { const newDateGroup: MessageDateGroup = { @@ -101,10 +132,12 @@ export function groupMessages( || (nextMessage.date - message.date) > GROUP_INTERVAL_SECONDS || (topMessageId && (message.id === topMessageId - || (lastMessageInSenderGroup - && 'mainMessage' in lastMessageInSenderGroup - && lastMessageInSenderGroup.mainMessage?.id === topMessageId)) - && nextMessage.id !== topMessageId) + || (lastMessageInSenderGroup && ( + (isAlbum(lastMessageInSenderGroup) && lastMessageInSenderGroup.mainMessage.id === topMessageId) + || (isDocumentGroup(lastMessageInSenderGroup) && lastMessageInSenderGroup.firstMessageId === topMessageId) + ))) + && nextMessage.id !== topMessageId + && !(message.groupedId && message.groupedId === nextMessage.groupedId)) || (isChatWithSelf && message.forwardInfo?.fromId !== nextMessage.forwardInfo?.fromId) ) { currentDateGroup.senderGroups.push([]); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 108b77525..533e5d849 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -34,6 +34,7 @@ import type { ChatTranslatedMessages, FocusDirection, IAlbum, + IDocumentGroup, MessageListType, ScrollTargetPosition, TextSummary, @@ -97,7 +98,6 @@ import { selectIsMessageFocused, selectIsMessageProtected, selectIsMessageSelected, - selectMessageIdsByGroupId, selectMessageSummary, selectOutgoingStatus, selectPeer, @@ -225,6 +225,7 @@ type MessagePositionProperties = { type OwnProps = { message: ApiMessage; album?: IAlbum; + documentGroup?: IDocumentGroup; noAvatars?: boolean; withAvatar?: boolean; withSenderName?: boolean; @@ -234,6 +235,7 @@ type OwnProps = { noReplies: boolean; appearanceOrder: number; isJustAdded: boolean; + isThreadTop?: boolean; memoFirstUnreadIdRef?: { current: number | undefined }; getIsMessageListReady?: Signal; observeIntersectionForBottom?: ObserveFn; @@ -249,7 +251,6 @@ type StateProps = { canShowSender: boolean; originSender?: ApiPeer; botSender?: ApiUser; - isThreadTop?: boolean; shouldHideReply?: boolean; replyMessage?: ApiMessage; replyMessageSender?: ApiPeer; @@ -2004,7 +2005,8 @@ export default memo(withGlobal( loadingThread, } = selectTabState(global); const { - message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup, + message, album, documentGroup, withSenderName, withAvatar, threadId, messageListType, + isLastInDocumentGroup, isFirstInGroup, } = ownProps; const { id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, viaBusinessBotId, effectId, @@ -2040,8 +2042,6 @@ export default memo(withGlobal( ? (adminMembersById?.[sender?.id] || members?.find((member) => member.userId === sender?.id)) : undefined; - const isThreadTop = message.id === threadId; - const { replyToMsgId, replyToPeerId, replyFrom } = getMessageReplyInfo(message) || {}; const { peerId: storyReplyPeerId, storyId: storyReplyId } = getStoryReplyInfo(message) || {}; @@ -2094,14 +2094,15 @@ export default memo(withGlobal( const downloadableMedia = selectMessageDownloadableMedia(global, message); const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia); - const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id); - const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum; - const documentGroupFirstMessageId = isInDocumentGroup - ? selectMessageIdsByGroupId(global, chatId, message.groupedId!)![0] - : undefined; + + const repliesThreadInfo = selectThreadInfo( + global, chatId, album?.commentsMessage?.id || documentGroup?.commentsMessage?.id || id, + ); const reactionMessage = isInDocumentGroup ? ( - isLastInDocumentGroup ? selectChatMessage(global, chatId, documentGroupFirstMessageId!) : undefined + isLastInDocumentGroup && documentGroup?.firstMessageId + ? selectChatMessage(global, chatId, documentGroup.firstMessageId) + : undefined ) : message; const readState = selectThreadReadState(global, chatId, threadId); @@ -2158,7 +2159,6 @@ export default memo(withGlobal( originSender, botSender, shouldHideReply: shouldHideReply || isReplyToTopicStart, - isThreadTop, replyMessage, replyMessageSender, replyMessageForwardSender, diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 28201ed28..b61dd0d8f 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -141,6 +141,7 @@ import { selectIsMonoforumAdmin, selectLanguageCode, selectListedIds, + selectMessageIdsByGroupId, selectMessageReplyInfo, selectOutlyingListByMessageId, selectPeer, @@ -1809,7 +1810,16 @@ async function loadViewportMessages( if (threadId !== MAIN_THREAD_ID && !getIsSavedDialog(chatId, threadId, global.currentUserId)) { const threadFirstMessageId = selectFirstMessageId(global, chatId, threadId); if ((!ids[0] || threadFirstMessageId === ids[0]) && threadFirstMessageId !== threadId) { - ids.unshift(Number(threadId)); + const threadTopMessage = selectChatMessage(global, chatId, Number(threadId)); + const groupedIds = threadTopMessage?.groupedId + ? selectMessageIdsByGroupId(global, chatId, threadTopMessage.groupedId) + : undefined; + + if (groupedIds && groupedIds.length > 1) { + ids.unshift(...groupedIds); + } else { + ids.unshift(Number(threadId)); + } } } diff --git a/src/types/index.ts b/src/types/index.ts index ba988aaea..c054b47ed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -96,6 +96,13 @@ export interface IAlbum { commentsMessage?: ApiMessage; } +export interface IDocumentGroup { + documentGroupId: string; + messages: ApiMessage[]; + firstMessageId: number; + commentsMessage?: ApiMessage; +} + export type ThreadId = string | number; export type ThemeKey = 'light' | 'dark';