From fc1883f1cca89896a5813e18f3ce21de8697f923 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:43:55 +0100 Subject: [PATCH] Bots: Better forum view handling (#6705) --- src/api/gramjs/apiBuilders/users.ts | 3 +- src/api/types/users.ts | 1 + src/assets/localization/fallback.strings | 1 + src/components/common/Composer.tsx | 188 +++++++++--------- src/components/left/main/Chat.tsx | 9 +- .../left/main/hooks/useChatListEntry.tsx | 5 +- src/components/middle/HeaderMenuContainer.tsx | 7 +- src/components/middle/MessageList.tsx | 12 +- src/components/middle/MessageListContent.tsx | 6 +- src/global/actions/api/messages.ts | 8 +- src/global/reducers/topics.ts | 2 +- src/index.tsx | 4 +- src/types/language.d.ts | 1 + 13 files changed, 136 insertions(+), 111 deletions(-) diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 02d63e3f5..f058b946c 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -112,7 +112,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { const { id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit, - sendPaidMessagesStars, profileColor, botForumView, + sendPaidMessagesStars, profileColor, botForumView, botForumCanManageTopics, } = mtpUser; const storiesMaxId = mtpUser.storiesMaxId?.maxId; const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined; @@ -157,6 +157,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { profileColor: profileColor && buildApiPeerColor(profileColor), paidMessagesStars: toJSNumber(sendPaidMessagesStars), isBotForum: botForumView, + canManageBotForumTopics: botForumCanManageTopics, }; } diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 797c9f071..380eff4fb 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -48,6 +48,7 @@ export interface ApiUser { botVerificationIconId?: string; paidMessagesStars?: number; isBotForum?: boolean; + canManageBotForumTopics?: boolean; } export interface ApiUserFullInfo { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 0606d9f6c..9821e5327 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1374,6 +1374,7 @@ "ComposerPlaceholderBroadcastSilent" = "Silent Broadcast"; "ComposerPlaceholderTopic" = "Message in {topic}"; "ComposerPlaceholderTopicGeneral" = "Message in General"; +"ComposerPlaceholderBotTopicGeneral" = "Off-thread message"; "ComposerStoryPlaceholderLocked" = "Replies restricted"; "ComposerPlaceholderNoText" = "Text not allowed"; "AriaComposerCancelVoice" = "Cancel voice recording"; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 7421ad45a..0687a93f6 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -222,96 +222,96 @@ type OwnProps = { onBlur?: NoneToVoidFunction; }; -type StateProps = - { - isOnActiveTab: boolean; - editingMessage?: ApiMessage; - chat?: ApiChat; - chatFullInfo?: ApiChatFullInfo; - draft?: ApiDraft; - replyToTopic?: ApiTopic; - currentMessageList?: MessageList; - isChatWithBot?: boolean; - isChatWithSelf?: boolean; - isChannel?: boolean; - isForCurrentMessageList: boolean; - isRightColumnShown?: boolean; - isSelectModeActive?: boolean; - isReactionPickerOpen?: boolean; - shouldDisplayGiftsButton?: boolean; - isForwarding?: boolean; - isReplying?: boolean; - hasSuggestedPost?: boolean; - forwardedMessagesCount?: number; - pollModal: TabState['pollModal']; - todoListModal: TabState['todoListModal']; - botKeyboardMessageId?: number; - botKeyboardPlaceholder?: string; - withScheduledButton?: boolean; - isInScheduledList?: boolean; - canScheduleUntilOnline?: boolean; - stickersForEmoji?: ApiSticker[]; - customEmojiForEmoji?: ApiSticker[]; - currentUserId?: string; - currentUser?: ApiUser; - recentEmojis: string[]; - contentToBeScheduled?: TabState['contentToBeScheduled']; - shouldSuggestStickers?: boolean; - shouldSuggestCustomEmoji?: boolean; - baseEmojiKeywords?: Record; - emojiKeywords?: Record; - topInlineBotIds?: string[]; - isInlineBotLoading: boolean; - inlineBots?: Record; - botCommands?: ApiBotCommand[] | false; - botMenuButton?: ApiBotMenuButton; - sendAsPeer?: ApiPeer; - sendAsId?: string; - editingDraft?: ApiFormattedText; - requestedDraft?: ApiFormattedText; - requestedDraftFiles?: File[]; - attachBots: GlobalState['attachMenu']['bots']; - attachMenuPeerType?: ApiAttachMenuPeerType; - theme: ThemeKey; - fileSizeLimit: number; - captionLimit: number; - isCurrentUserPremium?: boolean; - canSendVoiceByPrivacy?: boolean; - attachmentSettings: GlobalState['attachmentSettings']; - slowMode?: ApiChatFullInfo['slowMode']; - shouldUpdateStickerSetOrder?: boolean; - availableReactions?: ApiAvailableReaction[]; - topReactions?: ApiReaction[]; - canPlayAnimatedEmojis?: boolean; - canBuyPremium?: boolean; - shouldCollectDebugLogs?: boolean; - sentStoryReaction?: ApiReaction; - stealthMode?: ApiStealthMode; - canSendOneTimeMedia?: boolean; - quickReplyMessages?: Record; - quickReplies?: Record; - canSendQuickReplies?: boolean; - webPagePreview?: ApiWebPage; - noWebPage?: boolean; - isContactRequirePremium?: boolean; - paidMessagesStars?: number; - effect?: ApiAvailableEffect; - effectReactions?: ApiReaction[]; - areEffectsSupported?: boolean; - canPlayEffect?: boolean; - shouldPlayEffect?: boolean; - maxMessageLength: number; - shouldPaidMessageAutoApprove?: boolean; - isSilentPosting?: boolean; - isPaymentMessageConfirmDialogOpen: boolean; - starsBalance: number; - isStarsBalanceModalOpen: boolean; - disallowedGifts?: ApiDisallowedGifts; - isAccountFrozen?: boolean; - isAppConfigLoaded?: boolean; - insertingPeerIdMention?: string; - pollMaxAnswers?: number; - }; +type StateProps = { + isOnActiveTab: boolean; + editingMessage?: ApiMessage; + chat?: ApiChat; + user?: ApiUser; + chatFullInfo?: ApiChatFullInfo; + draft?: ApiDraft; + replyToTopic?: ApiTopic; + currentMessageList?: MessageList; + isChatWithBot?: boolean; + isChatWithSelf?: boolean; + isChannel?: boolean; + isForCurrentMessageList: boolean; + isRightColumnShown?: boolean; + isSelectModeActive?: boolean; + isReactionPickerOpen?: boolean; + shouldDisplayGiftsButton?: boolean; + isForwarding?: boolean; + isReplying?: boolean; + hasSuggestedPost?: boolean; + forwardedMessagesCount?: number; + pollModal: TabState['pollModal']; + todoListModal: TabState['todoListModal']; + botKeyboardMessageId?: number; + botKeyboardPlaceholder?: string; + withScheduledButton?: boolean; + isInScheduledList?: boolean; + canScheduleUntilOnline?: boolean; + stickersForEmoji?: ApiSticker[]; + customEmojiForEmoji?: ApiSticker[]; + currentUserId?: string; + currentUser?: ApiUser; + recentEmojis: string[]; + contentToBeScheduled?: TabState['contentToBeScheduled']; + shouldSuggestStickers?: boolean; + shouldSuggestCustomEmoji?: boolean; + baseEmojiKeywords?: Record; + emojiKeywords?: Record; + topInlineBotIds?: string[]; + isInlineBotLoading: boolean; + inlineBots?: Record; + botCommands?: ApiBotCommand[] | false; + botMenuButton?: ApiBotMenuButton; + sendAsPeer?: ApiPeer; + sendAsId?: string; + editingDraft?: ApiFormattedText; + requestedDraft?: ApiFormattedText; + requestedDraftFiles?: File[]; + attachBots: GlobalState['attachMenu']['bots']; + attachMenuPeerType?: ApiAttachMenuPeerType; + theme: ThemeKey; + fileSizeLimit: number; + captionLimit: number; + isCurrentUserPremium?: boolean; + canSendVoiceByPrivacy?: boolean; + attachmentSettings: GlobalState['attachmentSettings']; + slowMode?: ApiChatFullInfo['slowMode']; + shouldUpdateStickerSetOrder?: boolean; + availableReactions?: ApiAvailableReaction[]; + topReactions?: ApiReaction[]; + canPlayAnimatedEmojis?: boolean; + canBuyPremium?: boolean; + shouldCollectDebugLogs?: boolean; + sentStoryReaction?: ApiReaction; + stealthMode?: ApiStealthMode; + canSendOneTimeMedia?: boolean; + quickReplyMessages?: Record; + quickReplies?: Record; + canSendQuickReplies?: boolean; + webPagePreview?: ApiWebPage; + noWebPage?: boolean; + isContactRequirePremium?: boolean; + paidMessagesStars?: number; + effect?: ApiAvailableEffect; + effectReactions?: ApiReaction[]; + areEffectsSupported?: boolean; + canPlayEffect?: boolean; + shouldPlayEffect?: boolean; + maxMessageLength: number; + shouldPaidMessageAutoApprove?: boolean; + isSilentPosting?: boolean; + isPaymentMessageConfirmDialogOpen: boolean; + starsBalance: number; + isStarsBalanceModalOpen: boolean; + disallowedGifts?: ApiDisallowedGifts; + isAccountFrozen?: boolean; + isAppConfigLoaded?: boolean; + insertingPeerIdMention?: string; + pollMaxAnswers?: number; +}; enum MainButtonState { Send = 'send', @@ -352,6 +352,7 @@ const Composer: FC = ({ draft, chat, chatFullInfo, + user, replyToTopic, isForCurrentMessageList, isCurrentUserPremium, @@ -1758,6 +1759,10 @@ const Composer: FC = ({ return lang('ComposerPlaceholderAnonymous'); } + if (chat?.isBotForum && !user?.canManageBotForumTopics && threadId === MAIN_THREAD_ID) { + return lang('ComposerPlaceholderBotTopicGeneral'); + } + if (chat?.isForum && !chat.isBotForum && chat.isForumAsMessages && threadId === MAIN_THREAD_ID) { return replyToTopic ? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title }) @@ -1775,7 +1780,7 @@ const Composer: FC = ({ }, [ activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked, isInStoryViewer, isSilentPosting, lang, replyToTopic, isReplying, threadId, windowWidth, paidMessagesStars, - hasSuggestedPost, slowModePlaceholder, stealthMode?.activeUntil, + hasSuggestedPost, slowModePlaceholder, stealthMode?.activeUntil, user?.canManageBotForumTopics, ]); useEffect(() => { @@ -2635,6 +2640,7 @@ export default memo(withGlobal( editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), draft, chat, + user, isChatWithBot, isChatWithSelf, isForCurrentMessageList, diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 78cc9598a..3a9502806 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -204,6 +204,8 @@ const Chat: FC = ({ const { isForum, isForumAsMessages, isMonoforum } = chat || {}; + const shouldForceNonForumView = chat?.isBotForum && listedTopicIds && !listedTopicIds.length; + useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage); const tagFolderIds = useMemo(() => { @@ -244,6 +246,7 @@ const Chat: FC = ({ onReorderAnimationEnd, topicIds: listedTopicIds, hasTags: shouldRenderTags, + shouldForceNonForumView, }); const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); @@ -255,7 +258,7 @@ const Chat: FC = ({ return; } - const noForumTopicPanel = isMobile && isForumAsMessages; + const noForumTopicPanel = (isMobile && isForumAsMessages) || shouldForceNonForumView; if (isMobile) { setShouldCloseRightColumn({ value: true }); @@ -288,7 +291,7 @@ const Chat: FC = ({ openForumPanel({ chatId }, { forceOnHeavyAnimation: true }); } - if (!isForumAsMessages) return; + if (!isForumAsMessages && !shouldForceNonForumView) return; } } @@ -397,7 +400,7 @@ const Chat: FC = ({ const chatClassName = buildClassName( 'Chat chat-item-clickable', isUserId(chatId) ? 'private' : 'group', - isForum && 'forum', + isForum && !shouldForceNonForumView && 'forum', isSelected && 'selected', isSelectedForum && 'selected-forum', isPreview && 'standalone', diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 06cf59d3c..145f84b10 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -53,6 +53,7 @@ export default function useChatListEntry({ isSavedDialog, isPreview, hasTags, + shouldForceNonForumView, onReorderAnimationEnd, }: { chat?: ApiChat; @@ -74,6 +75,7 @@ export default function useChatListEntry({ orderDiff: number; shiftDiff: number; withInterfaceAnimations?: boolean; + shouldForceNonForumView?: boolean; onReorderAnimationEnd?: NoneToVoidFunction; }) { const lang = useLang(); @@ -151,7 +153,8 @@ export default function useChatListEntry({ ]); function renderSubtitle() { - if (chat?.isForum && !isTopic) { + const shouldRenderAsForum = chat?.isForum && !isTopic && !shouldForceNonForumView; + if (shouldRenderAsForum) { return ( = ({ withForumActions, isTopic, isForum, + isBotForum, isForumAsMessages, isChatInfoShown, canStartBot, @@ -627,7 +629,7 @@ const HeaderMenuContainer: FC = ({ {oldLang('lng_forum_topic_edit')} )} - {isMobile && !withForumActions && isForum && !isTopic && ( + {isMobile && !withForumActions && isForum && !isBotForum && !isTopic && ( = ({
{pendingJoinRequests}
)} - {withForumActions && !isTopic && !isForumAsMessages && ( + {withForumActions && !isTopic && !isBotForum && !isForumAsMessages && ( ( isPrivate, isTopic: chat?.isForum && !isMainThread, isForum: chat?.isForum, + isBotForum: chat?.isBotForum, isForumAsMessages: chat?.isForumAsMessages, canAddContact, canDeleteChat: getCanDeleteChat(chat), diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 94ced632e..b21812c9d 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -45,6 +45,7 @@ import { selectTabState, selectTopic, selectTranslationLanguage, + selectUser, selectUserFullInfo, } from '../../global/selectors'; import { selectIsChatRestricted } from '../../global/selectors/chats'; @@ -149,7 +150,7 @@ type StateProps = { translationLanguage?: string; shouldAutoTranslate?: boolean; isActive?: boolean; - isBotForum?: boolean; + canManageBotForumTopics?: boolean; shouldScrollToBottom?: boolean; }; @@ -199,7 +200,7 @@ const MessageList = ({ canPost, isSynced, isActive, - isBotForum, + canManageBotForumTopics, shouldScrollToBottom, // eslint-disable-next-line @typescript-eslint/no-shadow isChatMonoforum, @@ -812,7 +813,7 @@ const MessageList = ({ Content.StarsRequired ) : isContactRequirePremium && !hasMessages ? ( Content.PremiumRequired - ) : (isBot || isNonContact) && !hasMessages ? ( + ) : (isBot || isNonContact) && !hasMessages && threadId === MAIN_THREAD_ID ? ( Content.AccountInfo ) : shouldRenderGreeting ? ( Content.ContactGreeting @@ -878,7 +879,7 @@ const MessageList = ({ noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current} isQuickPreview={isQuickPreview} canPost={canPost} - isBotForum={isBotForum} + canManageBotForumTopics={canManageBotForumTopics} shouldScrollToBottom={shouldScrollToBottom} onScrollDownToggle={onScrollDownToggle} onNotchToggle={onNotchToggle} @@ -910,6 +911,7 @@ export default memo(withGlobal( const tabState = selectTabState(global); const currentUserId = global.currentUserId!; const chat = selectChat(global, chatId); + const user = selectUser(global, chatId); const userFullInfo = selectUserFullInfo(global, chatId); const readState = selectThreadReadState(global, chatId, threadId); if (!chat) { @@ -1014,7 +1016,7 @@ export default memo(withGlobal( canTranslate, translationLanguage, shouldAutoTranslate, - isBotForum: chat.isBotForum, + canManageBotForumTopics: chat.isBotForum && user?.canManageBotForumTopics, shouldScrollToBottom, }; }, diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 34d3d6460..612658b55 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -60,7 +60,7 @@ interface OwnProps { withUsers: boolean; isChannelChat: boolean | undefined; isChatMonoforum?: boolean; - isBotForum?: boolean; + canManageBotForumTopics?: boolean; isEmptyThread?: boolean; isComments?: boolean; noAvatars: boolean; @@ -101,7 +101,7 @@ const MessageListContent = ({ withUsers, isChannelChat, isChatMonoforum, - isBotForum, + canManageBotForumTopics, noAvatars, containerRef, anchorIdRef, @@ -256,7 +256,7 @@ const MessageListContent = ({ }; const renderBotForumTopicAction = () => { - if (!isBotForum || threadId !== MAIN_THREAD_ID) return undefined; + if (!canManageBotForumTopics || threadId !== MAIN_THREAD_ID) return undefined; return (
diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 24124c933..d450ecc47 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -392,6 +392,7 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise } const chat = selectChat(global, chatId!)!; + const user = selectUser(global, chatId!); const draft = selectDraft(global, chatId!, threadId!); const isForwarding = selectTabState(global, tabId).forwardMessages?.messageIds?.length; @@ -450,7 +451,8 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise suggestedMedia = suggestedMessage.content; } - if (chat.isBotForum && threadId === MAIN_THREAD_ID && replyInfo?.type === 'message') { + if (chat.isBotForum && threadId === MAIN_THREAD_ID && replyInfo?.type === 'message' + && user?.canManageBotForumTopics) { const replyMessage = selectChatMessage(global, chatId!, replyInfo.replyToMsgId); const replyThreadId = replyMessage && selectThreadIdFromMessage(global, replyMessage); actions.openThread({ @@ -490,7 +492,9 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise } // Create new bot forum topic - if (chat.isBotForum && threadId === MAIN_THREAD_ID && replyInfo?.type !== 'message') { + if (chat.isBotForum && user?.canManageBotForumTopics && threadId === MAIN_THREAD_ID + && replyInfo?.type !== 'message' + ) { const baseTitle = params.text ?? getTranslationFn()('BotForumTopicTitlePlaceholder'); const title = baseTitle.length > 12 ? `${baseTitle.slice(0, 12)}...` : baseTitle; const topic = await callApi('createTopic', { diff --git a/src/global/reducers/topics.ts b/src/global/reducers/topics.ts index e7b3ac356..36f288507 100644 --- a/src/global/reducers/topics.ts +++ b/src/global/reducers/topics.ts @@ -27,7 +27,7 @@ const SAFE_MIN_PROPERTIES: (keyof ApiTopic)[] = [ export function updateTopicsInfo( global: T, chatId: string, update: Partial, ) { - const info = global.chats.topicsInfoById[chatId] || {}; + const info = global.chats.topicsInfoById[chatId] || { topicsById: {} }; global = { ...global, diff --git a/src/index.tsx b/src/index.tsx index 31e1b2015..a683a1450 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,7 +11,7 @@ import { DEBUG, STRICTERDOM_ENABLED, } from './config'; import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom'; -import { selectChat, selectChatFullInfo, selectCurrentMessageList, selectTabState } from './global/selectors'; +import { selectChat, selectCurrentMessageList, selectPeerFullInfo, selectTabState } from './global/selectors'; import { selectSharedSettings } from './global/selectors/sharedState'; import { betterView } from './util/betterView'; import { IS_TAURI } from './util/browser/globalEnvironment'; @@ -115,7 +115,7 @@ async function init() { console.warn( 'CURRENT MESSAGE LIST', selectChat(currentGlobal, currentMessageList.chatId), - selectChatFullInfo(currentGlobal, currentMessageList.chatId), + selectPeerFullInfo(currentGlobal, currentMessageList.chatId), currentGlobal.messages.byChatId[currentMessageList.chatId], ); } diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 254dfcded..0de8ccd2d 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1170,6 +1170,7 @@ export interface LangPair { 'ComposerPlaceholderBroadcast': undefined; 'ComposerPlaceholderBroadcastSilent': undefined; 'ComposerPlaceholderTopicGeneral': undefined; + 'ComposerPlaceholderBotTopicGeneral': undefined; 'ComposerStoryPlaceholderLocked': undefined; 'ComposerPlaceholderNoText': undefined; 'AriaComposerCancelVoice': undefined;