From 903f20010f20fea8f7598398a7393b75d0d5ecf8 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:29:06 +0200 Subject: [PATCH] Middle Header: Fix message counters in bot forums and saved (#6848) --- src/api/gramjs/methods/messages.ts | 2 +- src/assets/localization/fallback.strings | 6 +- src/components/common/GroupChatInfo.tsx | 8 +- src/components/common/PrivateChatInfo.tsx | 10 ++- src/components/left/main/LeftMainHeader.tsx | 2 +- src/components/middle/MessageList.tsx | 2 +- src/components/middle/MiddleHeader.tsx | 78 ++++++++++++------- .../quickPreview/QuickPreviewModalHeader.tsx | 23 ++++-- src/global/actions/api/chats.ts | 13 +++- src/hooks/useConnectionStatus.ts | 4 +- src/types/language.d.ts | 5 ++ 11 files changed, 110 insertions(+), 43 deletions(-) diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index cbef58e8b..b35b0df7f 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -205,7 +205,7 @@ export async function fetchMessages({ const messages = result.messages.map(buildApiMessage).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); - const count = !(result instanceof GramJs.messages.Messages) ? result.count : undefined; + const count = 'count' in result ? result.count : messages.length; const topics = result.topics.map(buildApiTopicWithState).filter(Boolean); return { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 230f2e1ef..7f709f3fb 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -287,7 +287,7 @@ "SendWithoutSound" = "Send Without Sound"; "SetReminder" = "Set a Reminder"; "ScheduleMessage" = "Schedule Message"; -"Updating" = "Updating..."; +"Updating" = "Updating"; "OnlineCount_one" = "{count} online"; "OnlineCount_other" = "{count} online"; "Subscribers_one" = "{count} subscriber"; @@ -777,6 +777,10 @@ "AccUnpinMessage" = "Unpin Message"; "Comments_one" = "{count} Comment"; "Comments_other" = "{count} Comments"; +"CommentsTitle" = "Comments"; +"Replies_one" = "{count} Reply"; +"Replies_other" = "{count} Replies"; +"RepliesTitle" = "Replies"; "LeaveAComment" = "Leave a comment"; "PollsStopWarning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone."; "PollsStopSure" = "Stop"; diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index e4b09e2d6..8b90d4bb8 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -287,10 +287,14 @@ const GroupChatInfo = ({ }; export default memo(withGlobal( - (global, { chatId, threadId }): Complete => { + (global, { chatId, threadId, isSavedDialog }): Complete => { const chat = selectChat(global, chatId); const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined; - const areMessagesLoaded = Boolean(selectChatMessages(global, chatId)); + const areMessagesLoaded = Boolean( + isSavedDialog + ? selectChatMessages(global, global.currentUserId!) + : selectChatMessages(global, chatId), + ); const topic = threadId ? selectTopic(global, chatId, threadId) : undefined; const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!); const self = selectUser(global, global.currentUserId!); diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 0902d1891..28a500602 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -335,13 +335,19 @@ const PrivateChatInfo = ({ }; export default memo(withGlobal( - (global, { userId, threadId, forceShowSelf }): Complete => { + (global, { + userId, threadId, forceShowSelf, isSavedDialog, + }): Complete => { const { isSynced } = global; const user = userId ? selectUser(global, userId) : undefined; const userStatus = userId ? selectUserStatus(global, userId) : undefined; const isSavedMessages = !forceShowSelf && user && user.isSelf; const self = isSavedMessages ? user : selectUser(global, global.currentUserId!); - const areMessagesLoaded = Boolean(userId ? selectChatMessages(global, userId) : undefined); + const areMessagesLoaded = Boolean( + isSavedDialog + ? selectChatMessages(global, global.currentUserId!) + : selectChatMessages(global, userId!), + ); const topic = threadId ? selectTopic(global, userId, threadId) : undefined; const messagesCount = topic && userId ? selectThreadMessagesCount(global, userId, threadId!) : undefined; diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index d48e88cc2..d3a90ac17 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -124,7 +124,7 @@ const LeftMainHeader = ({ }, [searchDate]); const { connectionStatus, connectionStatusText, connectionStatusPosition } = useConnectionStatus( - oldLang, + lang, connectionState, isSyncing || isFetchingDifference, isMessageListOpen, diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 36718b3bb..2900c724e 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -953,7 +953,7 @@ export default memo(withGlobal( const topic = selectTopic(global, chatId, threadId); const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; - const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount; + const isEmptyThread = selectThreadInfo(global, chatId, threadId)?.messagesCount === 0; const isCurrentUserPremium = selectIsCurrentUserPremium(global); const areAdsEnabled = !isCurrentUserPremium || selectUserFullInfo(global, currentUserId)?.areAdsEnabled; diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index b217691c5..f2f52eb3a 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -1,5 +1,3 @@ -import type { FC } from '../../lib/teact/teact'; -import type React from '../../lib/teact/teact'; import { memo, useRef, } from '../../lib/teact/teact'; @@ -32,7 +30,9 @@ import { selectScheduledIds, selectTabState, } from '../../global/selectors'; -import { selectThreadInfo, selectThreadLocalStateParam } from '../../global/selectors/threads'; +import { + selectThreadLocalStateParam, selectThreadMessagesCount, +} from '../../global/selectors/threads'; import { IS_TAURI } from '../../util/browser/globalEnvironment'; import { IS_MAC_OS } from '../../util/browser/windowEnvironment'; import buildClassName from '../../util/buildClassName'; @@ -40,9 +40,9 @@ import { isUserId } from '../../util/entities/ids'; import useAppLayout from '../../hooks/useAppLayout'; import useConnectionStatus from '../../hooks/useConnectionStatus'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useLongPress from '../../hooks/useLongPress'; -import useOldLang from '../../hooks/useOldLang'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import useWindowSize from '../../hooks/window/useWindowSize'; @@ -91,7 +91,7 @@ type StateProps = { emojiStatusSlug?: string; }; -const MiddleHeader: FC = ({ +const MiddleHeader = ({ chatId, threadId, messageListType, @@ -115,7 +115,7 @@ const MiddleHeader: FC = ({ emojiStatusSlug, isSavedDialog, onFocusPinnedMessage, -}) => { +}: OwnProps & StateProps) => { const { openThreadWithInfo, openChat, @@ -128,7 +128,8 @@ const MiddleHeader: FC = ({ openUniqueGiftBySlug, } = getActions(); - const lang = useOldLang(); + const lang = useLang(); + const isBackButtonActiveRef = useRef(true); const { isDesktop, isTablet } = useAppLayout(); @@ -228,7 +229,41 @@ const MiddleHeader: FC = ({ const isAudioPlayerRendering = isDesktop && isAudioPlayerActive; const isPinnedMessagesFullWidth = isAudioPlayerActive || !isDesktop; - const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true); + const { connectionStatusText } = useConnectionStatus( + lang, connectionState, isSyncing || isFetchingDifference, true, + ); + + function renderInfoTitle() { + if (messagesCount === undefined) { + return lang('Loading'); + } + + if (messageListType === 'thread') { + if (!messagesCount) { + return lang(isComments ? 'CommentsTitle' : 'RepliesTitle'); + } + + return lang( + isComments ? 'Comments' : 'Replies', + { count: messagesCount }, + { pluralValue: messagesCount }, + ); + } + + if (messageListType === 'pinned') { + return lang('PinnedMessagesCount', { count: messagesCount }, { pluralValue: messagesCount }); + } + + if (messageListType === 'scheduled') { + if (isChatWithSelf) { + return lang('Reminders'); + } + + return lang('Messages', { count: messagesCount }, { pluralValue: messagesCount }); + } + + return undefined; + } function renderInfo() { if (messageListType === 'thread') { @@ -240,25 +275,17 @@ const MiddleHeader: FC = ({ return ( <> {renderBackButton(currentTransitionKey === 0)} -

- {messagesCount !== undefined ? ( - messageListType === 'thread' ? ( - (messagesCount - ? lang(isComments ? 'Comments' : 'Replies', messagesCount, 'i') - : lang(isComments ? 'CommentsTitle' : 'RepliesTitle'))) - : messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i')) - : messageListType === 'scheduled' ? ( - isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i') - ) : undefined - ) : lang('Loading')} -

+

{renderInfoTitle()}

); } function renderChatInfo() { - // TODO Implement count - const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined; + const savedMessagesStatus = isSavedDialog + ? (messagesCount !== undefined + ? lang('Messages', { count: messagesCount }, { pluralValue: messagesCount }) + : lang('SavedMessages')) + : undefined; const realChatId = isSavedDialog ? String(threadId) : chatId; @@ -387,6 +414,8 @@ export default memo(withGlobal( ? selectChatMessage(global, audioChatId, audioMessageId) : undefined; + const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); + let messagesCount: number | undefined; if (messageListType === 'pinned') { const pinnedIds = selectPinnedIds(global, chatId, threadId); @@ -395,8 +424,7 @@ export default memo(withGlobal( const scheduledIds = selectScheduledIds(global, chatId, threadId); messagesCount = scheduledIds?.length; } else if (messageListType === 'thread' && threadId !== MAIN_THREAD_ID) { - const threadInfo = selectThreadInfo(global, chatId, threadId); - messagesCount = threadInfo?.messagesCount || 0; + messagesCount = selectThreadMessagesCount(global, chatId, threadId); } const typingStatus = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatus'); @@ -405,8 +433,6 @@ export default memo(withGlobal( const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId); const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; - const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); - return { typingStatus, isLeftColumnShown, diff --git a/src/components/modals/quickPreview/QuickPreviewModalHeader.tsx b/src/components/modals/quickPreview/QuickPreviewModalHeader.tsx index 2aef8a441..a520a4182 100644 --- a/src/components/modals/quickPreview/QuickPreviewModalHeader.tsx +++ b/src/components/modals/quickPreview/QuickPreviewModalHeader.tsx @@ -8,13 +8,16 @@ import { MAIN_THREAD_ID } from '../../../api/types'; import { getIsSavedDialog } from '../../../global/helpers'; import { selectChat } from '../../../global/selectors'; -import { selectThreadLocalStateParam, selectThreadReadState } from '../../../global/selectors/threads'; +import { + selectThreadLocalStateParam, + selectThreadMessagesCount, + selectThreadReadState, +} from '../../../global/selectors/threads'; import { isUserId } from '../../../util/entities/ids'; import useConnectionStatus from '../../../hooks/useConnectionStatus'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import useOldLang from '../../../hooks/useOldLang'; import GroupChatInfo from '../../common/GroupChatInfo'; import PrivateChatInfo from '../../common/PrivateChatInfo'; @@ -35,6 +38,7 @@ type StateProps = { isFetchingDifference?: boolean; typingStatus?: ApiTypingStatus; isSavedDialog?: boolean; + messagesCount?: number; unreadCount?: number; hasUnreadMark?: boolean; }; @@ -50,23 +54,26 @@ const QuickPreviewModalHeader: FC = ({ isFetchingDifference, typingStatus, isSavedDialog, + messagesCount, unreadCount, hasUnreadMark, onClose, }) => { const lang = useLang(); - const oldLang = useOldLang(); const { markChatMessagesRead } = getActions(); const { connectionStatusText, - } = useConnectionStatus(oldLang, connectionState, isSyncing || isFetchingDifference, true); + } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true); const handleMarkAsRead = useLastCallback(() => { markChatMessagesRead({ id: chatId }); }); - const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined; - const realChatId = isSavedDialog ? String(MAIN_THREAD_ID) : chatId; + const savedMessagesStatus = isSavedDialog + ? (messagesCount !== undefined + ? lang('Messages', { count: messagesCount }, { pluralValue: messagesCount }) : lang('SavedMessages')) + : undefined; + const realChatId = isSavedDialog && threadId ? String(threadId) : chatId; const displayChatId = chat?.isMonoforum ? chat.linkedMonoforumId! : realChatId; return ( @@ -137,6 +144,9 @@ export default memo(withGlobal( const chat = selectChat(global, chatId); const typingStatus = selectThreadLocalStateParam(global, chatId, threadId || MAIN_THREAD_ID, 'typingStatus'); const isSavedDialog = getIsSavedDialog(chatId, threadId || MAIN_THREAD_ID, global.currentUserId); + const messagesCount = isSavedDialog && threadId + ? selectThreadMessagesCount(global, chatId, threadId) + : undefined; const readState = selectThreadReadState(global, chatId, threadId || MAIN_THREAD_ID); const unreadCount = readState?.unreadCount; @@ -147,6 +157,7 @@ export default memo(withGlobal( isFetchingDifference: global.isFetchingDifference, typingStatus, isSavedDialog, + messagesCount, unreadCount, hasUnreadMark: readState?.hasUnreadMark, }; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 4a08fac11..25f009357 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -684,7 +684,18 @@ addActionHandler('requestSavedDialogUpdate', async (global, actions, payload): P global = addMessages(global, result.messages); if (result.messages.length) { - global = updateChatLastMessageId(global, chatId, result.messages[0].id, 'saved'); + const currentUserId = global.currentUserId!; + const messagesCount = result.count ?? result.messages.length; + const lastMessageId = result.messages[0].id; + + global = updateThreadInfo(global, { + isCommentsInfo: false, + chatId: currentUserId, + threadId: chatId, + lastMessageId, + messagesCount, + }); + global = updateChatLastMessageId(global, chatId, lastMessageId, 'saved'); global = addChatListIds(global, 'saved', [chatId]); setGlobal(global); diff --git a/src/hooks/useConnectionStatus.ts b/src/hooks/useConnectionStatus.ts index 3599c7358..ea70a30c3 100644 --- a/src/hooks/useConnectionStatus.ts +++ b/src/hooks/useConnectionStatus.ts @@ -1,5 +1,5 @@ import type { GlobalState } from '../global/types'; -import type { OldLangFn } from './useOldLang'; +import type { LangFn } from '../util/localization'; import useBrowserOnline from './window/useBrowserOnline'; @@ -16,7 +16,7 @@ type ConnectionStatusPosition = | 'none'; export default function useConnectionStatus( - lang: OldLangFn, + lang: LangFn, connectionState: GlobalState['connectionState'], isSyncing: boolean | undefined, hasMiddleHeader: boolean, diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 5f96ec5ad..4922e07c9 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -686,6 +686,8 @@ export interface LangPair { 'PinnedMessageTitleSingle': undefined; 'AccPinnedMessages': undefined; 'AccUnpinMessage': undefined; + 'CommentsTitle': undefined; + 'RepliesTitle': undefined; 'LeaveAComment': undefined; 'PollsStopWarning': undefined; 'PollsStopSure': undefined; @@ -3749,6 +3751,9 @@ export interface LangPairPluralWithVariables { 'Comments': { 'count': V; }; + 'Replies': { + 'count': V; + }; 'ChatContextReactionCount': { 'count': V; };