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 {
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<ApiBaseThreadInfo> = {
messagesCount: replies,
...(maxId && { lastMessageId: maxId }),
...(readMaxId && { lastReadMessageId: readMaxId }),
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
lastMessageId: maxId,
recentReplierIds: recentRepliers?.map(getApiChatIdFromMtpPeer),
};
if (comments) {
return {
return omitUndefined<ApiCommentsInfo>({
...baseThreadInfo,
isCommentsInfo: true,
chatId: apiChannelId!,
originChannelId: chatId,
originMessageId: messageId,
};
hasUnread: Boolean(readMaxId && maxId && readMaxId < maxId),
});
}
return {
return omitUndefined<ApiMessageThreadInfo>({
...baseThreadInfo,
isCommentsInfo: false,
chatId,
threadId: messageId,
};
fromChannelId: fromId && channelPost ? getApiChatIdFromMtpPeer(fromId) : undefined,
fromMessageId: channelPost,
});
}
export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply {

View File

@ -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,

View File

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

View File

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

View File

@ -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);

View File

@ -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) => {

View File

@ -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 && (
<CommentButton
threadInfo={commentsThreadInfo}
threadReadState={commentsReadState}
disabled={noComments || !commentsThreadInfo}
isLoading={isLoadingComments}
isCustomShape
@ -1927,7 +1922,6 @@ const Message = ({
{withCommentButton && !isCustomShape && (
<CommentButton
threadInfo={commentsThreadInfo}
threadReadState={commentsReadState}
disabled={noComments || !commentsThreadInfo}
isLoading={isLoadingComments}
/>
@ -2095,8 +2089,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb,
shouldLoopStickers: selectShouldLoopStickers(global),
repliesThreadInfo,
repliesReadState,
availableReactions: global.reactions.availableReactions,
defaultReaction: isMessageLocal(message) || messageListType === 'scheduled'
? undefined : selectDefaultReaction(global, chatId),

View File

@ -422,22 +422,9 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
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);

View File

@ -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)!;

View File

@ -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<T extends GlobalState>(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]) => {

View File

@ -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);

View File

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

View File

@ -783,6 +783,11 @@ export function selectRealLastReadId<T extends GlobalState>(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;
}

View File

@ -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],
);
}
});
}
}