diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 7739caa56..6dc4acb49 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -1,4 +1,3 @@ -import type { ThreadId } from '../../types'; import type { ApiBotCommand } from './bots'; import type { ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet, @@ -48,10 +47,6 @@ export interface ApiChat { emojiStatus?: ApiEmojiStatus; isForum?: boolean; isForumAsMessages?: true; - topics?: Record; - listedTopicIds?: number[]; - topicsCount?: number; - orderedPinnedTopicIds?: number[]; boostLevel?: number; // Calls diff --git a/src/components/common/ChatForumLastMessage.tsx b/src/components/common/ChatForumLastMessage.tsx index 0be542f8d..abada322a 100644 --- a/src/components/common/ChatForumLastMessage.tsx +++ b/src/components/common/ChatForumLastMessage.tsx @@ -8,7 +8,7 @@ import React, { } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiChat } from '../../api/types'; +import type { ApiChat, ApiTopic } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { getOrderedTopics } from '../../global/helpers'; @@ -26,6 +26,7 @@ import styles from './ChatForumLastMessage.module.scss'; type OwnProps = { chat: ApiChat; + topics?: Record; renderLastMessage: () => React.ReactNode; observeIntersection?: ObserveFn; }; @@ -35,6 +36,7 @@ const MAX_TOPICS = 3; const ChatForumLastMessage: FC = ({ chat, + topics, renderLastMessage, observeIntersection, }) => { @@ -48,12 +50,12 @@ const ChatForumLastMessage: FC = ({ const lang = useOldLang(); const [lastActiveTopic, ...otherTopics] = useMemo(() => { - if (!chat.topics) { + if (!topics) { return []; } - return getOrderedTopics(Object.values(chat.topics), undefined, true).slice(0, MAX_TOPICS); - }, [chat.topics]); + return getOrderedTopics(Object.values(topics), undefined, true).slice(0, MAX_TOPICS); + }, [topics]); const [isReversedCorner, setIsReversedCorner] = useState(false); const [overwrittenWidth, setOverwrittenWidth] = useState(undefined); diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 56afbb05c..1055c5bfc 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -20,6 +20,7 @@ import { selectChatOnlineCount, selectThreadInfo, selectThreadMessagesCount, + selectTopic, selectUser, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; @@ -267,7 +268,7 @@ export default memo(withGlobal( const threadInfo = threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined; const areMessagesLoaded = Boolean(selectChatMessages(global, chatId)); - const topic = threadId ? chat?.topics?.[threadId] : undefined; + 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/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 396cb6a32..ee24b3e15 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -16,6 +16,7 @@ import { selectPeerPhotos, selectTabState, selectThreadMessagesCount, + selectTopic, selectUser, selectUserStatus, } from '../../global/selectors'; @@ -373,7 +374,7 @@ export default memo(withGlobal( const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer; const isForum = chat?.isForum; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; - const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined; + const topic = isForum && currentTopicId ? selectTopic(global, peerId, currentTopicId) : undefined; const emojiStatus = (user || chat)?.emojiStatus; const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; diff --git a/src/components/common/RecipientPicker.tsx b/src/components/common/RecipientPicker.tsx index 0953ceecb..90aca7a68 100644 --- a/src/components/common/RecipientPicker.tsx +++ b/src/components/common/RecipientPicker.tsx @@ -4,7 +4,6 @@ import { getGlobal, withGlobal } from '../../global'; import type { ApiChatType } from '../../api/types'; import type { ThreadId } from '../../types'; -import { MAIN_THREAD_ID } from '../../api/types'; import { API_CHAT_TYPES } from '../../config'; import { @@ -80,7 +79,7 @@ const RecipientPicker: FC = ({ const user = usersById[id]; if (user && isDeletedUser(user)) return false; - return chat && getCanPostInChat(chat, MAIN_THREAD_ID, undefined, chatFullInfoById[id]); + return chat && getCanPostInChat(chat, undefined, undefined, chatFullInfoById[id]); }); const sorted = sortChatIds( diff --git a/src/components/common/StickerSetModal.tsx b/src/components/common/StickerSetModal.tsx index 7e9042907..6f38b7f1b 100644 --- a/src/components/common/StickerSetModal.tsx +++ b/src/components/common/StickerSetModal.tsx @@ -19,6 +19,7 @@ import { selectShouldSchedule, selectStickerSet, selectThreadInfo, + selectTopic, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { copyTextToClipboard } from '../../util/clipboard'; @@ -263,8 +264,9 @@ export default memo(withGlobal( const sendOptions = chat ? getAllowedAttachmentOptions(chat, chatFullInfo) : undefined; const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); + const topic = chatId && threadId ? selectTopic(global, chatId, threadId) : undefined; const canSendStickers = Boolean( - chat && threadId && getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo) + chat && threadId && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo) && sendOptions?.canSendStickers, ); const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId); diff --git a/src/components/common/pickers/ChatOrUserPicker.tsx b/src/components/common/pickers/ChatOrUserPicker.tsx index a8d0fff86..f40f55806 100644 --- a/src/components/common/pickers/ChatOrUserPicker.tsx +++ b/src/components/common/pickers/ChatOrUserPicker.tsx @@ -13,7 +13,7 @@ import { getCanPostInChat, getGroupStatus, getUserStatus, isUserOnline, } from '../../../global/helpers'; import { isApiPeerChat } from '../../../global/helpers/peers'; -import { selectChat, selectPeer, selectUserStatus } from '../../../global/selectors'; +import { selectPeer, selectTopics, selectUserStatus } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { REM } from '../helpers/mediaDimensions'; import renderText from '../helpers/renderText'; @@ -94,15 +94,15 @@ const ChatOrUserPicker: FC = ({ useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch); useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE); - const selectTopics = useLastCallback((global: GlobalState) => { + const selectTopicsById = useLastCallback((global: GlobalState) => { if (!forumId) { return undefined; } - return selectChat(global, forumId)?.topics; + return selectTopics(global, forumId); }); - const forumTopics = useSelector(selectTopics); + const forumTopicsById = useSelector(selectTopicsById); const [topicIds, topics] = useMemo(() => { const global = getGlobal(); @@ -111,16 +111,16 @@ const ChatOrUserPicker: FC = ({ const chat = chatsById[forumId!]; - if (!chat || !forumTopics) { + if (!chat || !forumTopicsById) { return [undefined, undefined]; } const searchTitle = topicSearch.toLowerCase(); - const result = forumTopics - ? Object.values(forumTopics).reduce((acc, topic) => { + const result = forumTopicsById + ? Object.values(forumTopicsById).reduce((acc, topic) => { if ( - getCanPostInChat(chat, topic.id, undefined, chatFullInfoById[forumId!]) + getCanPostInChat(chat, topic, undefined, chatFullInfoById[forumId!]) && (!searchTitle || topic.title.toLowerCase().includes(searchTitle)) ) { acc[topic.id] = topic; @@ -128,10 +128,10 @@ const ChatOrUserPicker: FC = ({ return acc; }, {} as Record) - : forumTopics; + : forumTopicsById; return [Object.keys(result).map(Number), result]; - }, [forumId, topicSearch, forumTopics]); + }, [forumId, topicSearch, forumTopicsById]); const handleHeaderBackClick = useLastCallback(() => { setForumId(undefined); @@ -153,7 +153,7 @@ const ChatOrUserPicker: FC = ({ const chatId = viewportIds[index === -1 ? 0 : index]; const chat = chatsById[chatId]; if (chat?.isForum) { - if (!chat.topics) loadTopics({ chatId }); + if (!forumTopicsById) loadTopics({ chatId }); setForumId(chatId); } else { onSelectChatOrUser(chatId); @@ -171,7 +171,7 @@ const ChatOrUserPicker: FC = ({ const chatsById = getGlobal().chats.byId; const chat = chatsById?.[chatId]; if (chat?.isForum) { - if (!chat.topics) loadTopics({ chatId }); + if (!forumTopicsById) loadTopics({ chatId }); setForumId(chatId); resetSearch(); } else { diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 7f7b283cf..177b868ef 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -43,6 +43,7 @@ import { selectTabState, selectThreadParam, selectTopicFromMessage, + selectTopicsInfo, selectUser, selectUserStatus, } from '../../../global/selectors'; @@ -90,6 +91,8 @@ type OwnProps = { type StateProps = { chat?: ApiChat; + listedTopicIds?: number[]; + topics?: Record; isMuted?: boolean; user?: ApiUser; userStatus?: ApiUserStatus; @@ -118,6 +121,8 @@ const Chat: FC = ({ orderDiff, animationType, isPinned, + listedTopicIds, + topics, observeIntersection, chat, isMuted, @@ -190,6 +195,7 @@ const Chat: FC = ({ orderDiff, isSavedDialog, isPreview, + topics, }); const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); @@ -284,10 +290,10 @@ const Chat: FC = ({ // Load the forum topics to display unread count badge useEffect(() => { - if (isIntersecting && isForum && chat && chat.listedTopicIds === undefined) { + if (isIntersecting && isForum && listedTopicIds === undefined) { loadTopics({ chatId }); } - }, [chat, chatId, isForum, isIntersecting]); + }, [chatId, listedTopicIds, isForum, isIntersecting]); const isOnline = user && userStatus && isUserOnline(user, userStatus); const { hasShownClass: isAvatarOnlineShown } = useShowTransitionDeprecated(isOnline); @@ -343,7 +349,13 @@ const Chat: FC = ({ />
- +
{chat.isCallActive && chat.isCallNotEmpty && ( @@ -376,6 +388,7 @@ const Chat: FC = ({ isPinned={isPinned} isMuted={isMuted} isSavedDialog={isSavedDialog} + topics={topics} /> )}
@@ -460,6 +473,8 @@ export default memo(withGlobal( const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus'); + const topicsInfo = selectTopicsInfo(global, chatId); + return { chat, isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), @@ -484,6 +499,8 @@ export default memo(withGlobal( lastMessage, lastMessageId, currentUserId: global.currentUserId!, + listedTopicIds: topicsInfo?.listedTopicIds, + topics: topicsInfo?.topicsById, }; }, )(Chat)); diff --git a/src/components/left/main/ChatBadge.tsx b/src/components/left/main/ChatBadge.tsx index e39b9c291..6a83c52de 100644 --- a/src/components/left/main/ChatBadge.tsx +++ b/src/components/left/main/ChatBadge.tsx @@ -24,10 +24,19 @@ type OwnProps = { isSavedDialog?: boolean; shouldShowOnlyMostImportant?: boolean; forceHidden?: boolean | Signal; + topics?: Record; }; const ChatBadge: FC = ({ - topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden, isSavedDialog, + topic, + topics, + chat, + isPinned, + isMuted, + shouldShowOnlyMostImportant, + wasTopicOpened, + forceHidden, + isSavedDialog, }) => { const { unreadMentionsCount = 0, unreadReactionsCount = 0, @@ -36,8 +45,8 @@ const ChatBadge: FC = ({ const isTopicUnopened = !isPinned && topic && !wasTopicOpened; const isForum = chat.isForum && !topic; const topicsWithUnread = useMemo(() => ( - isForum && chat?.topics ? Object.values(chat.topics).filter(({ unreadCount }) => unreadCount) : undefined - ), [chat, isForum]); + isForum && topics ? Object.values(topics).filter(({ unreadCount }) => unreadCount) : undefined + ), [topics, isForum]); const unreadCount = useMemo(() => ( isForum @@ -48,11 +57,11 @@ const ChatBadge: FC = ({ ), [chat, topic, topicsWithUnread, isForum, isMuted]); const shouldBeMuted = useMemo(() => { - const hasUnmutedUnreadTopics = chat.topics - && Object.values(chat.topics).some((acc) => !acc.isMuted && acc.unreadCount); + const hasUnmutedUnreadTopics = topics + && Object.values(topics).some((acc) => !acc.isMuted && acc.unreadCount); - return isMuted || (chat.topics && !hasUnmutedUnreadTopics); - }, [chat, isMuted]); + return isMuted || (topics && !hasUnmutedUnreadTopics); + }, [topics, isMuted]); const hasUnreadMark = topic ? false : chat.hasUnreadMark; diff --git a/src/components/left/main/ForumPanel.tsx b/src/components/left/main/ForumPanel.tsx index 99c8b3068..3b983ae7b 100644 --- a/src/components/left/main/ForumPanel.tsx +++ b/src/components/left/main/ForumPanel.tsx @@ -6,6 +6,7 @@ import React, { import { getActions, withGlobal } from '../../../global'; import type { ApiChat } from '../../../api/types'; +import type { TopicsInfo } from '../../../global/types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { @@ -14,7 +15,12 @@ import { import { requestNextMutation } from '../../../lib/fasterdom/fasterdom'; import { getOrderedTopics } from '../../../global/helpers'; import { - selectCanAnimateInterface, selectChat, selectCurrentMessageList, selectIsForumPanelOpen, selectTabState, + selectCanAnimateInterface, + selectChat, + selectCurrentMessageList, + selectIsForumPanelOpen, + selectTabState, + selectTopicsInfo, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; @@ -52,6 +58,7 @@ type OwnProps = { type StateProps = { chat?: ApiChat; + topicsInfo?: TopicsInfo; currentTopicId?: number; withInterfaceAnimations?: boolean; }; @@ -63,6 +70,7 @@ const ForumPanel: FC = ({ currentTopicId, isOpen, isHidden, + topicsInfo, onTopicSearch, onCloseAnimationEnd, onOpenAnimationStart, @@ -80,12 +88,13 @@ const ForumPanel: FC = ({ // eslint-disable-next-line no-null/no-null const scrollTopHandlerRef = useRef(null); const { isMobile } = useAppLayout(); + const chatId = chat?.id; useEffect(() => { - if (chat && !chat.topics) { - loadTopics({ chatId: chat.id }); + if (chatId && !topicsInfo) { + loadTopics({ chatId }); } - }, [chat, loadTopics]); + }, [topicsInfo, chatId]); const [isScrolled, setIsScrolled] = useState(false); const lang = useOldLang(); @@ -115,17 +124,20 @@ const ForumPanel: FC = ({ }); const orderedIds = useMemo(() => { - return chat?.topics - ? getOrderedTopics(Object.values(chat.topics), chat.orderedPinnedTopicIds).map(({ id }) => id) + return topicsInfo + ? getOrderedTopics( + Object.values(topicsInfo.topicsById), + topicsInfo.orderedPinnedTopicIds, + ).map(({ id }) => id) : []; - }, [chat]); + }, [topicsInfo]); const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds, chat?.id); const [viewportIds, getMore] = useInfiniteScroll(() => { if (!chat) return; loadTopics({ chatId: chat.id }); - }, orderedIds, !chat?.topicsCount || orderedIds.length >= chat.topicsCount, TOPICS_SLICE); + }, orderedIds, !topicsInfo?.totalCount || orderedIds.length >= topicsInfo.totalCount, TOPICS_SLICE); const shouldRenderRef = useRef(false); const isVisible = isOpen && !isHidden; @@ -191,7 +203,7 @@ const ForumPanel: FC = ({ = ({ )); } - const isLoading = chat?.topics === undefined; + const isLoading = topicsInfo === undefined; return (
= ({ )} {!isLoading && viewportIds?.length === 1 && viewportIds[0] === GENERAL_TOPIC_ID && ( - + )}
); @@ -285,11 +297,13 @@ export default memo(withGlobal( chatId: currentChatId, threadId: currentThreadId, } = selectCurrentMessageList(global) || {}; + const topicsInfo = chatId ? selectTopicsInfo(global, chatId) : undefined; return { chat, currentTopicId: chatId === currentChatId ? Number(currentThreadId) : undefined, withInterfaceAnimations: selectCanAnimateInterface(global), + topicsInfo, }; }, (global) => selectIsForumPanelOpen(global), diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx index 5be750603..5143862da 100644 --- a/src/components/left/main/Topic.tsx +++ b/src/components/left/main/Topic.tsx @@ -22,6 +22,7 @@ import { selectOutgoingStatus, selectThreadInfo, selectThreadParam, + selectTopics, selectUser, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; @@ -68,6 +69,7 @@ type StateProps = { canScrollDown?: boolean; wasTopicOpened?: boolean; withInterfaceAnimations?: boolean; + topics?: Record; }; const Topic: FC = ({ @@ -91,6 +93,7 @@ const Topic: FC = ({ typingStatus, draft, wasTopicOpened, + topics, }) => { const { openThread, @@ -138,6 +141,7 @@ const Topic: FC = ({ observeIntersection, isTopic: true, typingStatus, + topics, animationType, withInterfaceAnimations, @@ -208,6 +212,7 @@ const Topic: FC = ({ isMuted={isMuted} topic={topic} wasTopicOpened={wasTopicOpened} + topics={topics} /> @@ -253,6 +258,7 @@ export default memo(withGlobal( const draft = selectDraft(global, chatId, topic.id); const threadInfo = selectThreadInfo(global, chatId, topic.id); const wasTopicOpened = Boolean(threadInfo?.lastReadInboxMessageId); + const topics = selectTopics(global, chatId); const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {}; @@ -272,6 +278,7 @@ export default memo(withGlobal( }), canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id, wasTopicOpened, + topics, }; }, )(Topic)); diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 7f65aeb86..81d348357 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -43,6 +43,7 @@ const ANIMATION_DURATION = 200; export default function useChatListEntry({ chat, + topics, lastMessage, chatId, typingStatus, @@ -61,6 +62,7 @@ export default function useChatListEntry({ isPreview, }: { chat?: ApiChat; + topics?: Record; lastMessage?: ApiMessage; chatId: string; typingStatus?: ApiTypingStatus; @@ -192,6 +194,7 @@ export default function useChatListEntry({ chat={chat} renderLastMessage={renderLastMessageOrTyping} observeIntersection={observeIntersection} + topics={topics} /> ); } diff --git a/src/components/left/search/LeftSearchResultTopic.tsx b/src/components/left/search/LeftSearchResultTopic.tsx index 0302b4574..b6092bb6e 100644 --- a/src/components/left/search/LeftSearchResultTopic.tsx +++ b/src/components/left/search/LeftSearchResultTopic.tsx @@ -4,7 +4,7 @@ import { withGlobal } from '../../../global'; import type { ApiTopic } from '../../../api/types'; -import { selectChat } from '../../../global/selectors'; +import { selectTopic } from '../../../global/selectors'; import { REM } from '../../common/helpers/mediaDimensions'; import renderText from '../../common/helpers/renderText'; @@ -60,8 +60,7 @@ const LeftSearchResultTopic: FC = ({ export default memo(withGlobal( (global, { chatId, topicId }): StateProps => { - const chat = selectChat(global, chatId); - const topic = chat?.topics?.[topicId]; + const topic = selectTopic(global, chatId, topicId); return { topic, diff --git a/src/components/main/GameModal.tsx b/src/components/main/GameModal.tsx index aff998f11..0edfc78a8 100644 --- a/src/components/main/GameModal.tsx +++ b/src/components/main/GameModal.tsx @@ -4,7 +4,6 @@ import { getActions } from '../../lib/teact/teactn'; import { withGlobal } from '../../global'; import type { TabState } from '../../global/types'; -import { MAIN_THREAD_ID } from '../../api/types'; import { getCanPostInChat } from '../../global/helpers'; import { selectChat, selectChatFullInfo } from '../../global/selectors'; @@ -94,7 +93,7 @@ export default memo(withGlobal( const { chatId } = openedGame || {}; const chat = chatId && selectChat(global, chatId); const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; - const canPost = Boolean(chat) && getCanPostInChat(chat, MAIN_THREAD_ID, undefined, chatFullInfo); + const canPost = Boolean(chat) && getCanPostInChat(chat, undefined, undefined, chatFullInfo); return { canPost, diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 6bca09a53..e1a8cdbb0 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -35,6 +35,7 @@ import { selectNotifyExceptions, selectNotifySettings, selectTabState, + selectTopic, selectUser, selectUserFullInfo, } from '../../global/selectors'; @@ -760,7 +761,7 @@ export default memo(withGlobal( && !selectIsPremiumPurchaseBlocked(global), ); - const topic = chat?.topics?.[threadId]; + const topic = selectTopic(global, chatId, threadId); const canCreateTopic = chat.isForum && ( chat.isCreator || !isUserRightBanned(chat, 'manageTopics') || getHasAdminRight(chat, 'manageTopics') ); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index e0334aae8..409eabce4 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -54,6 +54,7 @@ import { selectScrollOffset, selectTabState, selectThreadInfo, + selectTopic, selectUserFullInfo, } from '../../global/selectors'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; @@ -772,7 +773,7 @@ export default memo(withGlobal( const chatBot = selectBot(global, chatId); - const topic = chat.topics?.[threadId]; + const topic = selectTopic(global, chatId, threadId); const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount; diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 995c33b2a..d4bdd8b53 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -4,7 +4,9 @@ import React, { } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo } from '../../api/types'; +import type { + ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo, ApiTopic, +} from '../../api/types'; import type { ActiveEmojiInteraction, MessageListType, @@ -55,6 +57,8 @@ import { selectTabState, selectTheme, selectThreadInfo, + selectTopic, + selectTopics, selectUserFullInfo, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; @@ -156,6 +160,7 @@ type StateProps = { isSavedDialog?: boolean; canShowOpenChatButton?: boolean; isContactRequirePremium?: boolean; + topics?: Record; }; function isImage(item: DataTransferItem) { @@ -217,6 +222,7 @@ function MiddleColumn({ isSavedDialog, canShowOpenChatButton, isContactRequirePremium, + topics, }: OwnProps & StateProps) { const { openChat, @@ -455,7 +461,7 @@ function MiddleColumn({ const messageSendingRestrictionReason = getMessageSendingRestrictionReason( lang, currentUserBannedRights, defaultBannedRights, ); - const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(draftReplyInfo)); + const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, topics, Boolean(draftReplyInfo)); const composerRestrictionMessage = messageSendingRestrictionReason ?? forumComposerPlaceholder @@ -775,7 +781,8 @@ export default memo(withGlobal( const threadInfo = selectThreadInfo(global, chatId, threadId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); - const canPost = chat && getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo); + const topic = selectTopic(global, chatId, threadId); + const canPost = chat && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo); const isBotNotStarted = selectIsChatBotNotStarted(global, chatId); const isPinnedMessageList = messageListType === 'pinned'; const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID; @@ -794,11 +801,12 @@ export default memo(withGlobal( ); const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo; const shouldBlockSendInForum = chat?.isForum - ? threadId === MAIN_THREAD_ID && !draftReplyInfo && (chat.topics?.[GENERAL_TOPIC_ID]?.isClosed) + ? threadId === MAIN_THREAD_ID && !draftReplyInfo && (selectTopic(global, chatId, GENERAL_TOPIC_ID)?.isClosed) : false; const audioMessage = audioChatId && audioMessageId ? selectChatMessage(global, audioChatId, audioMessageId) : undefined; + const topics = selectTopics(global, chatId); const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID; @@ -854,6 +862,7 @@ export default memo(withGlobal( isSavedDialog, canShowOpenChatButton, isContactRequirePremium, + topics, }; }, )(MiddleColumn)); diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 2d7c1677f..7cb2fe44a 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -54,6 +54,7 @@ import { selectRequestedMessageTranslationLanguage, selectStickerSet, selectThreadInfo, + selectTopic, selectUserStatus, } from '../../../global/selectors'; import { copyTextToClipboard } from '../../../util/clipboard'; @@ -730,10 +731,11 @@ export default memo(withGlobal( const threadInfo = threadId && selectThreadInfo(global, message.chatId, threadId); const isMessageThread = Boolean(threadInfo && !threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); + const topic = threadId ? selectTopic(global, message.chatId, threadId) : undefined; const canSendText = chat && !isUserRightBanned(chat, 'sendPlain', chatFullInfo); - const canReplyInChat = chat && threadId ? getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo) + const canReplyInChat = chat && threadId ? getCanPostInChat(chat, topic, isMessageThread, chatFullInfo) && canSendText : false; const isLocal = isMessageLocal(message); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index feda1727f..9a8f14ba1 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -31,7 +31,7 @@ import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessag import { MAIN_THREAD_ID } from '../../../api/types'; import { AudioOrigin } from '../../../types'; -import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config'; +import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config'; import { areReactionsEmpty, getIsDownloading, @@ -1766,8 +1766,7 @@ export default memo(withGlobal( const hasUnreadReaction = chat?.unreadReactions?.includes(message.id); const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && isFirstInGroup; - const messageTopic = hasTopicChip ? (selectTopicFromMessage(global, message) || chat?.topics?.[GENERAL_TOPIC_ID]) - : undefined; + const messageTopic = hasTopicChip ? selectTopicFromMessage(global, message) : undefined; const chatTranslations = selectChatTranslations(global, chatId); diff --git a/src/components/right/EditTopic.tsx b/src/components/right/EditTopic.tsx index 5feb9233e..864b50344 100644 --- a/src/components/right/EditTopic.tsx +++ b/src/components/right/EditTopic.tsx @@ -8,7 +8,9 @@ import type { ApiChat, ApiSticker, ApiTopic } from '../../api/types'; import type { TabState } from '../../global/types'; import { DEFAULT_TOPIC_ICON_STICKER_ID, GENERAL_TOPIC_ID } from '../../config'; -import { selectChat, selectIsCurrentUserPremium, selectTabState } from '../../global/selectors'; +import { + selectChat, selectIsCurrentUserPremium, selectTabState, selectTopic, +} from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { REM } from '../common/helpers/mediaDimensions'; @@ -185,7 +187,8 @@ export default memo(withGlobal( (global): StateProps => { const { editTopicPanel } = selectTabState(global); const chat = editTopicPanel?.chatId ? selectChat(global, editTopicPanel.chatId) : undefined; - const topic = editTopicPanel?.topicId ? chat?.topics?.[editTopicPanel?.topicId] : undefined; + const topic = editTopicPanel?.chatId && editTopicPanel?.topicId + ? selectTopic(global, editTopicPanel.chatId, editTopicPanel.topicId) : undefined; return { chat, diff --git a/src/components/right/GifSearch.tsx b/src/components/right/GifSearch.tsx index 9d16f557d..2320749d6 100644 --- a/src/components/right/GifSearch.tsx +++ b/src/components/right/GifSearch.tsx @@ -14,6 +14,7 @@ import { selectCurrentMessageList, selectIsChatWithBot, selectIsChatWithSelf, selectThreadInfo, + selectTopic, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; @@ -174,8 +175,9 @@ export default memo(withGlobal( const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId); const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); + const topic = chatId && threadId ? selectTopic(global, chatId, threadId) : undefined; const canPostInChat = Boolean(chat) && Boolean(threadId) - && getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo); + && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo); return { query, diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 8489fc8fa..69be19608 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -18,6 +18,7 @@ import { selectCurrentStickerSearch, selectIsChatWithSelf, selectTabState, + selectTopic, selectUser, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; @@ -582,7 +583,7 @@ export default withGlobal( const user = isProfile && chatId && isUserId(chatId) ? selectUser(global, chatId) : undefined; const isChannel = chat && isChatChannel(chat); const isInsideTopic = chat?.isForum && Boolean(threadId && threadId !== MAIN_THREAD_ID); - const topic = isInsideTopic ? chat.topics?.[threadId!] : undefined; + const topic = isInsideTopic ? selectTopic(global, chatId!, threadId!) : undefined; const canEditTopic = isInsideTopic && topic && getCanManageTopic(chat, topic); const isBot = user && isUserBot(user); const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 944482642..93c75dfa3 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -112,6 +112,9 @@ import { selectTabState, selectThread, selectThreadInfo, + selectTopic, + selectTopics, + selectTopicsInfo, selectUser, selectUserByPhoneNumber, } from '../../selectors'; @@ -1169,7 +1172,9 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType = const chat = selectChat(global, chatId); if (!chat) return; - const lastTopicMessageId = chat.topics?.[topicId]?.lastMessageId; + const topic = selectTopic(global, chatId, topicId); + + const lastTopicMessageId = topic?.lastMessageId; if (!lastTopicMessageId) return; void callApi('markMessageListRead', { @@ -2074,13 +2079,15 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise = const chat = selectChat(global, chatId); if (!chat) return; - if (!force && chat.listedTopicIds && chat.listedTopicIds.length === chat.topicsCount) { + const topicsInfo = selectTopicsInfo(global, chatId); + + if (!force && topicsInfo?.listedTopicIds && topicsInfo.listedTopicIds.length === topicsInfo.totalCount) { return; } - const offsetTopic = !force && chat.listedTopicIds ? chat.listedTopicIds.reduce((acc, el) => { - const topic = chat.topics?.[el]; - const accTopic = chat.topics?.[acc]; + const offsetTopic = !force ? topicsInfo?.listedTopicIds?.reduce((acc, el) => { + const topic = selectTopic(global, chatId, el); + const accTopic = selectTopic(global, chatId, acc); if (!topic) return acc; if (!accTopic || topic.lastMessageId < accTopic.lastMessageId) { return el; @@ -2089,7 +2096,7 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise = }) : undefined; const { id: offsetTopicId, date: offsetDate, lastMessageId: offsetId } = (offsetTopic - && chat.topics?.[offsetTopic]) || {}; + && selectTopic(global, chatId, offsetTopic)) || {}; const result = await callApi('fetchTopics', { chat, offsetTopicId, offsetId, offsetDate, limit: offsetTopicId ? TOPICS_SLICE : TOPICS_SLICE_SECOND_LOAD, }); @@ -2231,7 +2238,7 @@ addActionHandler('editTopic', async (global, actions, payload): Promise => chatId, topicId, tabId = getCurrentTabId(), ...rest } = payload; const chat = selectChat(global, chatId); - const topic = chat?.topics?.[topicId]; + const topic = selectTopic(global, chatId, topicId); if (!chat || !topic) return; if (selectTabState(global, tabId).editTopicPanel) { @@ -2262,9 +2269,10 @@ addActionHandler('toggleTopicPinned', (global, actions, payload): ActionReturnTy const { topicsPinnedLimit } = global.appConfig || {}; const chat = selectChat(global, chatId); - if (!chat || !chat.topics || !topicsPinnedLimit) return; + const topics = selectTopics(global, chatId); + if (!chat || !topics || !topicsPinnedLimit) return; - if (isPinned && Object.values(chat.topics).filter((topic) => topic.isPinned).length >= topicsPinnedLimit) { + if (isPinned && Object.values(topics).filter((topic) => topic.isPinned).length >= topicsPinnedLimit) { actions.showNotification({ message: langProvider.oldTranslate('LimitReachedPinnedTopics', topicsPinnedLimit, 'i'), tabId, diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 94537592b..62ff7145e 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -127,6 +127,7 @@ import { selectSponsoredMessage, selectTabState, selectThreadIdFromMessage, + selectTopic, selectTranslationLanguage, selectUser, selectUserFullInfo, @@ -907,8 +908,8 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn return global; } - if (chat.isForum && chat.topics?.[threadId]) { - const topic = chat.topics[threadId]; + const topic = selectTopic(global, chatId, threadId); + if (chat.isForum && topic) { global = updateThreadInfo(global, chatId, threadId, { lastReadInboxMessageId: maxId, }); diff --git a/src/global/actions/api/sync.ts b/src/global/actions/api/sync.ts index fafe18897..e279fa729 100644 --- a/src/global/actions/api/sync.ts +++ b/src/global/actions/api/sync.ts @@ -36,6 +36,7 @@ import { selectEditingId, selectTabState, selectThreadInfo, + selectTopics, } from '../../selectors'; const RELEASE_STATUS_TIMEOUT = 15000; // 15 sec; @@ -160,10 +161,10 @@ async function loadAndReplaceMessages(global: T, actions: const localMessages = currentChatId === SERVICE_NOTIFICATIONS_USER_ID ? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message) : []; - const topicLastMessages = currentChat.isForum && currentChat.topics - ? Object.values(currentChat.topics) - .map(({ lastMessageId }) => currentChatMessages[lastMessageId]) - .filter(Boolean) + const topics = selectTopics(global, currentChatId); + const topicLastMessages = topics ? Object.values(topics) + .map(({ lastMessageId }) => currentChatMessages[lastMessageId]) + .filter(Boolean) : []; const resultMessageIds = result.messages.map(({ id }) => id); diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index dd1faa4ee..e0dd02cda 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -17,6 +17,7 @@ import { leaveChat, removeUnreadMentions, replacePeerPhotos, + replacePinnedTopicIds, replaceThreadParam, updateChat, updateChatFullInfo, @@ -482,9 +483,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { const chat = global.chats.byId[chatId]; if (!chat) return undefined; - global = updateChat(global, chatId, { - orderedPinnedTopicIds: order, - }); + global = replacePinnedTopicIds(global, chatId, order); setGlobal(global); return undefined; diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index c2d0744fd..793266c02 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -78,6 +78,7 @@ import { selectThreadByMessage, selectThreadIdFromMessage, selectThreadInfo, + selectTopic, selectTopicFromMessage, selectViewportIds, } from '../../selectors'; @@ -1061,7 +1062,7 @@ export function deleteMessages( isDeleting: true, }); - if (chat.topics?.[id]) { + if (selectTopic(global, chatId, id)) { global = deleteTopic(global, chatId, id); } @@ -1091,7 +1092,12 @@ export function deleteMessages( if (!threadInfo?.lastMessageId || !idsSet.has(threadInfo.lastMessageId)) return; const newLastMessage = findLastMessage(global, chatId, threadId); - if (!newLastMessage) return; + if (!newLastMessage) { + if (chat.isForum && threadId !== MAIN_THREAD_ID) { + actions.loadTopicById({ chatId, topicId: Number(threadId) }); + } + return; + } if (threadId === MAIN_THREAD_ID) { global = updateChatLastMessage(global, chatId, newLastMessage, true); diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 220f99a87..9138927f8 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -33,6 +33,7 @@ import { selectIsTrustedBot, selectSender, selectTabState, + selectTopic, } from '../../selectors'; import { getIsMobile, getIsTablet } from '../../../hooks/useAppLayout'; @@ -773,8 +774,9 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType const currentChat = selectChat(global, chatId); if (currentChat) { const title = getChatTitle(langProvider.oldTranslate, currentChat, chatId === currentUserId); - if (currentChat.isForum && currentChat.topics?.[threadId]) { - setPageTitle(`${title} › ${currentChat.topics[threadId].title}`); + const topic = selectTopic(global, chatId, threadId); + if (currentChat.isForum && topic) { + setPageTitle(`${title} › ${topic.title}`); return; } diff --git a/src/global/cache.ts b/src/global/cache.ts index dfc26b25a..4c18eadb3 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -36,10 +36,10 @@ import { addActionHandler, getGlobal } from './index'; import { INITIAL_GLOBAL_STATE, INITIAL_PERFORMANCE_STATE_MID, INITIAL_PERFORMANCE_STATE_MIN } from './initialState'; import { clearGlobalForLockScreen } from './reducers'; import { - selectChat, selectChatLastMessageId, selectChatMessages, selectCurrentMessageList, + selectTopics, selectViewportIds, selectVisibleUsers, } from './selectors'; @@ -248,6 +248,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.users.commonChatsById) { cached.users.commonChatsById = initialState.users.commonChatsById; } + if (!cached.chats.topicsInfoById) { + cached.chats.topicsInfoById = initialState.chats.topicsInfoById; + } } function updateCache(force?: boolean) { @@ -446,6 +449,7 @@ function reduceChats(global: T): GlobalState['chats'] { all: pickTruthy(global.chats.lastMessageIds.all || {}, idsToSave), saved: global.chats.lastMessageIds.saved, }, + topicsInfoById: pickTruthy(global.chats.topicsInfoById, currentChatIds), }; } @@ -486,7 +490,6 @@ function reduceMessages(global: T): GlobalState['messages return; } - const chat = selectChat(global, chatId); const chatLastMessageId = selectChatLastMessageId(global, chatId); const openedThreadIds = Array.from(openedChatThreadIds[chatId] || []); @@ -498,8 +501,9 @@ function reduceMessages(global: T): GlobalState['messages const threadsToSave = pickTruthy(current.threadsById, [MAIN_THREAD_ID, ...threadIds]); const viewportIdsToSave = unique(Object.values(threadsToSave).flatMap((thread) => thread.lastViewportIds || [])); - const topicLastMessageIds = chat?.topics ? Object.values(chat.topics).map(({ lastMessageId }) => lastMessageId) - : []; + const topics = selectTopics(global, chatId); + const topicLastMessageIds = topics && forumPanelChatIds.includes(chatId) + ? Object.values(topics).map(({ lastMessageId }) => lastMessageId) : []; const savedLastMessageIds = chatId === currentUserId && global.chats.lastMessageIds.saved ? Object.values(global.chats.lastMessageIds.saved) : []; const lastMessageIdsToSave = [chatLastMessageId].concat(topicLastMessageIds).concat(savedLastMessageIds) diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 7805a710c..2036d8451 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -145,15 +145,14 @@ export function isUserRightBanned(chat: ApiChat, key: keyof ApiChatBannedRights, } export function getCanPostInChat( - chat: ApiChat, threadId: ThreadId, isMessageThread?: boolean, chatFullInfo?: ApiChatFullInfo, + chat: ApiChat, topic?: ApiTopic, isMessageThread?: boolean, chatFullInfo?: ApiChatFullInfo, ) { - if (threadId !== MAIN_THREAD_ID) { + if (topic) { if (chat.isForum) { if (chat.isNotJoined) { return false; } - const topic = chat.topics?.[threadId]; if (topic?.isClosed && !topic.isOwner && !getHasAdminRight(chat, 'manageTopics')) { return false; } @@ -264,18 +263,22 @@ export function getMessageSendingRestrictionReason( } export function getForumComposerPlaceholder( - lang: LangFn, chat?: ApiChat, threadId: ThreadId = MAIN_THREAD_ID, isReplying?: boolean, + lang: LangFn, + chat?: ApiChat, + threadId: ThreadId = MAIN_THREAD_ID, + topics?: Record, + isReplying?: boolean, ) { if (!chat?.isForum) { return undefined; } if (threadId === MAIN_THREAD_ID) { - if (isReplying || (chat.topics && !chat.topics[GENERAL_TOPIC_ID]?.isClosed)) return undefined; + if (isReplying || (topics && !topics[GENERAL_TOPIC_ID]?.isClosed)) return undefined; return lang('lng_forum_replies_only'); } - const topic = chat.topics?.[threadId]; + const topic = topics?.[Number(threadId)]; if (!topic) { return undefined; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index ced1595f1..40f1a0878 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -110,6 +110,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { byId: {}, fullInfoById: {}, similarChannelsById: {}, + topicsInfoById: {}, loadingParameters: { active: {}, archived: {}, diff --git a/src/global/reducers/chats.ts b/src/global/reducers/chats.ts index afbbfd0f8..d2a5e7c43 100644 --- a/src/global/reducers/chats.ts +++ b/src/global/reducers/chats.ts @@ -1,5 +1,5 @@ import type { - ApiChat, ApiChatFullInfo, ApiChatMember, ApiTopic, + ApiChat, ApiChatFullInfo, ApiChatMember, } from '../../api/types'; import type { ChatListType, GlobalState } from '../types'; @@ -8,8 +8,7 @@ import { areDeepEqual } from '../../util/areDeepEqual'; import { areSortedArraysEqual, buildCollectionByKey, omit, pick, unique, } from '../../util/iteratees'; -import { selectChat, selectChatFullInfo } from '../selectors'; -import { updateThread, updateThreadInfo } from './messages'; +import { selectChatFullInfo } from '../selectors'; const DEFAULT_CHAT_LISTS: ChatListType[] = ['active', 'archived']; @@ -423,95 +422,6 @@ export function addChatMembers(global: T, chat: ApiChat, }); } -export function updateListedTopicIds( - global: T, chatId: string, topicIds: number[], -): T { - return updateChat(global, chatId, { - listedTopicIds: unique([ - ...(global.chats.byId[chatId]?.listedTopicIds || []), - ...topicIds, - ]), - }); -} - -export function updateTopics( - global: T, chatId: string, topicsCount: number, topics: ApiTopic[], -): T { - const chat = selectChat(global, chatId); - - const newTopics = buildCollectionByKey(topics, 'id'); - - global = updateChat(global, chatId, { - topics: { - ...chat?.topics, - ...newTopics, - }, - topicsCount, - }); - - topics.forEach((topic) => { - global = updateThread(global, chatId, topic.id, { - firstMessageId: topic.id, - }); - - global = updateThreadInfo(global, chatId, topic.id, { - lastMessageId: topic.lastMessageId, - threadId: topic.id, - chatId, - }); - }); - - return global; -} - -export function updateTopic( - global: T, chatId: string, topicId: number, update: Partial, -): T { - const chat = selectChat(global, chatId); - - if (!chat) return global; - - const topic = chat?.topics?.[topicId]; - - const updatedTopic = { - ...topic, - ...update, - } as ApiTopic; - - if (!updatedTopic.id) return global; - - global = updateChat(global, chatId, { - topics: { - ...(chat.topics || {}), - [topicId]: updatedTopic, - }, - }); - - global = updateThread(global, chatId, updatedTopic.id, { - firstMessageId: updatedTopic.id, - }); - - global = updateThreadInfo(global, chatId, updatedTopic.id, { - lastMessageId: updatedTopic.lastMessageId, - threadId: updatedTopic.id, - chatId, - }); - - return global; -} - -export function deleteTopic( - global: T, chatId: string, topicId: number, -) { - const chat = selectChat(global, chatId); - const topics = chat?.topics || {}; - global = updateChat(global, chatId, { - topics: omit(topics, [topicId]), - }); - - return global; -} - export function addSimilarChannels( global: T, chatId: string, diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index c48c7d4ff..3ed9c26fe 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -14,3 +14,4 @@ export * from './stories'; export * from './translations'; export * from './peers'; export * from './password'; +export * from './topics'; diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index 2ca529b1b..a99f7b1a1 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -21,7 +21,6 @@ import { mergeIdRanges, orderHistoryIds, orderPinnedIds, } from '../helpers'; import { - selectChat, selectChatMessage, selectChatMessages, selectChatScheduledMessages, @@ -43,10 +42,7 @@ import { removeIdFromSearchResults } from './middleSearch'; import { updateTabState } from './tabs'; import { clearMessageTranslation } from './translations'; -type MessageStoreSections = { - byId: Record; - threadsById: Record; -}; +type MessageStoreSections = GlobalState['messages']['byChatId'][string]; export function updateCurrentMessageList( global: T, @@ -128,7 +124,7 @@ export function updateThread( }); } -function updateMessageStore( +export function updateMessageStore( global: T, chatId: string, update: Partial, ): T { const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} }; @@ -823,32 +819,6 @@ export function updateThreadUnreadFromForwardedMessage( return global; } -export function updateTopicLastMessageId( - global: T, chatId: string, threadId: ThreadId, lastMessageId: number, -) { - const chat = selectChat(global, chatId); - if (!chat?.topics?.[threadId]) return global; - return { - ...global, - chats: { - ...global.chats, - byId: { - ...global.chats.byId, - [chatId]: { - ...chat, - topics: { - ...chat.topics, - [threadId]: { - ...chat.topics[threadId], - lastMessageId, - }, - }, - }, - }, - }, - }; -} - export function addActiveMediaDownload( global: T, mediaHash: string, diff --git a/src/global/reducers/topics.ts b/src/global/reducers/topics.ts new file mode 100644 index 000000000..21f8ede40 --- /dev/null +++ b/src/global/reducers/topics.ts @@ -0,0 +1,138 @@ +import type { ApiTopic } from '../../api/types'; +import type { GlobalState, TopicsInfo } from '../types'; + +import { buildCollectionByKey, omit, unique } from '../../util/iteratees'; +import { + selectChat, selectTopic, selectTopics, + selectTopicsInfo, +} from '../selectors'; +import { updateThread, updateThreadInfo } from './messages'; + +function updateTopicsStore( + global: T, chatId: string, update: Partial, +) { + const info = global.chats.topicsInfoById[chatId] || {}; + + global = { + ...global, + chats: { + ...global.chats, + topicsInfoById: { + ...global.chats.topicsInfoById, + [chatId]: { + ...info, + ...update, + }, + }, + }, + }; + + return global; +} + +export function updateListedTopicIds( + global: T, chatId: string, topicIds: number[], +): T { + const listedIds = selectTopicsInfo(global, chatId)?.listedTopicIds || []; + return updateTopicsStore(global, chatId, { + listedTopicIds: unique([ + ...listedIds, + ...topicIds, + ]), + }); +} + +export function updateTopics( + global: T, chatId: string, topicsCount: number, topics: ApiTopic[], +): T { + const oldTopics = selectTopics(global, chatId); + const newTopics = buildCollectionByKey(topics, 'id'); + + global = updateTopicsStore(global, chatId, { + topicsById: { + ...oldTopics, + ...newTopics, + }, + totalCount: topicsCount, + }); + + topics.forEach((topic) => { + global = updateThread(global, chatId, topic.id, { + firstMessageId: topic.id, + }); + + global = updateThreadInfo(global, chatId, topic.id, { + lastMessageId: topic.lastMessageId, + threadId: topic.id, + chatId, + }); + }); + + return global; +} + +export function updateTopic( + global: T, chatId: string, topicId: number, update: Partial, +): T { + const chat = selectChat(global, chatId); + + if (!chat) return global; + + const topic = selectTopic(global, chatId, topicId); + const oldTopics = selectTopics(global, chatId); + + const updatedTopic = { + ...topic, + ...update, + } as ApiTopic; + + if (!updatedTopic.id) return global; + + global = updateTopicsStore(global, chatId, { + topicsById: { + ...oldTopics, + [topicId]: updatedTopic, + }, + }); + + global = updateThread(global, chatId, updatedTopic.id, { + firstMessageId: updatedTopic.id, + }); + + global = updateThreadInfo(global, chatId, updatedTopic.id, { + lastMessageId: updatedTopic.lastMessageId, + threadId: updatedTopic.id, + chatId, + }); + + return global; +} + +export function deleteTopic( + global: T, chatId: string, topicId: number, +) { + const topics = selectTopics(global, chatId); + if (!topics) return global; + + global = updateTopicsStore(global, chatId, { + topicsById: omit(topics, [topicId]), + }); + + return global; +} + +export function updateTopicLastMessageId( + global: T, chatId: string, threadId: number, lastMessageId: number, +) { + return updateTopic(global, chatId, threadId, { + lastMessageId, + }); +} + +export function replacePinnedTopicIds( + global: T, chatId: string, pinnedTopicIds: number[], +) { + return updateTopicsStore(global, chatId, { + orderedPinnedTopicIds: pinnedTopicIds, + }); +} diff --git a/src/global/selectors/index.ts b/src/global/selectors/index.ts index 469d88b7d..3281775a8 100644 --- a/src/global/selectors/index.ts +++ b/src/global/selectors/index.ts @@ -12,3 +12,4 @@ export * from './statistics'; export * from './stories'; export * from './tabs'; export * from './peers'; +export * from './topics'; diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 5485efb51..e9897af4d 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -70,6 +70,7 @@ import { selectPeer } from './peers'; import { selectPeerStory } from './stories'; import { selectIsStickerFavorite } from './symbols'; import { selectTabState } from './tabs'; +import { selectTopic } from './topics'; import { selectBot, selectIsCurrentUserPremium, selectUser, selectUserStatus, } from './users'; @@ -483,17 +484,21 @@ export function selectForwardedSender( return undefined; } +export function selectTopicFromMessage(global: T, message: ApiMessage) { + const { chatId } = message; + const chat = selectChat(global, chatId); + if (!chat?.isForum) return undefined; + + const threadId = selectThreadIdFromMessage(global, message); + return selectTopic(global, chatId, threadId); +} + const MAX_MESSAGES_TO_DELETE_OWNER_TOPIC = 10; export function selectCanDeleteOwnerTopic(global: T, chatId: string, topicId: number) { - const chat = selectChat(global, chatId); - if (!chat) { - return false; - } - - if (chat.topics?.[topicId] && !chat.topics?.[topicId].isOwner) return false; - - const thread = global.messages.byChatId[chatId]?.threadsById[topicId]; + const topic = selectTopic(global, chatId, topicId); + if (topic && !topic.isOwner) return false; + const thread = selectThread(global, chatId, topicId); if (!thread) return false; const { listedIds } = thread; @@ -576,15 +581,6 @@ export function selectThreadIdFromMessage(global: T, mess return replyToTopId || replyToMsgId || GENERAL_TOPIC_ID; } -export function selectTopicFromMessage(global: T, message: ApiMessage) { - const { chatId } = message; - const chat = selectChat(global, chatId); - if (!chat?.isForum) return undefined; - - const threadId = selectThreadIdFromMessage(global, message); - return chat.topics?.[threadId]; -} - export function selectCanReplyToMessage(global: T, message: ApiMessage, threadId: ThreadId) { const chat = selectChat(global, message.chatId); if (!chat || chat.isRestricted || chat.isForbidden) return false; @@ -597,7 +593,8 @@ export function selectCanReplyToMessage(global: T, messag const threadInfo = selectThreadInfo(global, message.chatId, threadId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); const chatFullInfo = selectChatFullInfo(global, chat.id); - const canPostInChat = getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo); + const topic = selectTopic(global, chat.id, threadId); + const canPostInChat = getCanPostInChat(chat, topic, isMessageThread, chatFullInfo); if (!canPostInChat) return false; const messageTopic = selectTopicFromMessage(global, message); diff --git a/src/global/selectors/topics.ts b/src/global/selectors/topics.ts new file mode 100644 index 000000000..0b8ec77a2 --- /dev/null +++ b/src/global/selectors/topics.ts @@ -0,0 +1,14 @@ +import type { ThreadId } from '../../types'; +import type { GlobalState, TopicsInfo } from '../types'; + +export function selectTopicsInfo(global: T, chatId: string): TopicsInfo | undefined { + return global.chats.topicsInfoById[chatId]; +} + +export function selectTopics(global: T, chatId: string) { + return selectTopicsInfo(global, chatId)?.topicsById; +} + +export function selectTopic(global: T, chatId: string, threadId: ThreadId) { + return selectTopicsInfo(global, chatId)?.topicsById?.[threadId]; +} diff --git a/src/global/types.ts b/src/global/types.ts index 991c2d761..b9fc53b90 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -74,6 +74,7 @@ import type { ApiThemeParameters, ApiThreadInfo, ApiTimezone, + ApiTopic, ApiTranscription, ApiTypeStoryView, ApiTypingStatus, @@ -213,6 +214,13 @@ export interface ServiceNotification { isDeleted?: boolean; } +export interface TopicsInfo { + totalCount: number; + topicsById: Record; + listedTopicIds?: number[]; + orderedPinnedTopicIds?: number[]; +} + export type ApiLimitType = | 'uploadMaxFileparts' | 'stickersFaved' @@ -968,6 +976,7 @@ export type GlobalState = { all?: Record; saved?: Record; }; + topicsInfoById: Record; loadingParameters: Record; foldersById: Record; usersById: Record; @@ -215,6 +217,7 @@ function updateFolderManager(global: GlobalState) { const areChatsChanged = global.chats.byId !== prevGlobal.chatsById; const areSavedLastMessageIdsChanged = global.chats.lastMessageIds.saved !== prevGlobal.lastSavedMessageIds; const areAllLastMessageIdsChanged = global.chats.lastMessageIds.all !== prevGlobal.lastAllMessageIds; + const areTopicsChanged = global.chats.topicsInfoById !== prevGlobal.topicsInfoById; const areUsersChanged = global.users.byId !== prevGlobal.usersById; const areNotifySettingsChanged = selectNotifySettings(global) !== prevGlobal.notifySettings; const areNotifyExceptionsChanged = selectNotifyExceptions(global) !== prevGlobal.notifyExceptions; @@ -229,7 +232,7 @@ function updateFolderManager(global: GlobalState) { if (!( isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged || areFoldersChanged - || areChatsChanged || areUsersChanged || areNotifySettingsChanged || areNotifyExceptionsChanged + || areChatsChanged || areUsersChanged || areTopicsChanged || areNotifySettingsChanged || areNotifyExceptionsChanged || areSavedLastMessageIdsChanged || areAllLastMessageIdsChanged ) ) { @@ -515,8 +518,9 @@ function buildChatSummary( const { id, type, isRestricted, isNotJoined, migratedTo, folderId, unreadCount: chatUnreadCount, unreadMentionsCount: chatUnreadMentionsCount, hasUnreadMark, - isForum, topics, + isForum, } = chat; + const topics = selectTopics(global, chat.id); const { unreadCount, unreadMentionsCount } = isForum ? Object.values(topics || {}).reduce((acc, topic) => { @@ -805,6 +809,7 @@ function buildInitials() { foldersById: {}, chatsById: {}, usersById: {}, + topicsInfoById: {}, notifySettings: {} as NotifySettings, notifyExceptions: {}, },