Topics: Fix loading after refresh (#6967)

This commit is contained in:
zubiden 2026-06-01 01:16:04 +02:00 committed by Alexander Zinchuk
parent 73b5ebeeba
commit d7082efd93
6 changed files with 54 additions and 24 deletions

View File

@ -17,7 +17,7 @@ import {
selectMonoforumChannel, selectMonoforumChannel,
selectPeer, selectPeer,
selectTabState, selectTabState,
selectTopics, selectTopicsInfo,
selectUserStatus, selectUserStatus,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { selectAnimationLevel } from '../../../global/selectors/sharedState'; import { selectAnimationLevel } from '../../../global/selectors/sharedState';
@ -135,15 +135,16 @@ const ChatOrUserPicker = ({
useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch); useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch);
useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE); useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE);
const selectTopicsById = useCallback((global: GlobalState) => { const selectForumTopicsInfo = useCallback((global: GlobalState) => {
if (!forumId) { if (!forumId) {
return undefined; return undefined;
} }
return selectTopics(global, forumId); return selectTopicsInfo(global, forumId);
}, [forumId]); }, [forumId]);
const forumTopicsById = useSelector(selectTopicsById); const topicsInfo = useSelector(selectForumTopicsInfo);
const forumTopicsById = topicsInfo?.topicsById;
const [topicIds, topics] = useMemo(() => { const [topicIds, topics] = useMemo(() => {
const global = getGlobal(); const global = getGlobal();
@ -194,7 +195,7 @@ const ChatOrUserPicker = ({
const chatId = viewportIds[index === -1 ? 0 : index]; const chatId = viewportIds[index === -1 ? 0 : index];
const chat = chatsById[chatId]; const chat = chatsById[chatId];
if (chat?.isForum) { if (chat?.isForum) {
if (!forumTopicsById) loadTopics({ chatId }); if (!topicsInfo || topicsInfo.isCache) loadTopics({ chatId });
setForumId(chatId); setForumId(chatId);
} else { } else {
onSelectChatOrUser(chatId); onSelectChatOrUser(chatId);
@ -218,7 +219,7 @@ const ChatOrUserPicker = ({
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;
const chat = chatsById?.[chatId]; const chat = chatsById?.[chatId];
if (chat?.isForum) { if (chat?.isForum) {
if (!forumTopicsById) loadTopics({ chatId }); if (!topicsInfo || topicsInfo.isCache) loadTopics({ chatId });
setForumId(chatId); setForumId(chatId);
resetSearch(); resetSearch();
} else { } else {

View File

@ -18,7 +18,7 @@ import type {
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
import { MAIN_THREAD_ID } from '../../../api/types'; 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 { ALL_FOLDER_ID, UNMUTE_TIMESTAMP } from '../../../config';
import { import {
@ -106,7 +106,7 @@ type StateProps = {
chat?: ApiChat; chat?: ApiChat;
monoforumChannel?: ApiChat; monoforumChannel?: ApiChat;
lastMessageStory?: ApiTypeStory; lastMessageStory?: ApiTypeStory;
listedTopicIds?: number[]; topicsInfo?: TopicsInfo;
isMuted?: boolean; isMuted?: boolean;
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
@ -139,7 +139,7 @@ const Chat: FC<OwnProps & StateProps> = ({
shiftDiff, shiftDiff,
animationType, animationType,
isPinned, isPinned,
listedTopicIds, topicsInfo,
observeIntersection, observeIntersection,
chat, chat,
monoforumChannel, monoforumChannel,
@ -204,6 +204,7 @@ const Chat: FC<OwnProps & StateProps> = ({
const { isForum, isForumAsMessages, isMonoforum } = chat || {}; const { isForum, isForumAsMessages, isMonoforum } = chat || {};
const listedTopicIds = topicsInfo?.listedTopicIds;
const shouldForceNonForumView = chat?.isBotForum && listedTopicIds && !listedTopicIds.length; const shouldForceNonForumView = chat?.isBotForum && listedTopicIds && !listedTopicIds.length;
useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage); useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage);
@ -373,10 +374,10 @@ const Chat: FC<OwnProps & StateProps> = ({
// Load the forum topics to display unread count badge // Load the forum topics to display unread count badge
useEffect(() => { useEffect(() => {
if (isIntersecting && isForum && isSynced && listedTopicIds === undefined) { if (isIntersecting && isForum && isSynced && (!topicsInfo || topicsInfo.isCache)) {
loadTopics({ chatId }); loadTopics({ chatId });
} }
}, [chatId, listedTopicIds, isSynced, isForum, isIntersecting]); }, [chatId, topicsInfo, isSynced, isForum, isIntersecting]);
const isOnline = user && userStatus && isUserOnline(user, userStatus); const isOnline = user && userStatus && isUserOnline(user, userStatus);
const { hasShownClass: isAvatarOnlineShown } = useShowTransitionDeprecated(isOnline); const { hasShownClass: isAvatarOnlineShown } = useShowTransitionDeprecated(isOnline);
@ -598,7 +599,7 @@ export default memo(withGlobal<OwnProps>(
lastMessage, lastMessage,
lastMessageId, lastMessageId,
currentUserId: global.currentUserId!, currentUserId: global.currentUserId!,
listedTopicIds: topicsInfo?.listedTopicIds, topicsInfo,
isSynced: global.isSynced, isSynced: global.isSynced,
lastMessageStory, lastMessageStory,
isAccountFrozen, isAccountFrozen,

View File

@ -34,7 +34,6 @@ import { mapTruthyValues, mapValues } from '../../../../util/iteratees';
import useSelector from '../../../../hooks/data/useSelector'; import useSelector from '../../../../hooks/data/useSelector';
import useAppLayout from '../../../../hooks/useAppLayout'; import useAppLayout from '../../../../hooks/useAppLayout';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import useHistoryBack from '../../../../hooks/useHistoryBack'; import useHistoryBack from '../../../../hooks/useHistoryBack';
import useInfiniteScroll from '../../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../../hooks/useInfiniteScroll';
import { useIntersectionObserver, useOnIntersect } from '../../../../hooks/useIntersectionObserver'; import { useIntersectionObserver, useOnIntersect } from '../../../../hooks/useIntersectionObserver';
@ -97,13 +96,11 @@ const ForumPanel = ({
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const chatId = chat?.id; const chatId = chat?.id;
useEffectWithPrevDeps(([prevIsSynced]) => { useEffect(() => {
if (!isSynced) return; if (!chatId || !isSynced) return;
const hasJustSynced = prevIsSynced === false; if (topicsInfo && !topicsInfo.isCache) return;
if (chatId && (hasJustSynced || !topicsInfo)) { loadTopics({ chatId });
loadTopics({ chatId, force: hasJustSynced }); }, [chatId, topicsInfo, isSynced]);
}
}, [isSynced, chatId, topicsInfo]);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const lang = useLang(); const lang = useLang();

View File

@ -2622,12 +2622,17 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
if (!chat) return; if (!chat) return;
const topicsInfo = selectTopicsInfo(global, chatId); 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; return;
} }
const offsetTopic = !force ? topicsInfo?.listedTopicIds?.reduce((acc, el) => { const offsetTopic = !shouldRefreshFromStart ? topicsInfo?.listedTopicIds?.reduce((acc, el) => {
const topicThreadInfo = selectThreadInfo(global, chatId, el); const topicThreadInfo = selectThreadInfo(global, chatId, el);
const accTopicThreadInfo = selectThreadInfo(global, chatId, acc); const accTopicThreadInfo = selectThreadInfo(global, chatId, acc);
if (!topicThreadInfo?.lastMessageId) return acc; if (!topicThreadInfo?.lastMessageId) return acc;
@ -2649,12 +2654,22 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
if (!result) return; if (!result) return;
global = getGlobal(); 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); global = addMessages(global, result.messages);
result.topics.forEach((topic) => { result.topics.forEach((topic) => {
global = updateTopicWithState(global, chatId, topic); global = updateTopicWithState(global, chatId, topic);
}); });
global = updateTopicsInfo(global, chatId, { global = updateTopicsInfo(global, chatId, {
totalCount: result.count, totalCount: result.count,
isCache: undefined,
}); });
global = updateListedTopicIds(global, chatId, result.topics.map((topicState) => topicState.topic.id)); global = updateListedTopicIds(global, chatId, result.topics.map((topicState) => topicState.topic.id));
Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => { Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => {

View File

@ -5,7 +5,7 @@ import type {
ApiAvailableReaction, ApiAvailableReaction,
ApiMessage, ApiMessage,
} from '../api/types'; } from '../api/types';
import type { MessageList, ThreadId } from '../types'; import type { MessageList, ThreadId, TopicsInfo } from '../types';
import type { ActionReturnType, GlobalState, SharedState } from './types'; import type { ActionReturnType, GlobalState, SharedState } from './types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../api/types';
@ -649,10 +649,25 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
all: pickTruthy(global.chats.lastMessageIds.all || {}, idsToSave), all: pickTruthy(global.chats.lastMessageIds.all || {}, idsToSave),
saved: global.chats.lastMessageIds.saved, saved: global.chats.lastMessageIds.saved,
}, },
topicsInfoById: pickTruthy(global.chats.topicsInfoById, currentChatIds), topicsInfoById: reduceTopicsInfo(global.chats.topicsInfoById, currentChatIds),
}; };
} }
function reduceTopicsInfo(
topicsInfoById: Record<string, TopicsInfo>, 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<T extends GlobalState>(global: T) { function getTopPeerIds<T extends GlobalState>(global: T) {
return unique(Object.values(global.topPeerCategories).flatMap((category) => category?.peerIds || [])); return unique(Object.values(global.topPeerCategories).flatMap((category) => category?.peerIds || []));
} }

View File

@ -674,6 +674,7 @@ export interface ServiceNotification {
export interface TopicsInfo { export interface TopicsInfo {
totalCount: number; totalCount: number;
isCache?: true;
topicsById: Record<ThreadId, ApiTopic>; topicsById: Record<ThreadId, ApiTopic>;
listedTopicIds?: number[]; listedTopicIds?: number[];
orderedPinnedTopicIds?: number[]; orderedPinnedTopicIds?: number[];