Bots: Support reply keyboard in groups and forums (#2477)

This commit is contained in:
Alexander Zinchuk 2023-02-03 03:22:10 +01:00
parent b07be031a9
commit 172f03f474
6 changed files with 65 additions and 23 deletions

View File

@ -176,13 +176,15 @@ export function buildApiMessageWithChatId(
} = mtpMessage.replyTo || {};
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
const {
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse,
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
} = buildReplyButtons(mtpMessage, isInvoiceMedia) || {};
const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf);
const { replies, mediaUnread: isMediaUnread, postAuthor } = mtpMessage;
const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId);
const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker);
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
const isHideKeyboardSelective = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide
&& mtpMessage.replyMarkup.selective;
const isProtected = mtpMessage.noforwards || isInvoiceMedia;
const isForwardingAllowed = !mtpMessage.noforwards;
const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId);
@ -215,8 +217,10 @@ export function buildApiMessageWithChatId(
isInAlbum,
}),
inlineButtons,
...(keyboardButtons && { keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse }),
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons }),
...(keyboardButtons && {
keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
}),
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons, isHideKeyboardSelective }),
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
...(replies?.comments && { repliesThreadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
...(postAuthor && { postAuthorTitle: postAuthor }),
@ -1242,6 +1246,7 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool
...(replyMarkup instanceof GramJs.ReplyKeyboardMarkup && {
keyboardPlaceholder: replyMarkup.placeholder,
isKeyboardSingleUse: replyMarkup.singleUse,
isKeyboardSelective: replyMarkup.selective,
}),
};
}

View File

@ -413,11 +413,13 @@ export interface ApiMessage {
keyboardButtons?: ApiKeyboardButtons;
keyboardPlaceholder?: string;
isKeyboardSingleUse?: boolean;
isKeyboardSelective?: boolean;
viaBotId?: string;
repliesThreadInfo?: ApiThreadInfo;
postAuthorTitle?: string;
isScheduled?: boolean;
shouldHideKeyboardButtons?: boolean;
isHideKeyboardSelective?: boolean;
isFromScheduled?: boolean;
isSilent?: boolean;
seenByUserIds?: string[];
@ -600,6 +602,7 @@ export type ApiKeyboardButtons = ApiKeyboardButton[][];
export type ApiReplyKeyboard = {
keyboardPlaceholder?: string;
isKeyboardSingleUse?: boolean;
isKeyboardSelective?: boolean;
} & {
[K in 'inlineButtons' | 'keyboardButtons']?: ApiKeyboardButtons;
};

View File

@ -518,6 +518,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
const isEmptyTopic = messageIds?.length === 1
&& messagesById?.[messageIds[0]]?.content.action?.type === 'topicCreate';
const isBotInfoEmpty = botInfo && !botInfo.description && !botInfo.gif && !botInfo.photo;
const className = buildClassName(
'MessageList custom-scroll',
noAvatars && 'no-avatars',
@ -546,7 +548,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
) : botInfo ? (
<div className="empty">
{isLoadingBotInfo && <span>{lang('Loading')}</span>}
{!botInfo && !isLoadingBotInfo && <span>{lang('NoMessages')}</span>}
{isBotInfoEmpty && !isLoadingBotInfo && <span>{lang('NoMessages')}</span>}
{botInfo && (
<div
className="bot-info"

View File

@ -1526,7 +1526,7 @@ export default memo(withGlobal<OwnProps>(
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
const isChatWithBot = Boolean(chatBot);
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
const messageWithActualBotKeyboard = selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId);
const scheduledIds = selectScheduledIds(global, chatId, threadId);
const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = global.settings.byKey;
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];

View File

@ -175,11 +175,10 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur
addActionHandler('sendBotCommand', (global, actions, payload): ActionReturnType => {
const { command, chatId, tabId = getCurrentTabId() } = payload;
const { currentUserId } = global;
const chat = chatId ? selectChat(global, chatId) : selectCurrentChat(global, tabId);
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentUserId || !chat || !currentMessageList) {
if (!chat || !currentMessageList) {
return;
}
@ -188,7 +187,7 @@ addActionHandler('sendBotCommand', (global, actions, payload): ActionReturnType
actions.clearWebPagePreview({ tabId });
void sendBotCommand(
chat, currentUserId, command, selectReplyingToId(global, chat.id, threadId), selectSendAs(global, chat.id),
chat, threadId, command, selectReplyingToId(global, chat.id, threadId), selectSendAs(global, chat.id),
);
});
@ -209,7 +208,7 @@ addActionHandler('restartBot', async (global, actions, payload): Promise<void> =
global = getGlobal();
global = removeBlockedContact(global, bot.id);
setGlobal(global);
void sendBotCommand(chat, currentUserId, '/start', undefined, selectSendAs(global, chatId));
void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId));
});
addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
@ -927,10 +926,11 @@ async function searchInlineBot<T extends GlobalState>(global: T, {
}
async function sendBotCommand(
chat: ApiChat, currentUserId: string, command: string, replyingTo?: number, sendAs?: ApiChat | ApiUser,
chat: ApiChat, threadId = MAIN_THREAD_ID, command: string, replyingTo?: number, sendAs?: ApiChat | ApiUser,
) {
await callApi('sendMessage', {
chat,
replyingToTopId: threadId,
text: command,
replyingTo,
sendAs,

View File

@ -15,7 +15,7 @@ import {
GENERAL_TOPIC_ID, LOCAL_MESSAGE_MIN_ID, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
} from '../../config';
import {
selectChat, selectChatBot, selectIsChatWithBot, selectIsChatWithSelf,
selectChat, selectChatBot, selectIsChatWithSelf,
} from './chats';
import {
selectIsCurrentUserPremium, selectIsUserOrChatContact, selectUser, selectUserStatus,
@ -921,7 +921,7 @@ export function selectSelectedMessagesCount<T extends GlobalState>(
}
export function selectNewestMessageWithBotKeyboardButtons<T extends GlobalState>(
global: T, chatId: string,
global: T, chatId: string, threadId = MAIN_THREAD_ID,
...[tabId = getCurrentTabId()]: TabArgs<T>
): ApiMessage | undefined {
const chat = selectChat(global, chatId);
@ -929,23 +929,15 @@ export function selectNewestMessageWithBotKeyboardButtons<T extends GlobalState>
return undefined;
}
if (!selectIsChatWithBot(global, chat)) {
return undefined;
}
const chatMessages = selectChatMessages(global, chatId);
const viewportIds = selectViewportIds(global, chatId, MAIN_THREAD_ID, tabId);
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
if (!chatMessages || !viewportIds) {
return undefined;
}
const messageId = findLast(viewportIds, (id) => {
return !chatMessages[id].isOutgoing && Boolean(chatMessages[id].keyboardButtons);
});
const messageId = findLast(viewportIds, (id) => selectShouldDisplayReplyKeyboard(global, chatMessages[id]));
const replyHideMessageId = findLast(viewportIds, (id) => {
return Boolean(chatMessages[id].shouldHideKeyboardButtons);
});
const replyHideMessageId = findLast(viewportIds, (id) => selectShouldHideReplyKeyboard(global, chatMessages[id]));
if (messageId && replyHideMessageId && replyHideMessageId > messageId) {
return undefined;
@ -954,6 +946,46 @@ export function selectNewestMessageWithBotKeyboardButtons<T extends GlobalState>
return messageId ? chatMessages[messageId] : undefined;
}
function selectShouldHideReplyKeyboard<T extends GlobalState>(global: T, message: ApiMessage) {
const {
shouldHideKeyboardButtons,
isHideKeyboardSelective,
replyToMessageId,
isMentioned,
} = message;
if (!shouldHideKeyboardButtons) return false;
if (isHideKeyboardSelective) {
if (isMentioned) return true;
if (!replyToMessageId) return false;
const replyMessage = selectChatMessage(global, message.chatId, replyToMessageId);
return Boolean(replyMessage?.senderId === global.currentUserId);
}
return true;
}
function selectShouldDisplayReplyKeyboard<T extends GlobalState>(global: T, message: ApiMessage) {
const {
keyboardButtons,
shouldHideKeyboardButtons,
isKeyboardSelective,
isMentioned,
replyToMessageId,
} = message;
if (!keyboardButtons || shouldHideKeyboardButtons) return false;
if (isKeyboardSelective) {
if (isMentioned) return true;
if (!replyToMessageId) return false;
const replyMessage = selectChatMessage(global, message.chatId, replyToMessageId);
return Boolean(replyMessage?.senderId === global.currentUserId);
}
return true;
}
export function selectCanAutoLoadMedia<T extends GlobalState>(global: T, message: ApiMessage) {
const chat = selectChat(global, message.chatId);
if (!chat) {