diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 89fef200e..0dfe887a4 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -2,7 +2,9 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiAttachment, + ApiBaseThreadInfo, ApiChat, + ApiCommentsInfo, ApiContact, ApiDice, ApiDraft, @@ -15,6 +17,7 @@ import type { ApiMessageEntity, ApiMessageForwardInfo, ApiMessageReportResult, + ApiMessageThreadInfo, ApiNewMediaTodo, ApiNewPoll, ApiPeer, @@ -759,44 +762,51 @@ export function buildApiThreadInfoFromMessage( return undefined; } - return buildApiThreadInfo(mtpMessage.replies, mtpMessage.id, chatId); + return buildApiThreadInfo(chatId, mtpMessage.id, mtpMessage.replies, mtpMessage.fwdFrom); } export function buildApiThreadInfo( - messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string, + chatId: string, + messageId: number, + messageReplies: GramJs.TypeMessageReplies, + messageForwardInfo?: GramJs.MessageFwdHeader, ): ApiThreadInfo | undefined { const { - channelId, replies, maxId, readMaxId, recentRepliers, comments, + channelId, replies, maxId, recentRepliers, comments, readMaxId, } = messageReplies; + const { fromId, channelPost } = messageForwardInfo || {}; + const apiChannelId = channelId ? buildApiPeerId(channelId, 'channel') : undefined; if (apiChannelId === DELETED_COMMENTS_CHANNEL_ID) { return undefined; } - const baseThreadInfo = { + const baseThreadInfo: Partial = { messagesCount: replies, - ...(maxId && { lastMessageId: maxId }), - ...(readMaxId && { lastReadMessageId: readMaxId }), - ...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }), + lastMessageId: maxId, + recentReplierIds: recentRepliers?.map(getApiChatIdFromMtpPeer), }; if (comments) { - return { + return omitUndefined({ ...baseThreadInfo, isCommentsInfo: true, chatId: apiChannelId!, originChannelId: chatId, originMessageId: messageId, - }; + hasUnread: Boolean(readMaxId && maxId && readMaxId < maxId), + }); } - return { + return omitUndefined({ ...baseThreadInfo, isCommentsInfo: false, chatId, threadId: messageId, - }; + fromChannelId: fromId && channelPost ? getApiChatIdFromMtpPeer(fromId) : undefined, + fromMessageId: channelPost, + }); } export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 321fd7e3b..27e53efe6 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -74,6 +74,7 @@ import { buildApiSearchPostsFlood, buildApiSponsoredMessage, buildApiThreadInfo, + buildApiThreadInfoFromMessage, buildLocalForwardedMessage, buildLocalMessage, buildPreparedInlineMessage, @@ -1383,7 +1384,7 @@ export async function fetchMessageViews({ id, views, forwards, - threadInfo: replies ? buildApiThreadInfo(replies, id, chat.id) : undefined, + threadInfo: replies ? buildApiThreadInfo(chat.id, id, replies) : undefined, }; }); @@ -1456,16 +1457,23 @@ export async function fetchDiscussionMessage({ const messages = topMessages.concat(replies.messages); const threadId = result.messages[result.messages.length - 1]?.id; - if (!threadId) return undefined; + const chatId = topMessages[0]?.chatId; + if (!chatId || !threadId) return undefined; const { maxId } = result; const threadReadState = buildThreadReadState(result); + const topMessageWithReplies = result.messages.find((message): message is GramJs.Message => ( + message instanceof GramJs.Message && Boolean(message.replies) + ))!; + const threadInfo = buildApiThreadInfoFromMessage(topMessageWithReplies); + return { messages, topMessages, threadId, threadReadState, + threadInfo, lastMessageId: maxId, chatId: topMessages[0]?.chatId, firstMessageId: replies.messages[0]?.id, diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 9eda5107b..298b4103e 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -826,6 +826,7 @@ export interface ApiCommentsInfo extends ApiBaseThreadInfo { threadId?: never; originChannelId: string; originMessageId: number; + hasUnread?: boolean; } export interface ApiMessageThreadInfo extends ApiBaseThreadInfo { diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 89a923719..d46403c00 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -239,7 +239,7 @@ const MiddleHeader: FC = ({ return ( <> - {renderBackButton()} + {renderBackButton(currentTransitionKey === 0)}

{messagesCount !== undefined ? ( messageListType === 'thread' ? ( diff --git a/src/components/middle/message/CommentButton.scss b/src/components/middle/message/CommentButton.scss index 6a1f12c07..5cb970081 100644 --- a/src/components/middle/message/CommentButton.scss +++ b/src/components/middle/message/CommentButton.scss @@ -242,7 +242,7 @@ width: 0.5rem; height: 0.5rem; - margin-inline-start: 0.75rem; + margin-inline-start: 0.5rem; border-radius: 50%; background: var(--accent-color); diff --git a/src/components/middle/message/CommentButton.tsx b/src/components/middle/message/CommentButton.tsx index d9365d70f..306171e55 100644 --- a/src/components/middle/message/CommentButton.tsx +++ b/src/components/middle/message/CommentButton.tsx @@ -2,7 +2,6 @@ import { memo, useMemo } from '../../../lib/teact/teact'; import { getActions, getGlobal } from '../../../global'; import type { ApiCommentsInfo } from '../../../api/types'; -import type { ThreadReadState } from '../../../types'; import { selectIsCurrentUserFrozen, selectPeer } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; @@ -22,7 +21,6 @@ import './CommentButton.scss'; type OwnProps = { threadInfo?: ApiCommentsInfo; - threadReadState?: ThreadReadState; disabled?: boolean; isLoading?: boolean; isCustomShape?: boolean; @@ -33,7 +31,6 @@ const SHOW_LOADER_DELAY = 450; const CommentButton = ({ isCustomShape, threadInfo, - threadReadState, disabled, isLoading, }: OwnProps) => { @@ -44,9 +41,8 @@ const CommentButton = ({ const oldLang = useOldLang(); const lang = useLang(); const { - originMessageId, chatId, messagesCount, lastMessageId, recentReplierIds, originChannelId, + originMessageId, chatId, messagesCount, recentReplierIds, originChannelId, hasUnread, } = threadInfo || {}; - const { lastReadInboxMessageId } = threadReadState || {}; const handleClick = useLastCallback(() => { const global = getGlobal(); @@ -93,8 +89,6 @@ const CommentButton = ({ ); } - const hasUnread = Boolean(lastReadInboxMessageId && lastMessageId && lastReadInboxMessageId < lastMessageId); - const commentsText = messagesCount ? (oldLang('CommentsCount', '%COMMENTS_COUNT%', undefined, messagesCount)) .split('%') .map((s) => { diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 30c3dd31c..f874a2f04 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -39,7 +39,6 @@ import type { TextSummary, ThemeKey, ThreadId, - ThreadReadState, } from '../../../types'; import type { Signal } from '../../../util/signals'; import { MAIN_THREAD_ID } from '../../../api/types'; @@ -125,7 +124,7 @@ import { selectMessageTimestampableDuration, } from '../../../global/selectors/media'; import { selectSharedSettings } from '../../../global/selectors/sharedState'; -import { selectThread, selectThreadReadState } from '../../../global/selectors/threads'; +import { selectThreadInfo, selectThreadReadState } from '../../../global/selectors/threads'; import { IS_TAURI } from '../../../util/browser/globalEnvironment'; import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; @@ -295,7 +294,6 @@ type StateProps = { shouldLoopStickers?: boolean; autoLoadFileMaxSizeMb: number; repliesThreadInfo?: ApiThreadInfo; - repliesReadState?: ThreadReadState; reactionMessage?: ApiMessage; availableReactions?: ApiAvailableReaction[]; defaultReaction?: ApiReaction; @@ -429,7 +427,6 @@ const Message = ({ shouldLoopStickers, autoLoadFileMaxSizeMb, repliesThreadInfo, - repliesReadState, hasUnreadReaction, memoFirstUnreadIdRef, senderAdminMember, @@ -842,7 +839,6 @@ const Message = ({ const phoneCall = action?.type === 'phoneCall' ? action : undefined; const commentsThreadInfo = repliesThreadInfo?.isCommentsInfo ? repliesThreadInfo : undefined; - const commentsReadState = repliesThreadInfo?.isCommentsInfo ? repliesReadState : undefined; const isLocalWithCommentButton = hasLinkedChat && isChannel && isLocal; const isMediaWithCommentButton = (commentsThreadInfo || isLocalWithCommentButton) @@ -1895,7 +1891,6 @@ const Message = ({ {withCommentButton && isCustomShape && ( @@ -2095,8 +2089,7 @@ export default memo(withGlobal( const downloadableMedia = selectMessageDownloadableMedia(global, message); const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia); - const repliesThread = selectThread(global, chatId, album?.commentsMessage?.id || id); - const { threadInfo: repliesThreadInfo, readState: repliesReadState } = repliesThread || {}; + const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id); const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum; const documentGroupFirstMessageId = isInDocumentGroup @@ -2200,7 +2193,6 @@ export default memo(withGlobal( autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb, shouldLoopStickers: selectShouldLoopStickers(global), repliesThreadInfo, - repliesReadState, availableReactions: global.reactions.availableReactions, defaultReaction: isMessageLocal(message) || messageListType === 'scheduled' ? undefined : selectDefaultReaction(global, chatId), diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 15f2b417d..99a553ef9 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -422,22 +422,9 @@ addActionHandler('openThread', async (global, actions, payload): Promise = global = getGlobal(); global = addMessages(global, result.messages); + global = updateThreadInfo(global, result.threadInfo); global = updateThreadReadState(global, chatId, result.threadId, result.threadReadState); global = updateThreadInfoLastMessageId(global, chatId, result.threadId, result.lastMessageId); - - if (isComments) { - const lastMessageId = threadInfo?.lastMessageId !== undefined ? threadInfo.lastMessageId - : threadInfo?.messagesCount === 0 ? result.threadId : undefined; - - global = updateThreadInfo(global, { - isCommentsInfo: false, - threadId, - chatId, - fromChannelId: loadingChatId, - fromMessageId: loadingThreadId, - lastMessageId, - }); - } global = replaceThreadLocalStateParam(global, chatId, threadId, 'firstMessageId', result.firstMessageId); setGlobal(global); diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 5a0e2dd6e..24124c933 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -247,6 +247,13 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur // Prevent unnecessary requests in threads if (offsetId === threadId && direction === LoadMoreDirection.Backwards) return; + if (direction === LoadMoreDirection.Forwards && offsetId) { + const threadInfo = selectThreadInfo(global, chatId, threadId); + if (threadInfo?.lastMessageId && offsetId >= threadInfo.lastMessageId) { + return; + } + } + const isOutlying = Boolean(listedIds && offsetId && !listedIds.includes(offsetId)); const historyIds = (isOutlying ? selectOutlyingListByMessageId(global, chatId, threadId, offsetId!) : listedIds)!; diff --git a/src/global/actions/api/sync.ts b/src/global/actions/api/sync.ts index d40aa0245..90a18c852 100644 --- a/src/global/actions/api/sync.ts +++ b/src/global/actions/api/sync.ts @@ -23,7 +23,12 @@ import { updateUsers, } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; -import { updateThreadInfo, updateThreadLocalState } from '../../reducers/threads'; +import { + replaceThreadLocalStateParam, + updateThreadInfo, + updateThreadLocalState, + updateThreadReadState, +} from '../../reducers/threads'; import { selectChat, selectChatMessage, @@ -179,6 +184,14 @@ async function loadAndReplaceMessages(global: T, actions: } global = addChatMessagesById(global, currentChatId, byId); + if (resultDiscussion) { + global = updateThreadInfo(global, resultDiscussion.threadInfo); + global = updateThreadReadState(global, currentChatId, activeThreadId, resultDiscussion.threadReadState); + global = replaceThreadLocalStateParam( + global, currentChatId, activeThreadId, 'firstMessageId', resultDiscussion.firstMessageId, + ); + global = addChatMessagesById(global, currentChatId, buildCollectionByKey(resultDiscussion.topMessages, 'id')); + } global = updateListedIds(global, currentChatId, activeThreadId, listedIds); Object.entries(messagesThreads).forEach(([id, thread]) => { diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index 3edb08616..9b4a30185 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -41,7 +41,7 @@ import { updateFocusedMessage, } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; -import { replaceTabThreadParam, replaceThreadLocalStateParam } from '../../reducers/threads'; +import { replaceTabThreadParam, replaceThreadLocalStateParam, updateThreadReadState } from '../../reducers/threads'; import { selectAllowedMessageActionsSlow, selectCanForwardMessage, @@ -495,6 +495,7 @@ addActionHandler('scrollMessageListToBottom', (global, actions, payload): Action blurTimeout = window.setTimeout(() => { global = getGlobal(); global = updateFocusedMessage(global, undefined, tabId); + global = updateThreadReadState(global, chatId, threadId, { unreadCount: 0 }); setGlobal(global); }, FOCUS_NO_HIGHLIGHT_DURATION); diff --git a/src/global/reducers/threads.ts b/src/global/reducers/threads.ts index cf239ea0e..68968a1d9 100644 --- a/src/global/reducers/threads.ts +++ b/src/global/reducers/threads.ts @@ -140,7 +140,7 @@ export function updateLinkedThreadInfo( return global; } - const valuesToUpdate = pick(update, ['messagesCount', 'lastMessageId', 'recentReplierIds']); + const valuesToUpdate = pick(update, ['messagesCount', 'lastMessageId']); const newThreadInfo: ApiThreadInfo = { ...threadInfo, ...valuesToUpdate, diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 3fb48bef1..dfe9897b2 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -783,6 +783,11 @@ export function selectRealLastReadId(global: T, chatId: s // `lastReadInboxMessageId` is empty for new chats if (!readState.lastReadInboxMessageId) { + // For new comments, mark thread start as the last read + if (threadId !== MAIN_THREAD_ID && readState.unreadCount && typeof threadId === 'number') { + return threadId; + } + return undefined; } diff --git a/src/index.tsx b/src/index.tsx index 9c94a6c63..31e1b2015 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,7 +11,7 @@ import { DEBUG, STRICTERDOM_ENABLED, } from './config'; import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom'; -import { selectTabState } from './global/selectors'; +import { selectChat, selectChatFullInfo, selectCurrentMessageList, selectTabState } from './global/selectors'; import { selectSharedSettings } from './global/selectors/sharedState'; import { betterView } from './util/betterView'; import { IS_TAURI } from './util/browser/globalEnvironment'; @@ -104,10 +104,21 @@ async function init() { if (DEBUG) { document.addEventListener('dblclick', () => { + const currentGlobal = getGlobal(); + const currentMessageList = selectCurrentMessageList(currentGlobal); // eslint-disable-next-line no-console - console.warn('TAB STATE', selectTabState(getGlobal())); + console.warn('TAB STATE', selectTabState(currentGlobal)); // eslint-disable-next-line no-console - console.warn('GLOBAL STATE', getGlobal()); + console.warn('GLOBAL STATE', currentGlobal); + if (currentMessageList) { + // eslint-disable-next-line no-console + console.warn( + 'CURRENT MESSAGE LIST', + selectChat(currentGlobal, currentMessageList.chatId), + selectChatFullInfo(currentGlobal, currentMessageList.chatId), + currentGlobal.messages.byChatId[currentMessageList.chatId], + ); + } }); } }