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

View File

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

View File

@ -518,6 +518,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
const isEmptyTopic = messageIds?.length === 1 const isEmptyTopic = messageIds?.length === 1
&& messagesById?.[messageIds[0]]?.content.action?.type === 'topicCreate'; && messagesById?.[messageIds[0]]?.content.action?.type === 'topicCreate';
const isBotInfoEmpty = botInfo && !botInfo.description && !botInfo.gif && !botInfo.photo;
const className = buildClassName( const className = buildClassName(
'MessageList custom-scroll', 'MessageList custom-scroll',
noAvatars && 'no-avatars', noAvatars && 'no-avatars',
@ -546,7 +548,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
) : botInfo ? ( ) : botInfo ? (
<div className="empty"> <div className="empty">
{isLoadingBotInfo && <span>{lang('Loading')}</span>} {isLoadingBotInfo && <span>{lang('Loading')}</span>}
{!botInfo && !isLoadingBotInfo && <span>{lang('NoMessages')}</span>} {isBotInfoEmpty && !isLoadingBotInfo && <span>{lang('NoMessages')}</span>}
{botInfo && ( {botInfo && (
<div <div
className="bot-info" 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 chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
const isChatWithBot = Boolean(chatBot); const isChatWithBot = Boolean(chatBot);
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId); const messageWithActualBotKeyboard = selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId);
const scheduledIds = selectScheduledIds(global, chatId, threadId); const scheduledIds = selectScheduledIds(global, chatId, threadId);
const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = global.settings.byKey; const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = global.settings.byKey;
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG]; 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 => { addActionHandler('sendBotCommand', (global, actions, payload): ActionReturnType => {
const { command, chatId, tabId = getCurrentTabId() } = payload; const { command, chatId, tabId = getCurrentTabId() } = payload;
const { currentUserId } = global;
const chat = chatId ? selectChat(global, chatId) : selectCurrentChat(global, tabId); const chat = chatId ? selectChat(global, chatId) : selectCurrentChat(global, tabId);
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentUserId || !chat || !currentMessageList) { if (!chat || !currentMessageList) {
return; return;
} }
@ -188,7 +187,7 @@ addActionHandler('sendBotCommand', (global, actions, payload): ActionReturnType
actions.clearWebPagePreview({ tabId }); actions.clearWebPagePreview({ tabId });
void sendBotCommand( 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 = getGlobal();
global = removeBlockedContact(global, bot.id); global = removeBlockedContact(global, bot.id);
setGlobal(global); 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> => { addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
@ -927,10 +926,11 @@ async function searchInlineBot<T extends GlobalState>(global: T, {
} }
async function sendBotCommand( 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', { await callApi('sendMessage', {
chat, chat,
replyingToTopId: threadId,
text: command, text: command,
replyingTo, replyingTo,
sendAs, sendAs,

View File

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