Forum: Fix empty topic preview after reload (#4981)

This commit is contained in:
zubiden 2024-09-19 20:43:58 +02:00 committed by Alexander Zinchuk
parent 73d9fe9756
commit ba762fd0ad
40 changed files with 378 additions and 242 deletions

View File

@ -1,4 +1,3 @@
import type { ThreadId } from '../../types';
import type { ApiBotCommand } from './bots'; import type { ApiBotCommand } from './bots';
import type { import type {
ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet, ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet,
@ -48,10 +47,6 @@ export interface ApiChat {
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatus;
isForum?: boolean; isForum?: boolean;
isForumAsMessages?: true; isForumAsMessages?: true;
topics?: Record<ThreadId, ApiTopic>;
listedTopicIds?: number[];
topicsCount?: number;
orderedPinnedTopicIds?: number[];
boostLevel?: number; boostLevel?: number;
// Calls // Calls

View File

@ -8,7 +8,7 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat } from '../../api/types'; import type { ApiChat, ApiTopic } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { getOrderedTopics } from '../../global/helpers'; import { getOrderedTopics } from '../../global/helpers';
@ -26,6 +26,7 @@ import styles from './ChatForumLastMessage.module.scss';
type OwnProps = { type OwnProps = {
chat: ApiChat; chat: ApiChat;
topics?: Record<number, ApiTopic>;
renderLastMessage: () => React.ReactNode; renderLastMessage: () => React.ReactNode;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
}; };
@ -35,6 +36,7 @@ const MAX_TOPICS = 3;
const ChatForumLastMessage: FC<OwnProps> = ({ const ChatForumLastMessage: FC<OwnProps> = ({
chat, chat,
topics,
renderLastMessage, renderLastMessage,
observeIntersection, observeIntersection,
}) => { }) => {
@ -48,12 +50,12 @@ const ChatForumLastMessage: FC<OwnProps> = ({
const lang = useOldLang(); const lang = useOldLang();
const [lastActiveTopic, ...otherTopics] = useMemo(() => { const [lastActiveTopic, ...otherTopics] = useMemo(() => {
if (!chat.topics) { if (!topics) {
return []; return [];
} }
return getOrderedTopics(Object.values(chat.topics), undefined, true).slice(0, MAX_TOPICS); return getOrderedTopics(Object.values(topics), undefined, true).slice(0, MAX_TOPICS);
}, [chat.topics]); }, [topics]);
const [isReversedCorner, setIsReversedCorner] = useState(false); const [isReversedCorner, setIsReversedCorner] = useState(false);
const [overwrittenWidth, setOverwrittenWidth] = useState<number | undefined>(undefined); const [overwrittenWidth, setOverwrittenWidth] = useState<number | undefined>(undefined);

View File

@ -20,6 +20,7 @@ import {
selectChatOnlineCount, selectChatOnlineCount,
selectThreadInfo, selectThreadInfo,
selectThreadMessagesCount, selectThreadMessagesCount,
selectTopic,
selectUser, selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -267,7 +268,7 @@ export default memo(withGlobal<OwnProps>(
const threadInfo = threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const threadInfo = threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined; const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined;
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId)); 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 messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!);
const self = selectUser(global, global.currentUserId!); const self = selectUser(global, global.currentUserId!);

View File

@ -16,6 +16,7 @@ import {
selectPeerPhotos, selectPeerPhotos,
selectTabState, selectTabState,
selectThreadMessagesCount, selectThreadMessagesCount,
selectTopic,
selectUser, selectUser,
selectUserStatus, selectUserStatus,
} from '../../global/selectors'; } from '../../global/selectors';
@ -373,7 +374,7 @@ export default memo(withGlobal<OwnProps>(
const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer; const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer;
const isForum = chat?.isForum; const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; 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 emojiStatus = (user || chat)?.emojiStatus;
const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined;

View File

@ -4,7 +4,6 @@ import { getGlobal, withGlobal } from '../../global';
import type { ApiChatType } from '../../api/types'; import type { ApiChatType } from '../../api/types';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { API_CHAT_TYPES } from '../../config'; import { API_CHAT_TYPES } from '../../config';
import { import {
@ -80,7 +79,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
const user = usersById[id]; const user = usersById[id];
if (user && isDeletedUser(user)) return false; 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( const sorted = sortChatIds(

View File

@ -19,6 +19,7 @@ import {
selectShouldSchedule, selectShouldSchedule,
selectStickerSet, selectStickerSet,
selectThreadInfo, selectThreadInfo,
selectTopic,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { copyTextToClipboard } from '../../util/clipboard'; import { copyTextToClipboard } from '../../util/clipboard';
@ -263,8 +264,9 @@ export default memo(withGlobal<OwnProps>(
const sendOptions = chat ? getAllowedAttachmentOptions(chat, chatFullInfo) : undefined; const sendOptions = chat ? getAllowedAttachmentOptions(chat, chatFullInfo) : undefined;
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const topic = chatId && threadId ? selectTopic(global, chatId, threadId) : undefined;
const canSendStickers = Boolean( const canSendStickers = Boolean(
chat && threadId && getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo) chat && threadId && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo)
&& sendOptions?.canSendStickers, && sendOptions?.canSendStickers,
); );
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId); const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);

View File

@ -13,7 +13,7 @@ import {
getCanPostInChat, getGroupStatus, getUserStatus, isUserOnline, getCanPostInChat, getGroupStatus, getUserStatus, isUserOnline,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { isApiPeerChat } from '../../../global/helpers/peers'; 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 buildClassName from '../../../util/buildClassName';
import { REM } from '../helpers/mediaDimensions'; import { REM } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText'; import renderText from '../helpers/renderText';
@ -94,15 +94,15 @@ const ChatOrUserPicker: FC<OwnProps> = ({
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 selectTopics = useLastCallback((global: GlobalState) => { const selectTopicsById = useLastCallback((global: GlobalState) => {
if (!forumId) { if (!forumId) {
return undefined; return undefined;
} }
return selectChat(global, forumId)?.topics; return selectTopics(global, forumId);
}); });
const forumTopics = useSelector(selectTopics); const forumTopicsById = useSelector(selectTopicsById);
const [topicIds, topics] = useMemo(() => { const [topicIds, topics] = useMemo(() => {
const global = getGlobal(); const global = getGlobal();
@ -111,16 +111,16 @@ const ChatOrUserPicker: FC<OwnProps> = ({
const chat = chatsById[forumId!]; const chat = chatsById[forumId!];
if (!chat || !forumTopics) { if (!chat || !forumTopicsById) {
return [undefined, undefined]; return [undefined, undefined];
} }
const searchTitle = topicSearch.toLowerCase(); const searchTitle = topicSearch.toLowerCase();
const result = forumTopics const result = forumTopicsById
? Object.values(forumTopics).reduce((acc, topic) => { ? Object.values(forumTopicsById).reduce((acc, topic) => {
if ( if (
getCanPostInChat(chat, topic.id, undefined, chatFullInfoById[forumId!]) getCanPostInChat(chat, topic, undefined, chatFullInfoById[forumId!])
&& (!searchTitle || topic.title.toLowerCase().includes(searchTitle)) && (!searchTitle || topic.title.toLowerCase().includes(searchTitle))
) { ) {
acc[topic.id] = topic; acc[topic.id] = topic;
@ -128,10 +128,10 @@ const ChatOrUserPicker: FC<OwnProps> = ({
return acc; return acc;
}, {} as Record<number, ApiTopic>) }, {} as Record<number, ApiTopic>)
: forumTopics; : forumTopicsById;
return [Object.keys(result).map(Number), result]; return [Object.keys(result).map(Number), result];
}, [forumId, topicSearch, forumTopics]); }, [forumId, topicSearch, forumTopicsById]);
const handleHeaderBackClick = useLastCallback(() => { const handleHeaderBackClick = useLastCallback(() => {
setForumId(undefined); setForumId(undefined);
@ -153,7 +153,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
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 (!chat.topics) loadTopics({ chatId }); if (!forumTopicsById) loadTopics({ chatId });
setForumId(chatId); setForumId(chatId);
} else { } else {
onSelectChatOrUser(chatId); onSelectChatOrUser(chatId);
@ -171,7 +171,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
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 (!chat.topics) loadTopics({ chatId }); if (!forumTopicsById) loadTopics({ chatId });
setForumId(chatId); setForumId(chatId);
resetSearch(); resetSearch();
} else { } else {

View File

@ -43,6 +43,7 @@ import {
selectTabState, selectTabState,
selectThreadParam, selectThreadParam,
selectTopicFromMessage, selectTopicFromMessage,
selectTopicsInfo,
selectUser, selectUser,
selectUserStatus, selectUserStatus,
} from '../../../global/selectors'; } from '../../../global/selectors';
@ -90,6 +91,8 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
listedTopicIds?: number[];
topics?: Record<number, ApiTopic>;
isMuted?: boolean; isMuted?: boolean;
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
@ -118,6 +121,8 @@ const Chat: FC<OwnProps & StateProps> = ({
orderDiff, orderDiff,
animationType, animationType,
isPinned, isPinned,
listedTopicIds,
topics,
observeIntersection, observeIntersection,
chat, chat,
isMuted, isMuted,
@ -190,6 +195,7 @@ const Chat: FC<OwnProps & StateProps> = ({
orderDiff, orderDiff,
isSavedDialog, isSavedDialog,
isPreview, isPreview,
topics,
}); });
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
@ -284,10 +290,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 && chat && chat.listedTopicIds === undefined) { if (isIntersecting && isForum && listedTopicIds === undefined) {
loadTopics({ chatId }); loadTopics({ chatId });
} }
}, [chat, chatId, isForum, isIntersecting]); }, [chatId, listedTopicIds, 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);
@ -343,7 +349,13 @@ const Chat: FC<OwnProps & StateProps> = ({
/> />
<div className="avatar-badge-wrapper"> <div className="avatar-badge-wrapper">
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} /> <div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} />
<ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={getIsForumPanelClosed} /> <ChatBadge
chat={chat}
isMuted={isMuted}
shouldShowOnlyMostImportant
forceHidden={getIsForumPanelClosed}
topics={topics}
/>
</div> </div>
{chat.isCallActive && chat.isCallNotEmpty && ( {chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={withInterfaceAnimations} /> <ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={withInterfaceAnimations} />
@ -376,6 +388,7 @@ const Chat: FC<OwnProps & StateProps> = ({
isPinned={isPinned} isPinned={isPinned}
isMuted={isMuted} isMuted={isMuted}
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}
topics={topics}
/> />
)} )}
</div> </div>
@ -460,6 +473,8 @@ export default memo(withGlobal<OwnProps>(
const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus'); const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
const topicsInfo = selectTopicsInfo(global, chatId);
return { return {
chat, chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
@ -484,6 +499,8 @@ export default memo(withGlobal<OwnProps>(
lastMessage, lastMessage,
lastMessageId, lastMessageId,
currentUserId: global.currentUserId!, currentUserId: global.currentUserId!,
listedTopicIds: topicsInfo?.listedTopicIds,
topics: topicsInfo?.topicsById,
}; };
}, },
)(Chat)); )(Chat));

View File

@ -24,10 +24,19 @@ type OwnProps = {
isSavedDialog?: boolean; isSavedDialog?: boolean;
shouldShowOnlyMostImportant?: boolean; shouldShowOnlyMostImportant?: boolean;
forceHidden?: boolean | Signal<boolean>; forceHidden?: boolean | Signal<boolean>;
topics?: Record<number, ApiTopic>;
}; };
const ChatBadge: FC<OwnProps> = ({ const ChatBadge: FC<OwnProps> = ({
topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden, isSavedDialog, topic,
topics,
chat,
isPinned,
isMuted,
shouldShowOnlyMostImportant,
wasTopicOpened,
forceHidden,
isSavedDialog,
}) => { }) => {
const { const {
unreadMentionsCount = 0, unreadReactionsCount = 0, unreadMentionsCount = 0, unreadReactionsCount = 0,
@ -36,8 +45,8 @@ const ChatBadge: FC<OwnProps> = ({
const isTopicUnopened = !isPinned && topic && !wasTopicOpened; const isTopicUnopened = !isPinned && topic && !wasTopicOpened;
const isForum = chat.isForum && !topic; const isForum = chat.isForum && !topic;
const topicsWithUnread = useMemo(() => ( const topicsWithUnread = useMemo(() => (
isForum && chat?.topics ? Object.values(chat.topics).filter(({ unreadCount }) => unreadCount) : undefined isForum && topics ? Object.values(topics).filter(({ unreadCount }) => unreadCount) : undefined
), [chat, isForum]); ), [topics, isForum]);
const unreadCount = useMemo(() => ( const unreadCount = useMemo(() => (
isForum isForum
@ -48,11 +57,11 @@ const ChatBadge: FC<OwnProps> = ({
), [chat, topic, topicsWithUnread, isForum, isMuted]); ), [chat, topic, topicsWithUnread, isForum, isMuted]);
const shouldBeMuted = useMemo(() => { const shouldBeMuted = useMemo(() => {
const hasUnmutedUnreadTopics = chat.topics const hasUnmutedUnreadTopics = topics
&& Object.values(chat.topics).some((acc) => !acc.isMuted && acc.unreadCount); && Object.values(topics).some((acc) => !acc.isMuted && acc.unreadCount);
return isMuted || (chat.topics && !hasUnmutedUnreadTopics); return isMuted || (topics && !hasUnmutedUnreadTopics);
}, [chat, isMuted]); }, [topics, isMuted]);
const hasUnreadMark = topic ? false : chat.hasUnreadMark; const hasUnreadMark = topic ? false : chat.hasUnreadMark;

View File

@ -6,6 +6,7 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat } from '../../../api/types'; import type { ApiChat } from '../../../api/types';
import type { TopicsInfo } from '../../../global/types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { import {
@ -14,7 +15,12 @@ import {
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom'; import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import { getOrderedTopics } from '../../../global/helpers'; import { getOrderedTopics } from '../../../global/helpers';
import { import {
selectCanAnimateInterface, selectChat, selectCurrentMessageList, selectIsForumPanelOpen, selectTabState, selectCanAnimateInterface,
selectChat,
selectCurrentMessageList,
selectIsForumPanelOpen,
selectTabState,
selectTopicsInfo,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
@ -52,6 +58,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
topicsInfo?: TopicsInfo;
currentTopicId?: number; currentTopicId?: number;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
}; };
@ -63,6 +70,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
currentTopicId, currentTopicId,
isOpen, isOpen,
isHidden, isHidden,
topicsInfo,
onTopicSearch, onTopicSearch,
onCloseAnimationEnd, onCloseAnimationEnd,
onOpenAnimationStart, onOpenAnimationStart,
@ -80,12 +88,13 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const scrollTopHandlerRef = useRef<HTMLDivElement>(null); const scrollTopHandlerRef = useRef<HTMLDivElement>(null);
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const chatId = chat?.id;
useEffect(() => { useEffect(() => {
if (chat && !chat.topics) { if (chatId && !topicsInfo) {
loadTopics({ chatId: chat.id }); loadTopics({ chatId });
} }
}, [chat, loadTopics]); }, [topicsInfo, chatId]);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const lang = useOldLang(); const lang = useOldLang();
@ -115,17 +124,20 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
}); });
const orderedIds = useMemo(() => { const orderedIds = useMemo(() => {
return chat?.topics return topicsInfo
? getOrderedTopics(Object.values(chat.topics), chat.orderedPinnedTopicIds).map(({ id }) => id) ? getOrderedTopics(
Object.values(topicsInfo.topicsById),
topicsInfo.orderedPinnedTopicIds,
).map(({ id }) => id)
: []; : [];
}, [chat]); }, [topicsInfo]);
const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds, chat?.id); const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds, chat?.id);
const [viewportIds, getMore] = useInfiniteScroll(() => { const [viewportIds, getMore] = useInfiniteScroll(() => {
if (!chat) return; if (!chat) return;
loadTopics({ chatId: chat.id }); 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 shouldRenderRef = useRef(false);
const isVisible = isOpen && !isHidden; const isVisible = isOpen && !isHidden;
@ -191,7 +203,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
<Topic <Topic
key={id} key={id}
chatId={chat!.id} chatId={chat!.id}
topic={chat!.topics![id]} topic={topicsInfo!.topicsById[id]}
style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`} style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`}
isSelected={currentTopicId === id} isSelected={currentTopicId === id}
observeIntersection={observe} observeIntersection={observe}
@ -201,7 +213,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
)); ));
} }
const isLoading = chat?.topics === undefined; const isLoading = topicsInfo === undefined;
return ( return (
<div <div
@ -271,7 +283,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
)} )}
</InfiniteScroll> </InfiniteScroll>
{!isLoading && viewportIds?.length === 1 && viewportIds[0] === GENERAL_TOPIC_ID && ( {!isLoading && viewportIds?.length === 1 && viewportIds[0] === GENERAL_TOPIC_ID && (
<EmptyForum chatId={chat.id} /> <EmptyForum chatId={chatId!} />
)} )}
</div> </div>
); );
@ -285,11 +297,13 @@ export default memo(withGlobal<OwnProps>(
chatId: currentChatId, chatId: currentChatId,
threadId: currentThreadId, threadId: currentThreadId,
} = selectCurrentMessageList(global) || {}; } = selectCurrentMessageList(global) || {};
const topicsInfo = chatId ? selectTopicsInfo(global, chatId) : undefined;
return { return {
chat, chat,
currentTopicId: chatId === currentChatId ? Number(currentThreadId) : undefined, currentTopicId: chatId === currentChatId ? Number(currentThreadId) : undefined,
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
topicsInfo,
}; };
}, },
(global) => selectIsForumPanelOpen(global), (global) => selectIsForumPanelOpen(global),

View File

@ -22,6 +22,7 @@ import {
selectOutgoingStatus, selectOutgoingStatus,
selectThreadInfo, selectThreadInfo,
selectThreadParam, selectThreadParam,
selectTopics,
selectUser, selectUser,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -68,6 +69,7 @@ type StateProps = {
canScrollDown?: boolean; canScrollDown?: boolean;
wasTopicOpened?: boolean; wasTopicOpened?: boolean;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
topics?: Record<number, ApiTopic>;
}; };
const Topic: FC<OwnProps & StateProps> = ({ const Topic: FC<OwnProps & StateProps> = ({
@ -91,6 +93,7 @@ const Topic: FC<OwnProps & StateProps> = ({
typingStatus, typingStatus,
draft, draft,
wasTopicOpened, wasTopicOpened,
topics,
}) => { }) => {
const { const {
openThread, openThread,
@ -138,6 +141,7 @@ const Topic: FC<OwnProps & StateProps> = ({
observeIntersection, observeIntersection,
isTopic: true, isTopic: true,
typingStatus, typingStatus,
topics,
animationType, animationType,
withInterfaceAnimations, withInterfaceAnimations,
@ -208,6 +212,7 @@ const Topic: FC<OwnProps & StateProps> = ({
isMuted={isMuted} isMuted={isMuted}
topic={topic} topic={topic}
wasTopicOpened={wasTopicOpened} wasTopicOpened={wasTopicOpened}
topics={topics}
/> />
</div> </div>
</div> </div>
@ -253,6 +258,7 @@ export default memo(withGlobal<OwnProps>(
const draft = selectDraft(global, chatId, topic.id); const draft = selectDraft(global, chatId, topic.id);
const threadInfo = selectThreadInfo(global, chatId, topic.id); const threadInfo = selectThreadInfo(global, chatId, topic.id);
const wasTopicOpened = Boolean(threadInfo?.lastReadInboxMessageId); const wasTopicOpened = Boolean(threadInfo?.lastReadInboxMessageId);
const topics = selectTopics(global, chatId);
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {}; const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
@ -272,6 +278,7 @@ export default memo(withGlobal<OwnProps>(
}), }),
canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id, canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id,
wasTopicOpened, wasTopicOpened,
topics,
}; };
}, },
)(Topic)); )(Topic));

View File

@ -43,6 +43,7 @@ const ANIMATION_DURATION = 200;
export default function useChatListEntry({ export default function useChatListEntry({
chat, chat,
topics,
lastMessage, lastMessage,
chatId, chatId,
typingStatus, typingStatus,
@ -61,6 +62,7 @@ export default function useChatListEntry({
isPreview, isPreview,
}: { }: {
chat?: ApiChat; chat?: ApiChat;
topics?: Record<number, ApiTopic>;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
chatId: string; chatId: string;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
@ -192,6 +194,7 @@ export default function useChatListEntry({
chat={chat} chat={chat}
renderLastMessage={renderLastMessageOrTyping} renderLastMessage={renderLastMessageOrTyping}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
topics={topics}
/> />
); );
} }

View File

@ -4,7 +4,7 @@ import { withGlobal } from '../../../global';
import type { ApiTopic } from '../../../api/types'; import type { ApiTopic } from '../../../api/types';
import { selectChat } from '../../../global/selectors'; import { selectTopic } from '../../../global/selectors';
import { REM } from '../../common/helpers/mediaDimensions'; import { REM } from '../../common/helpers/mediaDimensions';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -60,8 +60,7 @@ const LeftSearchResultTopic: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, topicId }): StateProps => { (global, { chatId, topicId }): StateProps => {
const chat = selectChat(global, chatId); const topic = selectTopic(global, chatId, topicId);
const topic = chat?.topics?.[topicId];
return { return {
topic, topic,

View File

@ -4,7 +4,6 @@ import { getActions } from '../../lib/teact/teactn';
import { withGlobal } from '../../global'; import { withGlobal } from '../../global';
import type { TabState } from '../../global/types'; import type { TabState } from '../../global/types';
import { MAIN_THREAD_ID } from '../../api/types';
import { getCanPostInChat } from '../../global/helpers'; import { getCanPostInChat } from '../../global/helpers';
import { selectChat, selectChatFullInfo } from '../../global/selectors'; import { selectChat, selectChatFullInfo } from '../../global/selectors';
@ -94,7 +93,7 @@ export default memo(withGlobal<OwnProps>(
const { chatId } = openedGame || {}; const { chatId } = openedGame || {};
const chat = chatId && selectChat(global, chatId); const chat = chatId && selectChat(global, chatId);
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; 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 { return {
canPost, canPost,

View File

@ -35,6 +35,7 @@ import {
selectNotifyExceptions, selectNotifyExceptions,
selectNotifySettings, selectNotifySettings,
selectTabState, selectTabState,
selectTopic,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
@ -760,7 +761,7 @@ export default memo(withGlobal<OwnProps>(
&& !selectIsPremiumPurchaseBlocked(global), && !selectIsPremiumPurchaseBlocked(global),
); );
const topic = chat?.topics?.[threadId]; const topic = selectTopic(global, chatId, threadId);
const canCreateTopic = chat.isForum && ( const canCreateTopic = chat.isForum && (
chat.isCreator || !isUserRightBanned(chat, 'manageTopics') || getHasAdminRight(chat, 'manageTopics') chat.isCreator || !isUserRightBanned(chat, 'manageTopics') || getHasAdminRight(chat, 'manageTopics')
); );

View File

@ -54,6 +54,7 @@ import {
selectScrollOffset, selectScrollOffset,
selectTabState, selectTabState,
selectThreadInfo, selectThreadInfo,
selectTopic,
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
@ -772,7 +773,7 @@ export default memo(withGlobal<OwnProps>(
const chatBot = selectBot(global, chatId); 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 chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount; const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount;

View File

@ -4,7 +4,9 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo } from '../../api/types'; import type {
ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo, ApiTopic,
} from '../../api/types';
import type { import type {
ActiveEmojiInteraction, ActiveEmojiInteraction,
MessageListType, MessageListType,
@ -55,6 +57,8 @@ import {
selectTabState, selectTabState,
selectTheme, selectTheme,
selectThreadInfo, selectThreadInfo,
selectTopic,
selectTopics,
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -156,6 +160,7 @@ type StateProps = {
isSavedDialog?: boolean; isSavedDialog?: boolean;
canShowOpenChatButton?: boolean; canShowOpenChatButton?: boolean;
isContactRequirePremium?: boolean; isContactRequirePremium?: boolean;
topics?: Record<number, ApiTopic>;
}; };
function isImage(item: DataTransferItem) { function isImage(item: DataTransferItem) {
@ -217,6 +222,7 @@ function MiddleColumn({
isSavedDialog, isSavedDialog,
canShowOpenChatButton, canShowOpenChatButton,
isContactRequirePremium, isContactRequirePremium,
topics,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { const {
openChat, openChat,
@ -455,7 +461,7 @@ function MiddleColumn({
const messageSendingRestrictionReason = getMessageSendingRestrictionReason( const messageSendingRestrictionReason = getMessageSendingRestrictionReason(
lang, currentUserBannedRights, defaultBannedRights, lang, currentUserBannedRights, defaultBannedRights,
); );
const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(draftReplyInfo)); const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, topics, Boolean(draftReplyInfo));
const composerRestrictionMessage = messageSendingRestrictionReason const composerRestrictionMessage = messageSendingRestrictionReason
?? forumComposerPlaceholder ?? forumComposerPlaceholder
@ -775,7 +781,8 @@ export default memo(withGlobal<OwnProps>(
const threadInfo = selectThreadInfo(global, chatId, threadId); const threadInfo = selectThreadInfo(global, chatId, threadId);
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); 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 isBotNotStarted = selectIsChatBotNotStarted(global, chatId);
const isPinnedMessageList = messageListType === 'pinned'; const isPinnedMessageList = messageListType === 'pinned';
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID; const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
@ -794,11 +801,12 @@ export default memo(withGlobal<OwnProps>(
); );
const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo; const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo;
const shouldBlockSendInForum = chat?.isForum 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; : false;
const audioMessage = audioChatId && audioMessageId const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId) ? selectChatMessage(global, audioChatId, audioMessageId)
: undefined; : undefined;
const topics = selectTopics(global, chatId);
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID; const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
@ -854,6 +862,7 @@ export default memo(withGlobal<OwnProps>(
isSavedDialog, isSavedDialog,
canShowOpenChatButton, canShowOpenChatButton,
isContactRequirePremium, isContactRequirePremium,
topics,
}; };
}, },
)(MiddleColumn)); )(MiddleColumn));

View File

@ -54,6 +54,7 @@ import {
selectRequestedMessageTranslationLanguage, selectRequestedMessageTranslationLanguage,
selectStickerSet, selectStickerSet,
selectThreadInfo, selectThreadInfo,
selectTopic,
selectUserStatus, selectUserStatus,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
@ -730,10 +731,11 @@ export default memo(withGlobal<OwnProps>(
const threadInfo = threadId && selectThreadInfo(global, message.chatId, threadId); const threadInfo = threadId && selectThreadInfo(global, message.chatId, threadId);
const isMessageThread = Boolean(threadInfo && !threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); 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 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; && canSendText : false;
const isLocal = isMessageLocal(message); const isLocal = isMessageLocal(message);

View File

@ -31,7 +31,7 @@ import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessag
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { AudioOrigin } from '../../../types'; import { AudioOrigin } from '../../../types';
import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config'; import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config';
import { import {
areReactionsEmpty, areReactionsEmpty,
getIsDownloading, getIsDownloading,
@ -1766,8 +1766,7 @@ export default memo(withGlobal<OwnProps>(
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id); const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && isFirstInGroup; const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && isFirstInGroup;
const messageTopic = hasTopicChip ? (selectTopicFromMessage(global, message) || chat?.topics?.[GENERAL_TOPIC_ID]) const messageTopic = hasTopicChip ? selectTopicFromMessage(global, message) : undefined;
: undefined;
const chatTranslations = selectChatTranslations(global, chatId); const chatTranslations = selectChatTranslations(global, chatId);

View File

@ -8,7 +8,9 @@ import type { ApiChat, ApiSticker, ApiTopic } from '../../api/types';
import type { TabState } from '../../global/types'; import type { TabState } from '../../global/types';
import { DEFAULT_TOPIC_ICON_STICKER_ID, GENERAL_TOPIC_ID } from '../../config'; 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 buildClassName from '../../util/buildClassName';
import { REM } from '../common/helpers/mediaDimensions'; import { REM } from '../common/helpers/mediaDimensions';
@ -185,7 +187,8 @@ export default memo(withGlobal(
(global): StateProps => { (global): StateProps => {
const { editTopicPanel } = selectTabState(global); const { editTopicPanel } = selectTabState(global);
const chat = editTopicPanel?.chatId ? selectChat(global, editTopicPanel.chatId) : undefined; 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 { return {
chat, chat,

View File

@ -14,6 +14,7 @@ import {
selectCurrentMessageList, selectCurrentMessageList,
selectIsChatWithBot, selectIsChatWithBot,
selectIsChatWithSelf, selectThreadInfo, selectIsChatWithSelf, selectThreadInfo,
selectTopic,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
@ -174,8 +175,9 @@ export default memo(withGlobal(
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId); const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const topic = chatId && threadId ? selectTopic(global, chatId, threadId) : undefined;
const canPostInChat = Boolean(chat) && Boolean(threadId) const canPostInChat = Boolean(chat) && Boolean(threadId)
&& getCanPostInChat(chat, threadId, isMessageThread, chatFullInfo); && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo);
return { return {
query, query,

View File

@ -18,6 +18,7 @@ import {
selectCurrentStickerSearch, selectCurrentStickerSearch,
selectIsChatWithSelf, selectIsChatWithSelf,
selectTabState, selectTabState,
selectTopic,
selectUser, selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -582,7 +583,7 @@ export default withGlobal<OwnProps>(
const user = isProfile && chatId && isUserId(chatId) ? selectUser(global, chatId) : undefined; const user = isProfile && chatId && isUserId(chatId) ? selectUser(global, chatId) : undefined;
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const isInsideTopic = chat?.isForum && Boolean(threadId && threadId !== MAIN_THREAD_ID); 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 canEditTopic = isInsideTopic && topic && getCanManageTopic(chat, topic);
const isBot = user && isUserBot(user); const isBot = user && isUserBot(user);
const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined; const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined;

View File

@ -112,6 +112,9 @@ import {
selectTabState, selectTabState,
selectThread, selectThread,
selectThreadInfo, selectThreadInfo,
selectTopic,
selectTopics,
selectTopicsInfo,
selectUser, selectUser,
selectUserByPhoneNumber, selectUserByPhoneNumber,
} from '../../selectors'; } from '../../selectors';
@ -1169,7 +1172,9 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType =
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
const lastTopicMessageId = chat.topics?.[topicId]?.lastMessageId; const topic = selectTopic(global, chatId, topicId);
const lastTopicMessageId = topic?.lastMessageId;
if (!lastTopicMessageId) return; if (!lastTopicMessageId) return;
void callApi('markMessageListRead', { void callApi('markMessageListRead', {
@ -2074,13 +2079,15 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; 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; return;
} }
const offsetTopic = !force && chat.listedTopicIds ? chat.listedTopicIds.reduce((acc, el) => { const offsetTopic = !force ? topicsInfo?.listedTopicIds?.reduce((acc, el) => {
const topic = chat.topics?.[el]; const topic = selectTopic(global, chatId, el);
const accTopic = chat.topics?.[acc]; const accTopic = selectTopic(global, chatId, acc);
if (!topic) return acc; if (!topic) return acc;
if (!accTopic || topic.lastMessageId < accTopic.lastMessageId) { if (!accTopic || topic.lastMessageId < accTopic.lastMessageId) {
return el; return el;
@ -2089,7 +2096,7 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
}) : undefined; }) : undefined;
const { id: offsetTopicId, date: offsetDate, lastMessageId: offsetId } = (offsetTopic const { id: offsetTopicId, date: offsetDate, lastMessageId: offsetId } = (offsetTopic
&& chat.topics?.[offsetTopic]) || {}; && selectTopic(global, chatId, offsetTopic)) || {};
const result = await callApi('fetchTopics', { const result = await callApi('fetchTopics', {
chat, offsetTopicId, offsetId, offsetDate, limit: offsetTopicId ? TOPICS_SLICE : TOPICS_SLICE_SECOND_LOAD, chat, offsetTopicId, offsetId, offsetDate, limit: offsetTopicId ? TOPICS_SLICE : TOPICS_SLICE_SECOND_LOAD,
}); });
@ -2231,7 +2238,7 @@ addActionHandler('editTopic', async (global, actions, payload): Promise<void> =>
chatId, topicId, tabId = getCurrentTabId(), ...rest chatId, topicId, tabId = getCurrentTabId(), ...rest
} = payload; } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const topic = chat?.topics?.[topicId]; const topic = selectTopic(global, chatId, topicId);
if (!chat || !topic) return; if (!chat || !topic) return;
if (selectTabState(global, tabId).editTopicPanel) { if (selectTabState(global, tabId).editTopicPanel) {
@ -2262,9 +2269,10 @@ addActionHandler('toggleTopicPinned', (global, actions, payload): ActionReturnTy
const { topicsPinnedLimit } = global.appConfig || {}; const { topicsPinnedLimit } = global.appConfig || {};
const chat = selectChat(global, chatId); 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({ actions.showNotification({
message: langProvider.oldTranslate('LimitReachedPinnedTopics', topicsPinnedLimit, 'i'), message: langProvider.oldTranslate('LimitReachedPinnedTopics', topicsPinnedLimit, 'i'),
tabId, tabId,

View File

@ -127,6 +127,7 @@ import {
selectSponsoredMessage, selectSponsoredMessage,
selectTabState, selectTabState,
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectTopic,
selectTranslationLanguage, selectTranslationLanguage,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
@ -907,8 +908,8 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return global; return global;
} }
if (chat.isForum && chat.topics?.[threadId]) { const topic = selectTopic(global, chatId, threadId);
const topic = chat.topics[threadId]; if (chat.isForum && topic) {
global = updateThreadInfo(global, chatId, threadId, { global = updateThreadInfo(global, chatId, threadId, {
lastReadInboxMessageId: maxId, lastReadInboxMessageId: maxId,
}); });

View File

@ -36,6 +36,7 @@ import {
selectEditingId, selectEditingId,
selectTabState, selectTabState,
selectThreadInfo, selectThreadInfo,
selectTopics,
} from '../../selectors'; } from '../../selectors';
const RELEASE_STATUS_TIMEOUT = 15000; // 15 sec; const RELEASE_STATUS_TIMEOUT = 15000; // 15 sec;
@ -160,10 +161,10 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
const localMessages = currentChatId === SERVICE_NOTIFICATIONS_USER_ID const localMessages = currentChatId === SERVICE_NOTIFICATIONS_USER_ID
? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message) ? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message)
: []; : [];
const topicLastMessages = currentChat.isForum && currentChat.topics const topics = selectTopics(global, currentChatId);
? Object.values(currentChat.topics) const topicLastMessages = topics ? Object.values(topics)
.map(({ lastMessageId }) => currentChatMessages[lastMessageId]) .map(({ lastMessageId }) => currentChatMessages[lastMessageId])
.filter(Boolean) .filter(Boolean)
: []; : [];
const resultMessageIds = result.messages.map(({ id }) => id); const resultMessageIds = result.messages.map(({ id }) => id);

View File

@ -17,6 +17,7 @@ import {
leaveChat, leaveChat,
removeUnreadMentions, removeUnreadMentions,
replacePeerPhotos, replacePeerPhotos,
replacePinnedTopicIds,
replaceThreadParam, replaceThreadParam,
updateChat, updateChat,
updateChatFullInfo, updateChatFullInfo,
@ -482,9 +483,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const chat = global.chats.byId[chatId]; const chat = global.chats.byId[chatId];
if (!chat) return undefined; if (!chat) return undefined;
global = updateChat(global, chatId, { global = replacePinnedTopicIds(global, chatId, order);
orderedPinnedTopicIds: order,
});
setGlobal(global); setGlobal(global);
return undefined; return undefined;

View File

@ -78,6 +78,7 @@ import {
selectThreadByMessage, selectThreadByMessage,
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectThreadInfo, selectThreadInfo,
selectTopic,
selectTopicFromMessage, selectTopicFromMessage,
selectViewportIds, selectViewportIds,
} from '../../selectors'; } from '../../selectors';
@ -1061,7 +1062,7 @@ export function deleteMessages<T extends GlobalState>(
isDeleting: true, isDeleting: true,
}); });
if (chat.topics?.[id]) { if (selectTopic(global, chatId, id)) {
global = deleteTopic(global, chatId, id); global = deleteTopic(global, chatId, id);
} }
@ -1091,7 +1092,12 @@ export function deleteMessages<T extends GlobalState>(
if (!threadInfo?.lastMessageId || !idsSet.has(threadInfo.lastMessageId)) return; if (!threadInfo?.lastMessageId || !idsSet.has(threadInfo.lastMessageId)) return;
const newLastMessage = findLastMessage(global, chatId, threadId); 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) { if (threadId === MAIN_THREAD_ID) {
global = updateChatLastMessage(global, chatId, newLastMessage, true); global = updateChatLastMessage(global, chatId, newLastMessage, true);

View File

@ -33,6 +33,7 @@ import {
selectIsTrustedBot, selectIsTrustedBot,
selectSender, selectSender,
selectTabState, selectTabState,
selectTopic,
} from '../../selectors'; } from '../../selectors';
import { getIsMobile, getIsTablet } from '../../../hooks/useAppLayout'; import { getIsMobile, getIsTablet } from '../../../hooks/useAppLayout';
@ -773,8 +774,9 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
const currentChat = selectChat(global, chatId); const currentChat = selectChat(global, chatId);
if (currentChat) { if (currentChat) {
const title = getChatTitle(langProvider.oldTranslate, currentChat, chatId === currentUserId); const title = getChatTitle(langProvider.oldTranslate, currentChat, chatId === currentUserId);
if (currentChat.isForum && currentChat.topics?.[threadId]) { const topic = selectTopic(global, chatId, threadId);
setPageTitle(`${title} ${currentChat.topics[threadId].title}`); if (currentChat.isForum && topic) {
setPageTitle(`${title} ${topic.title}`);
return; return;
} }

View File

@ -36,10 +36,10 @@ import { addActionHandler, getGlobal } from './index';
import { INITIAL_GLOBAL_STATE, INITIAL_PERFORMANCE_STATE_MID, INITIAL_PERFORMANCE_STATE_MIN } from './initialState'; import { INITIAL_GLOBAL_STATE, INITIAL_PERFORMANCE_STATE_MID, INITIAL_PERFORMANCE_STATE_MIN } from './initialState';
import { clearGlobalForLockScreen } from './reducers'; import { clearGlobalForLockScreen } from './reducers';
import { import {
selectChat,
selectChatLastMessageId, selectChatLastMessageId,
selectChatMessages, selectChatMessages,
selectCurrentMessageList, selectCurrentMessageList,
selectTopics,
selectViewportIds, selectViewportIds,
selectVisibleUsers, selectVisibleUsers,
} from './selectors'; } from './selectors';
@ -248,6 +248,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.users.commonChatsById) { if (!cached.users.commonChatsById) {
cached.users.commonChatsById = initialState.users.commonChatsById; cached.users.commonChatsById = initialState.users.commonChatsById;
} }
if (!cached.chats.topicsInfoById) {
cached.chats.topicsInfoById = initialState.chats.topicsInfoById;
}
} }
function updateCache(force?: boolean) { function updateCache(force?: boolean) {
@ -446,6 +449,7 @@ 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),
}; };
} }
@ -486,7 +490,6 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
return; return;
} }
const chat = selectChat(global, chatId);
const chatLastMessageId = selectChatLastMessageId(global, chatId); const chatLastMessageId = selectChatLastMessageId(global, chatId);
const openedThreadIds = Array.from(openedChatThreadIds[chatId] || []); const openedThreadIds = Array.from(openedChatThreadIds[chatId] || []);
@ -498,8 +501,9 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
const threadsToSave = pickTruthy(current.threadsById, [MAIN_THREAD_ID, ...threadIds]); const threadsToSave = pickTruthy(current.threadsById, [MAIN_THREAD_ID, ...threadIds]);
const viewportIdsToSave = unique(Object.values(threadsToSave).flatMap((thread) => thread.lastViewportIds || [])); 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 const savedLastMessageIds = chatId === currentUserId && global.chats.lastMessageIds.saved
? Object.values(global.chats.lastMessageIds.saved) : []; ? Object.values(global.chats.lastMessageIds.saved) : [];
const lastMessageIdsToSave = [chatLastMessageId].concat(topicLastMessageIds).concat(savedLastMessageIds) const lastMessageIdsToSave = [chatLastMessageId].concat(topicLastMessageIds).concat(savedLastMessageIds)

View File

@ -145,15 +145,14 @@ export function isUserRightBanned(chat: ApiChat, key: keyof ApiChatBannedRights,
} }
export function getCanPostInChat( 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.isForum) {
if (chat.isNotJoined) { if (chat.isNotJoined) {
return false; return false;
} }
const topic = chat.topics?.[threadId];
if (topic?.isClosed && !topic.isOwner && !getHasAdminRight(chat, 'manageTopics')) { if (topic?.isClosed && !topic.isOwner && !getHasAdminRight(chat, 'manageTopics')) {
return false; return false;
} }
@ -264,18 +263,22 @@ export function getMessageSendingRestrictionReason(
} }
export function getForumComposerPlaceholder( 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<number, ApiTopic>,
isReplying?: boolean,
) { ) {
if (!chat?.isForum) { if (!chat?.isForum) {
return undefined; return undefined;
} }
if (threadId === MAIN_THREAD_ID) { 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'); return lang('lng_forum_replies_only');
} }
const topic = chat.topics?.[threadId]; const topic = topics?.[Number(threadId)];
if (!topic) { if (!topic) {
return undefined; return undefined;
} }

View File

@ -110,6 +110,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
byId: {}, byId: {},
fullInfoById: {}, fullInfoById: {},
similarChannelsById: {}, similarChannelsById: {},
topicsInfoById: {},
loadingParameters: { loadingParameters: {
active: {}, active: {},
archived: {}, archived: {},

View File

@ -1,5 +1,5 @@
import type { import type {
ApiChat, ApiChatFullInfo, ApiChatMember, ApiTopic, ApiChat, ApiChatFullInfo, ApiChatMember,
} from '../../api/types'; } from '../../api/types';
import type { ChatListType, GlobalState } from '../types'; import type { ChatListType, GlobalState } from '../types';
@ -8,8 +8,7 @@ import { areDeepEqual } from '../../util/areDeepEqual';
import { import {
areSortedArraysEqual, buildCollectionByKey, omit, pick, unique, areSortedArraysEqual, buildCollectionByKey, omit, pick, unique,
} from '../../util/iteratees'; } from '../../util/iteratees';
import { selectChat, selectChatFullInfo } from '../selectors'; import { selectChatFullInfo } from '../selectors';
import { updateThread, updateThreadInfo } from './messages';
const DEFAULT_CHAT_LISTS: ChatListType[] = ['active', 'archived']; const DEFAULT_CHAT_LISTS: ChatListType[] = ['active', 'archived'];
@ -423,95 +422,6 @@ export function addChatMembers<T extends GlobalState>(global: T, chat: ApiChat,
}); });
} }
export function updateListedTopicIds<T extends GlobalState>(
global: T, chatId: string, topicIds: number[],
): T {
return updateChat(global, chatId, {
listedTopicIds: unique([
...(global.chats.byId[chatId]?.listedTopicIds || []),
...topicIds,
]),
});
}
export function updateTopics<T extends GlobalState>(
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<T extends GlobalState>(
global: T, chatId: string, topicId: number, update: Partial<ApiTopic>,
): 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<T extends GlobalState>(
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<T extends GlobalState>( export function addSimilarChannels<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,

View File

@ -14,3 +14,4 @@ export * from './stories';
export * from './translations'; export * from './translations';
export * from './peers'; export * from './peers';
export * from './password'; export * from './password';
export * from './topics';

View File

@ -21,7 +21,6 @@ import {
mergeIdRanges, orderHistoryIds, orderPinnedIds, mergeIdRanges, orderHistoryIds, orderPinnedIds,
} from '../helpers'; } from '../helpers';
import { import {
selectChat,
selectChatMessage, selectChatMessage,
selectChatMessages, selectChatMessages,
selectChatScheduledMessages, selectChatScheduledMessages,
@ -43,10 +42,7 @@ import { removeIdFromSearchResults } from './middleSearch';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
import { clearMessageTranslation } from './translations'; import { clearMessageTranslation } from './translations';
type MessageStoreSections = { type MessageStoreSections = GlobalState['messages']['byChatId'][string];
byId: Record<number, ApiMessage>;
threadsById: Record<number, Thread>;
};
export function updateCurrentMessageList<T extends GlobalState>( export function updateCurrentMessageList<T extends GlobalState>(
global: T, global: T,
@ -128,7 +124,7 @@ export function updateThread<T extends GlobalState>(
}); });
} }
function updateMessageStore<T extends GlobalState>( export function updateMessageStore<T extends GlobalState>(
global: T, chatId: string, update: Partial<MessageStoreSections>, global: T, chatId: string, update: Partial<MessageStoreSections>,
): T { ): T {
const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} }; const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} };
@ -823,32 +819,6 @@ export function updateThreadUnreadFromForwardedMessage<T extends GlobalState>(
return global; return global;
} }
export function updateTopicLastMessageId<T extends GlobalState>(
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<T extends GlobalState>( export function addActiveMediaDownload<T extends GlobalState>(
global: T, global: T,
mediaHash: string, mediaHash: string,

View File

@ -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<T extends GlobalState>(
global: T, chatId: string, update: Partial<TopicsInfo>,
) {
const info = global.chats.topicsInfoById[chatId] || {};
global = {
...global,
chats: {
...global.chats,
topicsInfoById: {
...global.chats.topicsInfoById,
[chatId]: {
...info,
...update,
},
},
},
};
return global;
}
export function updateListedTopicIds<T extends GlobalState>(
global: T, chatId: string, topicIds: number[],
): T {
const listedIds = selectTopicsInfo(global, chatId)?.listedTopicIds || [];
return updateTopicsStore(global, chatId, {
listedTopicIds: unique([
...listedIds,
...topicIds,
]),
});
}
export function updateTopics<T extends GlobalState>(
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<T extends GlobalState>(
global: T, chatId: string, topicId: number, update: Partial<ApiTopic>,
): 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<T extends GlobalState>(
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<T extends GlobalState>(
global: T, chatId: string, threadId: number, lastMessageId: number,
) {
return updateTopic(global, chatId, threadId, {
lastMessageId,
});
}
export function replacePinnedTopicIds<T extends GlobalState>(
global: T, chatId: string, pinnedTopicIds: number[],
) {
return updateTopicsStore(global, chatId, {
orderedPinnedTopicIds: pinnedTopicIds,
});
}

View File

@ -12,3 +12,4 @@ export * from './statistics';
export * from './stories'; export * from './stories';
export * from './tabs'; export * from './tabs';
export * from './peers'; export * from './peers';
export * from './topics';

View File

@ -70,6 +70,7 @@ import { selectPeer } from './peers';
import { selectPeerStory } from './stories'; import { selectPeerStory } from './stories';
import { selectIsStickerFavorite } from './symbols'; import { selectIsStickerFavorite } from './symbols';
import { selectTabState } from './tabs'; import { selectTabState } from './tabs';
import { selectTopic } from './topics';
import { import {
selectBot, selectIsCurrentUserPremium, selectUser, selectUserStatus, selectBot, selectIsCurrentUserPremium, selectUser, selectUserStatus,
} from './users'; } from './users';
@ -483,17 +484,21 @@ export function selectForwardedSender<T extends GlobalState>(
return undefined; return undefined;
} }
export function selectTopicFromMessage<T extends GlobalState>(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; const MAX_MESSAGES_TO_DELETE_OWNER_TOPIC = 10;
export function selectCanDeleteOwnerTopic<T extends GlobalState>(global: T, chatId: string, topicId: number) { export function selectCanDeleteOwnerTopic<T extends GlobalState>(global: T, chatId: string, topicId: number) {
const chat = selectChat(global, chatId); const topic = selectTopic(global, chatId, topicId);
if (!chat) { if (topic && !topic.isOwner) return false;
return false;
}
if (chat.topics?.[topicId] && !chat.topics?.[topicId].isOwner) return false;
const thread = global.messages.byChatId[chatId]?.threadsById[topicId];
const thread = selectThread(global, chatId, topicId);
if (!thread) return false; if (!thread) return false;
const { listedIds } = thread; const { listedIds } = thread;
@ -576,15 +581,6 @@ export function selectThreadIdFromMessage<T extends GlobalState>(global: T, mess
return replyToTopId || replyToMsgId || GENERAL_TOPIC_ID; return replyToTopId || replyToMsgId || GENERAL_TOPIC_ID;
} }
export function selectTopicFromMessage<T extends GlobalState>(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<T extends GlobalState>(global: T, message: ApiMessage, threadId: ThreadId) { export function selectCanReplyToMessage<T extends GlobalState>(global: T, message: ApiMessage, threadId: ThreadId) {
const chat = selectChat(global, message.chatId); const chat = selectChat(global, message.chatId);
if (!chat || chat.isRestricted || chat.isForbidden) return false; if (!chat || chat.isRestricted || chat.isForbidden) return false;
@ -597,7 +593,8 @@ export function selectCanReplyToMessage<T extends GlobalState>(global: T, messag
const threadInfo = selectThreadInfo(global, message.chatId, threadId); const threadInfo = selectThreadInfo(global, message.chatId, threadId);
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const chatFullInfo = selectChatFullInfo(global, chat.id); 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; if (!canPostInChat) return false;
const messageTopic = selectTopicFromMessage(global, message); const messageTopic = selectTopicFromMessage(global, message);

View File

@ -0,0 +1,14 @@
import type { ThreadId } from '../../types';
import type { GlobalState, TopicsInfo } from '../types';
export function selectTopicsInfo<T extends GlobalState>(global: T, chatId: string): TopicsInfo | undefined {
return global.chats.topicsInfoById[chatId];
}
export function selectTopics<T extends GlobalState>(global: T, chatId: string) {
return selectTopicsInfo(global, chatId)?.topicsById;
}
export function selectTopic<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
return selectTopicsInfo(global, chatId)?.topicsById?.[threadId];
}

View File

@ -74,6 +74,7 @@ import type {
ApiThemeParameters, ApiThemeParameters,
ApiThreadInfo, ApiThreadInfo,
ApiTimezone, ApiTimezone,
ApiTopic,
ApiTranscription, ApiTranscription,
ApiTypeStoryView, ApiTypeStoryView,
ApiTypingStatus, ApiTypingStatus,
@ -213,6 +214,13 @@ export interface ServiceNotification {
isDeleted?: boolean; isDeleted?: boolean;
} }
export interface TopicsInfo {
totalCount: number;
topicsById: Record<ThreadId, ApiTopic>;
listedTopicIds?: number[];
orderedPinnedTopicIds?: number[];
}
export type ApiLimitType = export type ApiLimitType =
| 'uploadMaxFileparts' | 'uploadMaxFileparts'
| 'stickersFaved' | 'stickersFaved'
@ -968,6 +976,7 @@ export type GlobalState = {
all?: Record<string, number>; all?: Record<string, number>;
saved?: Record<string, number>; saved?: Record<string, number>;
}; };
topicsInfoById: Record<string, TopicsInfo>;
loadingParameters: Record<ChatListType, { loadingParameters: Record<ChatListType, {
nextOffsetId?: number; nextOffsetId?: number;
nextOffsetPeerId?: string; nextOffsetPeerId?: string;

View File

@ -18,6 +18,7 @@ import {
selectNotifyExceptions, selectNotifyExceptions,
selectNotifySettings, selectNotifySettings,
selectTabState, selectTabState,
selectTopics,
} from '../global/selectors'; } from '../global/selectors';
import arePropsShallowEqual from './arePropsShallowEqual'; import arePropsShallowEqual from './arePropsShallowEqual';
import { createCallbackManager } from './callbacks'; import { createCallbackManager } from './callbacks';
@ -74,6 +75,7 @@ let prevGlobal: {
isSavedFolderFullyLoaded?: boolean; isSavedFolderFullyLoaded?: boolean;
lastAllMessageIds?: GlobalState['chats']['lastMessageIds']['all']; lastAllMessageIds?: GlobalState['chats']['lastMessageIds']['all'];
lastSavedMessageIds?: GlobalState['chats']['lastMessageIds']['saved']; lastSavedMessageIds?: GlobalState['chats']['lastMessageIds']['saved'];
topicsInfoById: GlobalState['chats']['topicsInfoById'];
chatsById: Record<string, ApiChat>; chatsById: Record<string, ApiChat>;
foldersById: Record<string, ApiChatFolder>; foldersById: Record<string, ApiChatFolder>;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
@ -215,6 +217,7 @@ function updateFolderManager(global: GlobalState) {
const areChatsChanged = global.chats.byId !== prevGlobal.chatsById; const areChatsChanged = global.chats.byId !== prevGlobal.chatsById;
const areSavedLastMessageIdsChanged = global.chats.lastMessageIds.saved !== prevGlobal.lastSavedMessageIds; const areSavedLastMessageIdsChanged = global.chats.lastMessageIds.saved !== prevGlobal.lastSavedMessageIds;
const areAllLastMessageIdsChanged = global.chats.lastMessageIds.all !== prevGlobal.lastAllMessageIds; const areAllLastMessageIdsChanged = global.chats.lastMessageIds.all !== prevGlobal.lastAllMessageIds;
const areTopicsChanged = global.chats.topicsInfoById !== prevGlobal.topicsInfoById;
const areUsersChanged = global.users.byId !== prevGlobal.usersById; const areUsersChanged = global.users.byId !== prevGlobal.usersById;
const areNotifySettingsChanged = selectNotifySettings(global) !== prevGlobal.notifySettings; const areNotifySettingsChanged = selectNotifySettings(global) !== prevGlobal.notifySettings;
const areNotifyExceptionsChanged = selectNotifyExceptions(global) !== prevGlobal.notifyExceptions; const areNotifyExceptionsChanged = selectNotifyExceptions(global) !== prevGlobal.notifyExceptions;
@ -229,7 +232,7 @@ function updateFolderManager(global: GlobalState) {
if (!( if (!(
isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged || areFoldersChanged isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged || areFoldersChanged
|| areChatsChanged || areUsersChanged || areNotifySettingsChanged || areNotifyExceptionsChanged || areChatsChanged || areUsersChanged || areTopicsChanged || areNotifySettingsChanged || areNotifyExceptionsChanged
|| areSavedLastMessageIdsChanged || areAllLastMessageIdsChanged || areSavedLastMessageIdsChanged || areAllLastMessageIdsChanged
) )
) { ) {
@ -515,8 +518,9 @@ function buildChatSummary<T extends GlobalState>(
const { const {
id, type, isRestricted, isNotJoined, migratedTo, folderId, id, type, isRestricted, isNotJoined, migratedTo, folderId,
unreadCount: chatUnreadCount, unreadMentionsCount: chatUnreadMentionsCount, hasUnreadMark, unreadCount: chatUnreadCount, unreadMentionsCount: chatUnreadMentionsCount, hasUnreadMark,
isForum, topics, isForum,
} = chat; } = chat;
const topics = selectTopics(global, chat.id);
const { unreadCount, unreadMentionsCount } = isForum const { unreadCount, unreadMentionsCount } = isForum
? Object.values(topics || {}).reduce((acc, topic) => { ? Object.values(topics || {}).reduce((acc, topic) => {
@ -805,6 +809,7 @@ function buildInitials() {
foldersById: {}, foldersById: {},
chatsById: {}, chatsById: {},
usersById: {}, usersById: {},
topicsInfoById: {},
notifySettings: {} as NotifySettings, notifySettings: {} as NotifySettings,
notifyExceptions: {}, notifyExceptions: {},
}, },