Comments: Fix thread bugs (#6698)

This commit is contained in:
zubiden 2026-02-22 23:43:34 +01:00 committed by Alexander Zinchuk
parent fec5e07b7d
commit fc498a222b
14 changed files with 81 additions and 52 deletions

View File

@ -2,7 +2,9 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiAttachment, ApiAttachment,
ApiBaseThreadInfo,
ApiChat, ApiChat,
ApiCommentsInfo,
ApiContact, ApiContact,
ApiDice, ApiDice,
ApiDraft, ApiDraft,
@ -15,6 +17,7 @@ import type {
ApiMessageEntity, ApiMessageEntity,
ApiMessageForwardInfo, ApiMessageForwardInfo,
ApiMessageReportResult, ApiMessageReportResult,
ApiMessageThreadInfo,
ApiNewMediaTodo, ApiNewMediaTodo,
ApiNewPoll, ApiNewPoll,
ApiPeer, ApiPeer,
@ -759,44 +762,51 @@ export function buildApiThreadInfoFromMessage(
return undefined; return undefined;
} }
return buildApiThreadInfo(mtpMessage.replies, mtpMessage.id, chatId); return buildApiThreadInfo(chatId, mtpMessage.id, mtpMessage.replies, mtpMessage.fwdFrom);
} }
export function buildApiThreadInfo( export function buildApiThreadInfo(
messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string, chatId: string,
messageId: number,
messageReplies: GramJs.TypeMessageReplies,
messageForwardInfo?: GramJs.MessageFwdHeader,
): ApiThreadInfo | undefined { ): ApiThreadInfo | undefined {
const { const {
channelId, replies, maxId, readMaxId, recentRepliers, comments, channelId, replies, maxId, recentRepliers, comments, readMaxId,
} = messageReplies; } = messageReplies;
const { fromId, channelPost } = messageForwardInfo || {};
const apiChannelId = channelId ? buildApiPeerId(channelId, 'channel') : undefined; const apiChannelId = channelId ? buildApiPeerId(channelId, 'channel') : undefined;
if (apiChannelId === DELETED_COMMENTS_CHANNEL_ID) { if (apiChannelId === DELETED_COMMENTS_CHANNEL_ID) {
return undefined; return undefined;
} }
const baseThreadInfo = { const baseThreadInfo: Partial<ApiBaseThreadInfo> = {
messagesCount: replies, messagesCount: replies,
...(maxId && { lastMessageId: maxId }), lastMessageId: maxId,
...(readMaxId && { lastReadMessageId: readMaxId }), recentReplierIds: recentRepliers?.map(getApiChatIdFromMtpPeer),
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
}; };
if (comments) { if (comments) {
return { return omitUndefined<ApiCommentsInfo>({
...baseThreadInfo, ...baseThreadInfo,
isCommentsInfo: true, isCommentsInfo: true,
chatId: apiChannelId!, chatId: apiChannelId!,
originChannelId: chatId, originChannelId: chatId,
originMessageId: messageId, originMessageId: messageId,
}; hasUnread: Boolean(readMaxId && maxId && readMaxId < maxId),
});
} }
return { return omitUndefined<ApiMessageThreadInfo>({
...baseThreadInfo, ...baseThreadInfo,
isCommentsInfo: false, isCommentsInfo: false,
chatId, chatId,
threadId: messageId, threadId: messageId,
}; fromChannelId: fromId && channelPost ? getApiChatIdFromMtpPeer(fromId) : undefined,
fromMessageId: channelPost,
});
} }
export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply { export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply {

View File

@ -74,6 +74,7 @@ import {
buildApiSearchPostsFlood, buildApiSearchPostsFlood,
buildApiSponsoredMessage, buildApiSponsoredMessage,
buildApiThreadInfo, buildApiThreadInfo,
buildApiThreadInfoFromMessage,
buildLocalForwardedMessage, buildLocalForwardedMessage,
buildLocalMessage, buildLocalMessage,
buildPreparedInlineMessage, buildPreparedInlineMessage,
@ -1383,7 +1384,7 @@ export async function fetchMessageViews({
id, id,
views, views,
forwards, 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 messages = topMessages.concat(replies.messages);
const threadId = result.messages[result.messages.length - 1]?.id; 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 { maxId } = result;
const threadReadState = buildThreadReadState(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 { return {
messages, messages,
topMessages, topMessages,
threadId, threadId,
threadReadState, threadReadState,
threadInfo,
lastMessageId: maxId, lastMessageId: maxId,
chatId: topMessages[0]?.chatId, chatId: topMessages[0]?.chatId,
firstMessageId: replies.messages[0]?.id, firstMessageId: replies.messages[0]?.id,

View File

@ -826,6 +826,7 @@ export interface ApiCommentsInfo extends ApiBaseThreadInfo {
threadId?: never; threadId?: never;
originChannelId: string; originChannelId: string;
originMessageId: number; originMessageId: number;
hasUnread?: boolean;
} }
export interface ApiMessageThreadInfo extends ApiBaseThreadInfo { export interface ApiMessageThreadInfo extends ApiBaseThreadInfo {

View File

@ -239,7 +239,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
return ( return (
<> <>
{renderBackButton()} {renderBackButton(currentTransitionKey === 0)}
<h3> <h3>
{messagesCount !== undefined ? ( {messagesCount !== undefined ? (
messageListType === 'thread' ? ( messageListType === 'thread' ? (

View File

@ -242,7 +242,7 @@
width: 0.5rem; width: 0.5rem;
height: 0.5rem; height: 0.5rem;
margin-inline-start: 0.75rem; margin-inline-start: 0.5rem;
border-radius: 50%; border-radius: 50%;
background: var(--accent-color); background: var(--accent-color);

View File

@ -2,7 +2,6 @@ import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global'; import { getActions, getGlobal } from '../../../global';
import type { ApiCommentsInfo } from '../../../api/types'; import type { ApiCommentsInfo } from '../../../api/types';
import type { ThreadReadState } from '../../../types';
import { selectIsCurrentUserFrozen, selectPeer } from '../../../global/selectors'; import { selectIsCurrentUserFrozen, selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -22,7 +21,6 @@ import './CommentButton.scss';
type OwnProps = { type OwnProps = {
threadInfo?: ApiCommentsInfo; threadInfo?: ApiCommentsInfo;
threadReadState?: ThreadReadState;
disabled?: boolean; disabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
isCustomShape?: boolean; isCustomShape?: boolean;
@ -33,7 +31,6 @@ const SHOW_LOADER_DELAY = 450;
const CommentButton = ({ const CommentButton = ({
isCustomShape, isCustomShape,
threadInfo, threadInfo,
threadReadState,
disabled, disabled,
isLoading, isLoading,
}: OwnProps) => { }: OwnProps) => {
@ -44,9 +41,8 @@ const CommentButton = ({
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
const { const {
originMessageId, chatId, messagesCount, lastMessageId, recentReplierIds, originChannelId, originMessageId, chatId, messagesCount, recentReplierIds, originChannelId, hasUnread,
} = threadInfo || {}; } = threadInfo || {};
const { lastReadInboxMessageId } = threadReadState || {};
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
const global = getGlobal(); 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)) const commentsText = messagesCount ? (oldLang('CommentsCount', '%COMMENTS_COUNT%', undefined, messagesCount))
.split('%') .split('%')
.map((s) => { .map((s) => {

View File

@ -39,7 +39,6 @@ import type {
TextSummary, TextSummary,
ThemeKey, ThemeKey,
ThreadId, ThreadId,
ThreadReadState,
} from '../../../types'; } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
@ -125,7 +124,7 @@ import {
selectMessageTimestampableDuration, selectMessageTimestampableDuration,
} from '../../../global/selectors/media'; } from '../../../global/selectors/media';
import { selectSharedSettings } from '../../../global/selectors/sharedState'; 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_TAURI } from '../../../util/browser/globalEnvironment';
import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment'; import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -295,7 +294,6 @@ type StateProps = {
shouldLoopStickers?: boolean; shouldLoopStickers?: boolean;
autoLoadFileMaxSizeMb: number; autoLoadFileMaxSizeMb: number;
repliesThreadInfo?: ApiThreadInfo; repliesThreadInfo?: ApiThreadInfo;
repliesReadState?: ThreadReadState;
reactionMessage?: ApiMessage; reactionMessage?: ApiMessage;
availableReactions?: ApiAvailableReaction[]; availableReactions?: ApiAvailableReaction[];
defaultReaction?: ApiReaction; defaultReaction?: ApiReaction;
@ -429,7 +427,6 @@ const Message = ({
shouldLoopStickers, shouldLoopStickers,
autoLoadFileMaxSizeMb, autoLoadFileMaxSizeMb,
repliesThreadInfo, repliesThreadInfo,
repliesReadState,
hasUnreadReaction, hasUnreadReaction,
memoFirstUnreadIdRef, memoFirstUnreadIdRef,
senderAdminMember, senderAdminMember,
@ -842,7 +839,6 @@ const Message = ({
const phoneCall = action?.type === 'phoneCall' ? action : undefined; const phoneCall = action?.type === 'phoneCall' ? action : undefined;
const commentsThreadInfo = repliesThreadInfo?.isCommentsInfo ? repliesThreadInfo : undefined; const commentsThreadInfo = repliesThreadInfo?.isCommentsInfo ? repliesThreadInfo : undefined;
const commentsReadState = repliesThreadInfo?.isCommentsInfo ? repliesReadState : undefined;
const isLocalWithCommentButton = hasLinkedChat && isChannel && isLocal; const isLocalWithCommentButton = hasLinkedChat && isChannel && isLocal;
const isMediaWithCommentButton = (commentsThreadInfo || isLocalWithCommentButton) const isMediaWithCommentButton = (commentsThreadInfo || isLocalWithCommentButton)
@ -1895,7 +1891,6 @@ const Message = ({
{withCommentButton && isCustomShape && ( {withCommentButton && isCustomShape && (
<CommentButton <CommentButton
threadInfo={commentsThreadInfo} threadInfo={commentsThreadInfo}
threadReadState={commentsReadState}
disabled={noComments || !commentsThreadInfo} disabled={noComments || !commentsThreadInfo}
isLoading={isLoadingComments} isLoading={isLoadingComments}
isCustomShape isCustomShape
@ -1927,7 +1922,6 @@ const Message = ({
{withCommentButton && !isCustomShape && ( {withCommentButton && !isCustomShape && (
<CommentButton <CommentButton
threadInfo={commentsThreadInfo} threadInfo={commentsThreadInfo}
threadReadState={commentsReadState}
disabled={noComments || !commentsThreadInfo} disabled={noComments || !commentsThreadInfo}
isLoading={isLoadingComments} isLoading={isLoadingComments}
/> />
@ -2095,8 +2089,7 @@ export default memo(withGlobal<OwnProps>(
const downloadableMedia = selectMessageDownloadableMedia(global, message); const downloadableMedia = selectMessageDownloadableMedia(global, message);
const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia); const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia);
const repliesThread = selectThread(global, chatId, album?.commentsMessage?.id || id); const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id);
const { threadInfo: repliesThreadInfo, readState: repliesReadState } = repliesThread || {};
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum; const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
const documentGroupFirstMessageId = isInDocumentGroup const documentGroupFirstMessageId = isInDocumentGroup
@ -2200,7 +2193,6 @@ export default memo(withGlobal<OwnProps>(
autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb, autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb,
shouldLoopStickers: selectShouldLoopStickers(global), shouldLoopStickers: selectShouldLoopStickers(global),
repliesThreadInfo, repliesThreadInfo,
repliesReadState,
availableReactions: global.reactions.availableReactions, availableReactions: global.reactions.availableReactions,
defaultReaction: isMessageLocal(message) || messageListType === 'scheduled' defaultReaction: isMessageLocal(message) || messageListType === 'scheduled'
? undefined : selectDefaultReaction(global, chatId), ? undefined : selectDefaultReaction(global, chatId),

View File

@ -422,22 +422,9 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
global = getGlobal(); global = getGlobal();
global = addMessages(global, result.messages); global = addMessages(global, result.messages);
global = updateThreadInfo(global, result.threadInfo);
global = updateThreadReadState(global, chatId, result.threadId, result.threadReadState); global = updateThreadReadState(global, chatId, result.threadId, result.threadReadState);
global = updateThreadInfoLastMessageId(global, chatId, result.threadId, result.lastMessageId); 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); global = replaceThreadLocalStateParam(global, chatId, threadId, 'firstMessageId', result.firstMessageId);
setGlobal(global); setGlobal(global);

View File

@ -247,6 +247,13 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
// Prevent unnecessary requests in threads // Prevent unnecessary requests in threads
if (offsetId === threadId && direction === LoadMoreDirection.Backwards) return; 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 isOutlying = Boolean(listedIds && offsetId && !listedIds.includes(offsetId));
const historyIds = (isOutlying const historyIds = (isOutlying
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId!) : listedIds)!; ? selectOutlyingListByMessageId(global, chatId, threadId, offsetId!) : listedIds)!;

View File

@ -23,7 +23,12 @@ import {
updateUsers, updateUsers,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { updateThreadInfo, updateThreadLocalState } from '../../reducers/threads'; import {
replaceThreadLocalStateParam,
updateThreadInfo,
updateThreadLocalState,
updateThreadReadState,
} from '../../reducers/threads';
import { import {
selectChat, selectChat,
selectChatMessage, selectChatMessage,
@ -179,6 +184,14 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
} }
global = addChatMessagesById(global, currentChatId, byId); 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); global = updateListedIds(global, currentChatId, activeThreadId, listedIds);
Object.entries(messagesThreads).forEach(([id, thread]) => { Object.entries(messagesThreads).forEach(([id, thread]) => {

View File

@ -41,7 +41,7 @@ import {
updateFocusedMessage, updateFocusedMessage,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { replaceTabThreadParam, replaceThreadLocalStateParam } from '../../reducers/threads'; import { replaceTabThreadParam, replaceThreadLocalStateParam, updateThreadReadState } from '../../reducers/threads';
import { import {
selectAllowedMessageActionsSlow, selectAllowedMessageActionsSlow,
selectCanForwardMessage, selectCanForwardMessage,
@ -495,6 +495,7 @@ addActionHandler('scrollMessageListToBottom', (global, actions, payload): Action
blurTimeout = window.setTimeout(() => { blurTimeout = window.setTimeout(() => {
global = getGlobal(); global = getGlobal();
global = updateFocusedMessage(global, undefined, tabId); global = updateFocusedMessage(global, undefined, tabId);
global = updateThreadReadState(global, chatId, threadId, { unreadCount: 0 });
setGlobal(global); setGlobal(global);
}, FOCUS_NO_HIGHLIGHT_DURATION); }, FOCUS_NO_HIGHLIGHT_DURATION);

View File

@ -140,7 +140,7 @@ export function updateLinkedThreadInfo<T extends GlobalState>(
return global; return global;
} }
const valuesToUpdate = pick(update, ['messagesCount', 'lastMessageId', 'recentReplierIds']); const valuesToUpdate = pick(update, ['messagesCount', 'lastMessageId']);
const newThreadInfo: ApiThreadInfo = { const newThreadInfo: ApiThreadInfo = {
...threadInfo, ...threadInfo,
...valuesToUpdate, ...valuesToUpdate,

View File

@ -783,6 +783,11 @@ export function selectRealLastReadId<T extends GlobalState>(global: T, chatId: s
// `lastReadInboxMessageId` is empty for new chats // `lastReadInboxMessageId` is empty for new chats
if (!readState.lastReadInboxMessageId) { 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; return undefined;
} }

View File

@ -11,7 +11,7 @@ import {
DEBUG, STRICTERDOM_ENABLED, DEBUG, STRICTERDOM_ENABLED,
} from './config'; } from './config';
import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom'; 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 { selectSharedSettings } from './global/selectors/sharedState';
import { betterView } from './util/betterView'; import { betterView } from './util/betterView';
import { IS_TAURI } from './util/browser/globalEnvironment'; import { IS_TAURI } from './util/browser/globalEnvironment';
@ -104,10 +104,21 @@ async function init() {
if (DEBUG) { if (DEBUG) {
document.addEventListener('dblclick', () => { document.addEventListener('dblclick', () => {
const currentGlobal = getGlobal();
const currentMessageList = selectCurrentMessageList(currentGlobal);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('TAB STATE', selectTabState(getGlobal())); console.warn('TAB STATE', selectTabState(currentGlobal));
// eslint-disable-next-line no-console // 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],
);
}
}); });
} }
} }