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 {
ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet,
@ -48,10 +47,6 @@ export interface ApiChat {
emojiStatus?: ApiEmojiStatus;
isForum?: boolean;
isForumAsMessages?: true;
topics?: Record<ThreadId, ApiTopic>;
listedTopicIds?: number[];
topicsCount?: number;
orderedPinnedTopicIds?: number[];
boostLevel?: number;
// Calls

View File

@ -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<number, ApiTopic>;
renderLastMessage: () => React.ReactNode;
observeIntersection?: ObserveFn;
};
@ -35,6 +36,7 @@ const MAX_TOPICS = 3;
const ChatForumLastMessage: FC<OwnProps> = ({
chat,
topics,
renderLastMessage,
observeIntersection,
}) => {
@ -48,12 +50,12 @@ const ChatForumLastMessage: FC<OwnProps> = ({
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<number | undefined>(undefined);

View File

@ -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<OwnProps>(
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!);

View File

@ -16,6 +16,7 @@ import {
selectPeerPhotos,
selectTabState,
selectThreadMessagesCount,
selectTopic,
selectUser,
selectUserStatus,
} from '../../global/selectors';
@ -373,7 +374,7 @@ export default memo(withGlobal<OwnProps>(
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;

View File

@ -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<OwnProps & StateProps> = ({
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(

View File

@ -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<OwnProps>(
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);

View File

@ -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<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
return acc;
}, {} as Record<number, ApiTopic>)
: 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<OwnProps> = ({
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<OwnProps> = ({
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 {

View File

@ -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<number, ApiTopic>;
isMuted?: boolean;
user?: ApiUser;
userStatus?: ApiUserStatus;
@ -118,6 +121,8 @@ const Chat: FC<OwnProps & StateProps> = ({
orderDiff,
animationType,
isPinned,
listedTopicIds,
topics,
observeIntersection,
chat,
isMuted,
@ -190,6 +195,7 @@ const Chat: FC<OwnProps & StateProps> = ({
orderDiff,
isSavedDialog,
isPreview,
topics,
});
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
@ -284,10 +290,10 @@ const Chat: FC<OwnProps & StateProps> = ({
// 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<OwnProps & StateProps> = ({
/>
<div className="avatar-badge-wrapper">
<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>
{chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={withInterfaceAnimations} />
@ -376,6 +388,7 @@ const Chat: FC<OwnProps & StateProps> = ({
isPinned={isPinned}
isMuted={isMuted}
isSavedDialog={isSavedDialog}
topics={topics}
/>
)}
</div>
@ -460,6 +473,8 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
lastMessage,
lastMessageId,
currentUserId: global.currentUserId!,
listedTopicIds: topicsInfo?.listedTopicIds,
topics: topicsInfo?.topicsById,
};
},
)(Chat));

View File

@ -24,10 +24,19 @@ type OwnProps = {
isSavedDialog?: boolean;
shouldShowOnlyMostImportant?: boolean;
forceHidden?: boolean | Signal<boolean>;
topics?: Record<number, ApiTopic>;
};
const ChatBadge: FC<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
), [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;

View File

@ -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<OwnProps & StateProps> = ({
currentTopicId,
isOpen,
isHidden,
topicsInfo,
onTopicSearch,
onCloseAnimationEnd,
onOpenAnimationStart,
@ -80,12 +88,13 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const scrollTopHandlerRef = useRef<HTMLDivElement>(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<OwnProps & StateProps> = ({
});
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<OwnProps & StateProps> = ({
<Topic
key={id}
chatId={chat!.id}
topic={chat!.topics![id]}
topic={topicsInfo!.topicsById[id]}
style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`}
isSelected={currentTopicId === id}
observeIntersection={observe}
@ -201,7 +213,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
));
}
const isLoading = chat?.topics === undefined;
const isLoading = topicsInfo === undefined;
return (
<div
@ -271,7 +283,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
)}
</InfiniteScroll>
{!isLoading && viewportIds?.length === 1 && viewportIds[0] === GENERAL_TOPIC_ID && (
<EmptyForum chatId={chat.id} />
<EmptyForum chatId={chatId!} />
)}
</div>
);
@ -285,11 +297,13 @@ export default memo(withGlobal<OwnProps>(
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),

View File

@ -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<number, ApiTopic>;
};
const Topic: FC<OwnProps & StateProps> = ({
@ -91,6 +93,7 @@ const Topic: FC<OwnProps & StateProps> = ({
typingStatus,
draft,
wasTopicOpened,
topics,
}) => {
const {
openThread,
@ -138,6 +141,7 @@ const Topic: FC<OwnProps & StateProps> = ({
observeIntersection,
isTopic: true,
typingStatus,
topics,
animationType,
withInterfaceAnimations,
@ -208,6 +212,7 @@ const Topic: FC<OwnProps & StateProps> = ({
isMuted={isMuted}
topic={topic}
wasTopicOpened={wasTopicOpened}
topics={topics}
/>
</div>
</div>
@ -253,6 +258,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
}),
canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id,
wasTopicOpened,
topics,
};
},
)(Topic));

View File

@ -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<number, ApiTopic>;
lastMessage?: ApiMessage;
chatId: string;
typingStatus?: ApiTypingStatus;
@ -192,6 +194,7 @@ export default function useChatListEntry({
chat={chat}
renderLastMessage={renderLastMessageOrTyping}
observeIntersection={observeIntersection}
topics={topics}
/>
);
}

View File

@ -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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global, { chatId, topicId }): StateProps => {
const chat = selectChat(global, chatId);
const topic = chat?.topics?.[topicId];
const topic = selectTopic(global, chatId, topicId);
return {
topic,

View File

@ -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<OwnProps>(
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,

View File

@ -35,6 +35,7 @@ import {
selectNotifyExceptions,
selectNotifySettings,
selectTabState,
selectTopic,
selectUser,
selectUserFullInfo,
} from '../../global/selectors';
@ -760,7 +761,7 @@ export default memo(withGlobal<OwnProps>(
&& !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')
);

View File

@ -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<OwnProps>(
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;

View File

@ -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<number, ApiTopic>;
};
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<OwnProps>(
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<OwnProps>(
);
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<OwnProps>(
isSavedDialog,
canShowOpenChatButton,
isContactRequirePremium,
topics,
};
},
)(MiddleColumn));

View File

@ -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<OwnProps>(
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);

View File

@ -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<OwnProps>(
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);

View File

@ -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,

View File

@ -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,

View File

@ -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<OwnProps>(
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;

View File

@ -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<void> =
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<void> =
}) : 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<void> =>
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,

View File

@ -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,
});

View File

@ -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<T extends GlobalState>(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);

View File

@ -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;

View File

@ -78,6 +78,7 @@ import {
selectThreadByMessage,
selectThreadIdFromMessage,
selectThreadInfo,
selectTopic,
selectTopicFromMessage,
selectViewportIds,
} from '../../selectors';
@ -1061,7 +1062,7 @@ export function deleteMessages<T extends GlobalState>(
isDeleting: true,
});
if (chat.topics?.[id]) {
if (selectTopic(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;
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);

View File

@ -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;
}

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 { 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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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)

View File

@ -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<number, ApiTopic>,
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;
}

View File

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

View File

@ -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<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>(
global: T,
chatId: string,

View File

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

View File

@ -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<number, ApiMessage>;
threadsById: Record<number, Thread>;
};
type MessageStoreSections = GlobalState['messages']['byChatId'][string];
export function updateCurrentMessageList<T extends GlobalState>(
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>,
): T {
const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} };
@ -823,32 +819,6 @@ export function updateThreadUnreadFromForwardedMessage<T extends GlobalState>(
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>(
global: T,
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 './tabs';
export * from './peers';
export * from './topics';

View File

@ -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<T extends GlobalState>(
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;
export function selectCanDeleteOwnerTopic<T extends GlobalState>(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<T extends GlobalState>(global: T, mess
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) {
const chat = selectChat(global, message.chatId);
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 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);

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

View File

@ -18,6 +18,7 @@ import {
selectNotifyExceptions,
selectNotifySettings,
selectTabState,
selectTopics,
} from '../global/selectors';
import arePropsShallowEqual from './arePropsShallowEqual';
import { createCallbackManager } from './callbacks';
@ -74,6 +75,7 @@ let prevGlobal: {
isSavedFolderFullyLoaded?: boolean;
lastAllMessageIds?: GlobalState['chats']['lastMessageIds']['all'];
lastSavedMessageIds?: GlobalState['chats']['lastMessageIds']['saved'];
topicsInfoById: GlobalState['chats']['topicsInfoById'];
chatsById: Record<string, ApiChat>;
foldersById: Record<string, ApiChatFolder>;
usersById: Record<string, ApiUser>;
@ -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<T extends GlobalState>(
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: {},
},