From d7082efd93eaf50c65bff42b9b9b98a237c58d2b Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:16:04 +0200 Subject: [PATCH] Topics: Fix loading after refresh (#6967) --- .../common/pickers/ChatOrUserPicker.tsx | 13 +++++++------ src/components/left/main/Chat.tsx | 13 +++++++------ src/components/left/main/forum/ForumPanel.tsx | 13 +++++-------- src/global/actions/api/chats.ts | 19 +++++++++++++++++-- src/global/cache.ts | 19 +++++++++++++++++-- src/types/index.ts | 1 + 6 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/components/common/pickers/ChatOrUserPicker.tsx b/src/components/common/pickers/ChatOrUserPicker.tsx index 60decceac..d1d0308bd 100644 --- a/src/components/common/pickers/ChatOrUserPicker.tsx +++ b/src/components/common/pickers/ChatOrUserPicker.tsx @@ -17,7 +17,7 @@ import { selectMonoforumChannel, selectPeer, selectTabState, - selectTopics, + selectTopicsInfo, selectUserStatus, } from '../../../global/selectors'; import { selectAnimationLevel } from '../../../global/selectors/sharedState'; @@ -135,15 +135,16 @@ const ChatOrUserPicker = ({ useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch); useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE); - const selectTopicsById = useCallback((global: GlobalState) => { + const selectForumTopicsInfo = useCallback((global: GlobalState) => { if (!forumId) { return undefined; } - return selectTopics(global, forumId); + return selectTopicsInfo(global, forumId); }, [forumId]); - const forumTopicsById = useSelector(selectTopicsById); + const topicsInfo = useSelector(selectForumTopicsInfo); + const forumTopicsById = topicsInfo?.topicsById; const [topicIds, topics] = useMemo(() => { const global = getGlobal(); @@ -194,7 +195,7 @@ const ChatOrUserPicker = ({ const chatId = viewportIds[index === -1 ? 0 : index]; const chat = chatsById[chatId]; if (chat?.isForum) { - if (!forumTopicsById) loadTopics({ chatId }); + if (!topicsInfo || topicsInfo.isCache) loadTopics({ chatId }); setForumId(chatId); } else { onSelectChatOrUser(chatId); @@ -218,7 +219,7 @@ const ChatOrUserPicker = ({ const chatsById = getGlobal().chats.byId; const chat = chatsById?.[chatId]; if (chat?.isForum) { - if (!forumTopicsById) loadTopics({ chatId }); + if (!topicsInfo || topicsInfo.isCache) loadTopics({ chatId }); setForumId(chatId); resetSearch(); } else { diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 277b94c75..66a8d9181 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -18,7 +18,7 @@ import type { import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ChatAnimationTypes } from './hooks'; import { MAIN_THREAD_ID } from '../../../api/types'; -import { StoryViewerOrigin } from '../../../types'; +import { StoryViewerOrigin, type TopicsInfo } from '../../../types'; import { ALL_FOLDER_ID, UNMUTE_TIMESTAMP } from '../../../config'; import { @@ -106,7 +106,7 @@ type StateProps = { chat?: ApiChat; monoforumChannel?: ApiChat; lastMessageStory?: ApiTypeStory; - listedTopicIds?: number[]; + topicsInfo?: TopicsInfo; isMuted?: boolean; user?: ApiUser; userStatus?: ApiUserStatus; @@ -139,7 +139,7 @@ const Chat: FC = ({ shiftDiff, animationType, isPinned, - listedTopicIds, + topicsInfo, observeIntersection, chat, monoforumChannel, @@ -204,6 +204,7 @@ const Chat: FC = ({ const { isForum, isForumAsMessages, isMonoforum } = chat || {}; + const listedTopicIds = topicsInfo?.listedTopicIds; const shouldForceNonForumView = chat?.isBotForum && listedTopicIds && !listedTopicIds.length; useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage); @@ -373,10 +374,10 @@ const Chat: FC = ({ // Load the forum topics to display unread count badge useEffect(() => { - if (isIntersecting && isForum && isSynced && listedTopicIds === undefined) { + if (isIntersecting && isForum && isSynced && (!topicsInfo || topicsInfo.isCache)) { loadTopics({ chatId }); } - }, [chatId, listedTopicIds, isSynced, isForum, isIntersecting]); + }, [chatId, topicsInfo, isSynced, isForum, isIntersecting]); const isOnline = user && userStatus && isUserOnline(user, userStatus); const { hasShownClass: isAvatarOnlineShown } = useShowTransitionDeprecated(isOnline); @@ -598,7 +599,7 @@ export default memo(withGlobal( lastMessage, lastMessageId, currentUserId: global.currentUserId!, - listedTopicIds: topicsInfo?.listedTopicIds, + topicsInfo, isSynced: global.isSynced, lastMessageStory, isAccountFrozen, diff --git a/src/components/left/main/forum/ForumPanel.tsx b/src/components/left/main/forum/ForumPanel.tsx index ccd0210be..e28e2307c 100644 --- a/src/components/left/main/forum/ForumPanel.tsx +++ b/src/components/left/main/forum/ForumPanel.tsx @@ -34,7 +34,6 @@ import { mapTruthyValues, mapValues } from '../../../../util/iteratees'; import useSelector from '../../../../hooks/data/useSelector'; import useAppLayout from '../../../../hooks/useAppLayout'; -import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps'; import useHistoryBack from '../../../../hooks/useHistoryBack'; import useInfiniteScroll from '../../../../hooks/useInfiniteScroll'; import { useIntersectionObserver, useOnIntersect } from '../../../../hooks/useIntersectionObserver'; @@ -97,13 +96,11 @@ const ForumPanel = ({ const { isMobile } = useAppLayout(); const chatId = chat?.id; - useEffectWithPrevDeps(([prevIsSynced]) => { - if (!isSynced) return; - const hasJustSynced = prevIsSynced === false; - if (chatId && (hasJustSynced || !topicsInfo)) { - loadTopics({ chatId, force: hasJustSynced }); - } - }, [isSynced, chatId, topicsInfo]); + useEffect(() => { + if (!chatId || !isSynced) return; + if (topicsInfo && !topicsInfo.isCache) return; + loadTopics({ chatId }); + }, [chatId, topicsInfo, isSynced]); const [isScrolled, setIsScrolled] = useState(false); const lang = useLang(); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 6f659d982..d80359aea 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -2622,12 +2622,17 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise = if (!chat) return; const topicsInfo = selectTopicsInfo(global, chatId); + const shouldRefreshFromStart = force || topicsInfo?.isCache; - if (!force && topicsInfo?.listedTopicIds && topicsInfo.listedTopicIds.length === topicsInfo.totalCount) { + if ( + !shouldRefreshFromStart + && topicsInfo?.listedTopicIds + && topicsInfo.listedTopicIds.length === topicsInfo.totalCount + ) { return; } - const offsetTopic = !force ? topicsInfo?.listedTopicIds?.reduce((acc, el) => { + const offsetTopic = !shouldRefreshFromStart ? topicsInfo?.listedTopicIds?.reduce((acc, el) => { const topicThreadInfo = selectThreadInfo(global, chatId, el); const accTopicThreadInfo = selectThreadInfo(global, chatId, acc); if (!topicThreadInfo?.lastMessageId) return acc; @@ -2649,12 +2654,22 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise = if (!result) return; global = getGlobal(); + const updatedTopicsInfo = selectTopicsInfo(global, chatId); + if (updatedTopicsInfo?.isCache) { // Reset local state + global = updateTopicsInfo(global, chatId, { + topicsById: {}, + listedTopicIds: [], + orderedPinnedTopicIds: undefined, + }); + } + global = addMessages(global, result.messages); result.topics.forEach((topic) => { global = updateTopicWithState(global, chatId, topic); }); global = updateTopicsInfo(global, chatId, { totalCount: result.count, + isCache: undefined, }); global = updateListedTopicIds(global, chatId, result.topics.map((topicState) => topicState.topic.id)); Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => { diff --git a/src/global/cache.ts b/src/global/cache.ts index 4ae6bdd09..2a0bf94c5 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -5,7 +5,7 @@ import type { ApiAvailableReaction, ApiMessage, } from '../api/types'; -import type { MessageList, ThreadId } from '../types'; +import type { MessageList, ThreadId, TopicsInfo } from '../types'; import type { ActionReturnType, GlobalState, SharedState } from './types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../api/types'; @@ -649,10 +649,25 @@ function reduceChats(global: T): GlobalState['chats'] { all: pickTruthy(global.chats.lastMessageIds.all || {}, idsToSave), saved: global.chats.lastMessageIds.saved, }, - topicsInfoById: pickTruthy(global.chats.topicsInfoById, currentChatIds), + topicsInfoById: reduceTopicsInfo(global.chats.topicsInfoById, currentChatIds), }; } +function reduceTopicsInfo( + topicsInfoById: Record, chatIds: string[], +): GlobalState['chats']['topicsInfoById'] { + const topicsInfoToSave = pickTruthy(topicsInfoById, chatIds); + + return Object.entries(topicsInfoToSave).reduce((acc, [chatId, topicsInfo]) => { + acc[chatId] = { + ...topicsInfo, + isCache: true, + }; + + return acc; + }, {} as GlobalState['chats']['topicsInfoById']); +} + function getTopPeerIds(global: T) { return unique(Object.values(global.topPeerCategories).flatMap((category) => category?.peerIds || [])); } diff --git a/src/types/index.ts b/src/types/index.ts index 7bd9b38ad..dca9a5d02 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -674,6 +674,7 @@ export interface ServiceNotification { export interface TopicsInfo { totalCount: number; + isCache?: true; topicsById: Record; listedTopicIds?: number[]; orderedPinnedTopicIds?: number[];