Bots: Better forum view handling (#6705)

This commit is contained in:
zubiden 2026-02-22 23:43:55 +01:00 committed by Alexander Zinchuk
parent 48b722939b
commit fc1883f1cc
13 changed files with 136 additions and 111 deletions

View File

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

View File

@ -48,6 +48,7 @@ export interface ApiUser {
botVerificationIconId?: string;
paidMessagesStars?: number;
isBotForum?: boolean;
canManageBotForumTopics?: boolean;
}
export interface ApiUserFullInfo {

View File

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

View File

@ -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<string, string[]>;
emojiKeywords?: Record<string, string[]>;
topInlineBotIds?: string[];
isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>;
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<number, ApiMessage>;
quickReplies?: Record<number, ApiQuickReply>;
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<string, string[]>;
emojiKeywords?: Record<string, string[]>;
topInlineBotIds?: string[];
isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>;
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<number, ApiMessage>;
quickReplies?: Record<number, ApiQuickReply>;
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<OwnProps & StateProps> = ({
draft,
chat,
chatFullInfo,
user,
replyToTopic,
isForCurrentMessageList,
isCurrentUserPremium,
@ -1758,6 +1759,10 @@ const Composer: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}, [
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<OwnProps>(
editingMessage: selectEditingMessage(global, chatId, threadId, messageListType),
draft,
chat,
user,
isChatWithBot,
isChatWithSelf,
isForCurrentMessageList,

View File

@ -204,6 +204,8 @@ const Chat: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onReorderAnimationEnd,
topicIds: listedTopicIds,
hasTags: shouldRenderTags,
shouldForceNonForumView,
});
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
@ -255,7 +258,7 @@ const Chat: FC<OwnProps & StateProps> = ({
return;
}
const noForumTopicPanel = isMobile && isForumAsMessages;
const noForumTopicPanel = (isMobile && isForumAsMessages) || shouldForceNonForumView;
if (isMobile) {
setShouldCloseRightColumn({ value: true });
@ -288,7 +291,7 @@ const Chat: FC<OwnProps & StateProps> = ({
openForumPanel({ chatId }, { forceOnHeavyAnimation: true });
}
if (!isForumAsMessages) return;
if (!isForumAsMessages && !shouldForceNonForumView) return;
}
}
@ -397,7 +400,7 @@ const Chat: FC<OwnProps & StateProps> = ({
const chatClassName = buildClassName(
'Chat chat-item-clickable',
isUserId(chatId) ? 'private' : 'group',
isForum && 'forum',
isForum && !shouldForceNonForumView && 'forum',
isSelected && 'selected',
isSelectedForum && 'selected-forum',
isPreview && 'standalone',

View File

@ -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 (
<ChatForumLastMessage
chat={chat}

View File

@ -114,6 +114,7 @@ type StateProps = {
isMuted?: boolean;
isTopic?: boolean;
isForum?: boolean;
isBotForum?: boolean;
isForumAsMessages?: true;
canAddContact?: boolean;
canDeleteChat?: boolean;
@ -148,6 +149,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
withForumActions,
isTopic,
isForum,
isBotForum,
isForumAsMessages,
isChatInfoShown,
canStartBot,
@ -627,7 +629,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{oldLang('lng_forum_topic_edit')}
</MenuItem>
)}
{isMobile && !withForumActions && isForum && !isTopic && (
{isMobile && !withForumActions && isForum && !isBotForum && !isTopic && (
<MenuItem
icon="forums"
onClick={handleViewAsTopicsClick}
@ -644,7 +646,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
<div className="right-badge">{pendingJoinRequests}</div>
</MenuItem>
)}
{withForumActions && !isTopic && !isForumAsMessages && (
{withForumActions && !isTopic && !isBotForum && !isForumAsMessages && (
<MenuItem
icon="message"
onClick={handleOpenAsMessages}
@ -883,6 +885,7 @@ export default memo(withGlobal<OwnProps>(
isPrivate,
isTopic: chat?.isForum && !isMainThread,
isForum: chat?.isForum,
isBotForum: chat?.isBotForum,
isForumAsMessages: chat?.isForumAsMessages,
canAddContact,
canDeleteChat: getCanDeleteChat(chat),

View File

@ -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<OwnProps>(
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<OwnProps>(
canTranslate,
translationLanguage,
shouldAutoTranslate,
isBotForum: chat.isBotForum,
canManageBotForumTopics: chat.isBotForum && user?.canManageBotForumTopics,
shouldScrollToBottom,
};
},

View File

@ -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 (
<div className={buildClassName('local-action-message', actionMessageStyles.root)} key="botforum-new-topic">
<div className={actionMessageStyles.contentBox}>

View File

@ -392,6 +392,7 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
}
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<void>
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<void>
}
// 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', {

View File

@ -27,7 +27,7 @@ const SAFE_MIN_PROPERTIES: (keyof ApiTopic)[] = [
export function updateTopicsInfo<T extends GlobalState>(
global: T, chatId: string, update: Partial<TopicsInfo>,
) {
const info = global.chats.topicsInfoById[chatId] || {};
const info = global.chats.topicsInfoById[chatId] || { topicsById: {} };
global = {
...global,

View File

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

View File

@ -1170,6 +1170,7 @@ export interface LangPair {
'ComposerPlaceholderBroadcast': undefined;
'ComposerPlaceholderBroadcastSilent': undefined;
'ComposerPlaceholderTopicGeneral': undefined;
'ComposerPlaceholderBotTopicGeneral': undefined;
'ComposerStoryPlaceholderLocked': undefined;
'ComposerPlaceholderNoText': undefined;
'AriaComposerCancelVoice': undefined;