Handle bot command clicks in Replies / Scheduled.

This commit is contained in:
John Preston 2020-11-10 19:38:21 +03:00
parent ac02e2be9e
commit cf6ca3b1ac
25 changed files with 210 additions and 73 deletions

View File

@ -180,20 +180,14 @@ auto CashtagClickHandler::getTextEntity() const -> TextEntity {
return { EntityType::Cashtag };
}
PeerData *BotCommandClickHandler::_peer = nullptr;
UserData *BotCommandClickHandler::_bot = nullptr;
void BotCommandClickHandler::onClick(ClickContext context) const {
const auto button = context.button;
if (button == Qt::LeftButton || button == Qt::MiddleButton) {
if (auto peer = peerForCommand()) {
if (auto bot = peer->isUser() ? peer->asUser() : botForCommand()) {
Ui::showPeerHistory(peer, ShowAtTheEndMsgId);
App::sendBotCommand(peer, bot, _cmd);
return;
}
}
if (auto peer = Ui::getPeerForMouseAction()) { // old way
const auto my = context.other.value<ClickHandlerContext>();
if (const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr) {
delegate->elementSendBotCommand(_cmd, my.itemId);
return;
} else if (auto peer = Ui::getPeerForMouseAction()) { // old way
auto bot = peer->isUser() ? peer->asUser() : nullptr;
if (!bot) {
if (const auto view = App::hoveredLinkItem()) {

View File

@ -13,8 +13,18 @@ namespace Main {
class Session;
} // namespace Main
namespace HistoryView {
class ElementDelegate;
} // namespace HistoryView
[[nodiscard]] bool UrlRequiresConfirmation(const QUrl &url);
struct ClickHandlerContext {
FullMsgId itemId;
Fn<HistoryView::ElementDelegate*()> elementDelegate;
};
Q_DECLARE_METATYPE(ClickHandlerContext);
class HiddenUrlClickHandler : public UrlClickHandler {
public:
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
@ -165,30 +175,14 @@ public:
return _cmd;
}
static void setPeerForCommand(PeerData *peer) {
_peer = peer;
}
static void setBotForCommand(UserData *bot) {
_bot = bot;
}
TextEntity getTextEntity() const override;
protected:
QString url() const override {
return _cmd;
}
static PeerData *peerForCommand() {
return _peer;
}
static UserData *botForCommand() {
return _bot;
}
private:
QString _cmd;
static PeerData *_peer;
static UserData *_bot;
};

View File

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "core/update_checker.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "boxes/confirm_phone_box.h"
#include "boxes/background_preview_box.h"
#include "boxes/confirm_box.h"
@ -76,11 +77,11 @@ bool ShowTheme(
if (!controller) {
return false;
}
const auto clickFromMessageId = context.value<FullMsgId>();
const auto fromMessageId = context.value<ClickHandlerContext>().itemId;
Core::App().hideMediaView();
controller->session().data().cloudThemes().resolve(
match->captured(1),
clickFromMessageId);
fromMessageId);
return true;
}
@ -280,7 +281,7 @@ bool ResolveUsername(
startToken = gameParam;
post = ShowAtGameShareMsgId;
}
const auto clickFromMessageId = context.value<FullMsgId>();
const auto fromMessageId = context.value<ClickHandlerContext>().itemId;
using Navigation = Window::SessionNavigation;
controller->showPeerByLink(Navigation::PeerByLinkInfo{
.usernameOrId = domain,
@ -295,7 +296,7 @@ bool ResolveUsername(
}
: Navigation::RepliesByLinkInfo{ v::null },
.startToken = startToken,
.clickFromMessageId = clickFromMessageId,
.clickFromMessageId = fromMessageId,
});
return true;
}
@ -319,7 +320,7 @@ bool ResolvePrivatePost(
if (!channelId || !IsServerMsgId(msgId)) {
return false;
}
const auto clickFromMessageId = context.value<FullMsgId>();
const auto fromMessageId = context.value<ClickHandlerContext>().itemId;
using Navigation = Window::SessionNavigation;
controller->showPeerByLink(Navigation::PeerByLinkInfo{
.usernameOrId = channelId,
@ -333,7 +334,7 @@ bool ResolvePrivatePost(
Navigation::ThreadId{ threadId }
}
: Navigation::RepliesByLinkInfo{ v::null },
.clickFromMessageId = clickFromMessageId,
.clickFromMessageId = fromMessageId,
});
return true;
}

View File

@ -65,7 +65,7 @@ QString UiIntegration::timeFormat() {
std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
const EntityLinkData &data,
const std::any &context) {
const auto my = std::any_cast<Context>(&context);
const auto my = std::any_cast<MarkedTextContext>(&context);
switch (data.type) {
case EntityType::Url:
return (!data.data.isEmpty()
@ -82,6 +82,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<BotCommandClickHandler>(data.data);
case EntityType::Hashtag:
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
if (my && my->type == HashtagMentionType::Twitter) {
return std::make_shared<UrlClickHandler>(
(qsl("https://twitter.com/hashtag/")
@ -101,6 +102,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<CashtagClickHandler>(data.data);
case EntityType::Mention:
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
if (my && my->type == HashtagMentionType::Twitter) {
return std::make_shared<UrlClickHandler>(
qsl("https://twitter.com/") + data.data.mid(1),

View File

@ -13,20 +13,25 @@ namespace Main {
class Session;
} // namespace Main
namespace HistoryView {
class ElementDelegate;
} // namespace HistoryView
namespace Core {
class UiIntegration : public Ui::Integration {
public:
struct MarkedTextContext {
enum class HashtagMentionType : uchar {
Telegram,
Twitter,
Instagram,
};
struct Context {
Main::Session *session = nullptr;
HashtagMentionType type = HashtagMentionType::Telegram;
};
Main::Session *session = nullptr;
HashtagMentionType type = HashtagMentionType::Telegram;
};
class UiIntegration : public Ui::Integration {
public:
void postponeCall(FnMut<void()> &&callable) override;
void registerLeaveSubscription(not_null<QWidget*> widget) override;
void unregisterLeaveSubscription(not_null<QWidget*> widget) override;

View File

@ -600,6 +600,11 @@ bool InnerWidget::elementShownUnread(not_null<const Element*> view) {
return view->data()->unread();
}
void InnerWidget::elementSendBotCommand(
const QString &command,
const FullMsgId &context) {
}
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
memento->setFilter(std::move(_filter));
memento->setAdmins(std::move(_admins));

View File

@ -116,6 +116,9 @@ public:
not_null<const HistoryView::Element*> view) override;
bool elementShownUnread(
not_null<const HistoryView::Element*> view) override;
void elementSendBotCommand(
const QString &command,
const FullMsgId &context) override;
~InnerWidget();

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <rpl/merge.h>
#include "core/file_utilities.h"
#include "core/crash_reports.h"
#include "core/click_handler_types.h"
#include "history/history.h"
#include "history/history_message.h"
#include "history/view/media/history_view_media.h"
@ -1330,7 +1331,14 @@ void HistoryInner::mouseActionFinish(
: FullMsgId();
ActivateClickHandler(window(), activated, {
button,
QVariant::fromValue(pressedItemId)
QVariant::fromValue(ClickHandlerContext{
.itemId = pressedItemId,
.elementDelegate = [weak = Ui::MakeWeak(this)] {
return weak
? HistoryInner::ElementDelegate().get()
: nullptr;
},
})
});
return;
}
@ -2540,6 +2548,24 @@ bool HistoryInner::elementIsGifPaused() {
return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
}
void HistoryInner::elementSendBotCommand(
const QString &command,
const FullMsgId &context) {
if (auto peer = Ui::getPeerForMouseAction()) { // old way
auto bot = peer->isUser() ? peer->asUser() : nullptr;
if (!bot) {
if (const auto view = App::hoveredLinkItem()) {
// may return nullptr
bot = view->data()->fromOriginal()->asUser();
}
}
Ui::showPeerHistory(peer, ShowAtTheEndMsgId);
App::sendBotCommand(peer, bot, command);
} else {
App::insertBotCommand(command);
}
}
auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {};
@ -3434,6 +3460,13 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
bool elementShownUnread(not_null<const Element*> view) override {
return view->data()->unread();
}
void elementSendBotCommand(
const QString &command,
const FullMsgId &context) {
if (Instance) {
Instance->elementSendBotCommand(command, context);
}
}
};
static Result result;

View File

@ -92,6 +92,9 @@ public:
const TextWithEntities &text,
Fn<void()> hiddenCallback);
bool elementIsGifPaused();
void elementSendBotCommand(
const QString &command,
const FullMsgId &context);
void updateBotInfo(bool recount = true);

View File

@ -1464,7 +1464,7 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
}
clearIsolatedEmoji();
const auto context = Core::UiIntegration::Context{
const auto context = Core::MarkedTextContext{
.session = &history()->session()
};
_text.setMarkedText(

View File

@ -3405,15 +3405,9 @@ void HistoryWidget::sendBotCommand(
bool lastKeyboardUsed = (_keyboard->forMsgId() == FullMsgId(_channel, _history->lastKeyboardId)) && (_keyboard->forMsgId() == FullMsgId(_channel, replyTo));
QString toSend = cmd;
if (bot && (!bot->isUser() || !bot->asUser()->isBot())) {
bot = nullptr;
}
QString username = bot ? bot->asUser()->username : QString();
int32 botStatus = _peer->isChat() ? _peer->asChat()->botStatus : (_peer->isMegagroup() ? _peer->asChannel()->mgInfo->botStatus : -1);
if (!replyTo && toSend.indexOf('@') < 2 && !username.isEmpty() && (botStatus == 0 || botStatus == 2)) {
toSend += '@' + username;
}
const auto toSend = replyTo
? cmd
: HistoryView::WrapBotCommandInChat(_peer, cmd, bot);
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { toSend, TextWithTags::Tags() };

View File

@ -121,6 +121,11 @@ bool SimpleElementDelegate::elementShownUnread(
return view->data()->unread();
}
void SimpleElementDelegate::elementSendBotCommand(
const QString &command,
const FullMsgId &context) {
}
TextSelection UnshiftItemSelection(
TextSelection selection,
uint16 byLength) {

View File

@ -67,6 +67,9 @@ public:
virtual bool elementIsGifPaused() = 0;
virtual bool elementHideReply(not_null<const Element*> view) = 0;
virtual bool elementShownUnread(not_null<const Element*> view) = 0;
virtual void elementSendBotCommand(
const QString &command,
const FullMsgId &context) = 0;
};
@ -99,6 +102,9 @@ public:
bool elementIsGifPaused() override;
bool elementHideReply(not_null<const Element*> view) override;
bool elementShownUnread(not_null<const Element*> view) override;
void elementSendBotCommand(
const QString &command,
const FullMsgId &context) override;
private:
const not_null<Window::SessionController*> _controller;

View File

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwindow.h"
#include "mainwidget.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "apiwrap.h"
#include "layout.h"
#include "window/window_session_controller.h"
@ -37,6 +38,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_media_types.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "facades.h"
#include "styles/style_chat.h"
@ -1290,6 +1294,12 @@ bool ListWidget::elementShownUnread(not_null<const Element*> view) {
return _delegate->listElementShownUnread(view);
}
void ListWidget::elementSendBotCommand(
const QString &command,
const FullMsgId &context) {
return _delegate->listSendBotCommand(command, context);
}
void ListWidget::saveState(not_null<ListMemento*> memento) {
memento->setAroundPosition(_aroundPosition);
auto state = countScrollState();
@ -2186,7 +2196,14 @@ void ListWidget::mouseActionFinish(
mouseActionCancel();
ActivateClickHandler(window(), activated, {
button,
QVariant::fromValue(pressState.itemId)
QVariant::fromValue(ClickHandlerContext{
.itemId = pressState.itemId,
.elementDelegate = [weak = Ui::MakeWeak(this)] {
return weak
? (ElementDelegate*)weak
: nullptr;
},
})
});
return;
}
@ -2786,4 +2803,34 @@ void ConfirmSendNowSelectedItems(not_null<ListWidget*> widget) {
[=] { navigation->showBackFromStack(); });
}
QString WrapBotCommandInChat(
not_null<PeerData*> peer,
const QString &command,
const FullMsgId &context) {
auto result = command;
if (const auto item = peer->owner().message(context)) {
if (const auto user = item->fromOriginal()->asUser()) {
return WrapBotCommandInChat(peer, command, user);
}
}
return result;
}
QString WrapBotCommandInChat(
not_null<PeerData*> peer,
const QString &command,
not_null<UserData*> bot) {
if (!bot->isBot() || bot->username.isEmpty()) {
return command;
}
const auto botStatus = peer->isChat()
? peer->asChat()->botStatus
: peer->isMegagroup()
? peer->asChannel()->mgInfo->botStatus
: -1;
return ((command.indexOf('@') < 2) && (botStatus == 0 || botStatus == 2))
? command + '@' + bot->username
: command;
}
} // namespace HistoryView

View File

@ -87,6 +87,9 @@ public:
virtual bool listElementShownUnread(not_null<const Element*> view) = 0;
virtual bool listIsGoodForAroundPosition(
not_null<const Element*> view) = 0;
virtual void listSendBotCommand(
const QString &command,
const FullMsgId &context) = 0;
};
@ -233,6 +236,9 @@ public:
bool elementIsGifPaused() override;
bool elementHideReply(not_null<const Element*> view) override;
bool elementShownUnread(not_null<const Element*> view) override;
void elementSendBotCommand(
const QString &command,
const FullMsgId &context) override;
~ListWidget();
@ -556,4 +562,13 @@ void ConfirmDeleteSelectedItems(not_null<ListWidget*> widget);
void ConfirmForwardSelectedItems(not_null<ListWidget*> widget);
void ConfirmSendNowSelectedItems(not_null<ListWidget*> widget);
[[nodiscard]] QString WrapBotCommandInChat(
not_null<PeerData*> peer,
const QString &command,
const FullMsgId &context);
[[nodiscard]] QString WrapBotCommandInChat(
not_null<PeerData*> peer,
const QString &command,
not_null<UserData*> bot);
} // namespace HistoryView

View File

@ -639,6 +639,11 @@ bool PinnedWidget::listIsGoodForAroundPosition(
return IsServerMsgId(view->data()->id);
}
void PinnedWidget::listSendBotCommand(
const QString &command,
const FullMsgId &context) {
}
void PinnedWidget::confirmDeleteSelected() {
ConfirmDeleteSelectedItems(_inner);
}

View File

@ -93,6 +93,9 @@ public:
bool listElementHideReply(not_null<const Element*> view) override;
bool listElementShownUnread(not_null<const Element*> view) override;
bool listIsGoodForAroundPosition(not_null<const Element*> view) override;
void listSendBotCommand(
const QString &command,
const FullMsgId &context) override;
protected:
void resizeEvent(QResizeEvent *e) override;

View File

@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_replies_list.h"
#include "data/data_changes.h"
@ -445,11 +446,7 @@ void RepliesWidget::setupComposeControls() {
if (showSlowmodeError()) {
return;
}
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { command };
message.action.replyTo = replyToId();
session().api().sendMessage(std::move(message));
finishSending();
listSendBotCommand(command, FullMsgId());
}, lifetime());
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
@ -1766,6 +1763,17 @@ bool RepliesWidget::listIsGoodForAroundPosition(
return IsServerMsgId(view->data()->id);
}
void RepliesWidget::listSendBotCommand(
const QString &command,
const FullMsgId &context) {
const auto text = WrapBotCommandInChat(_history->peer, command, context);
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { text };
message.action.replyTo = replyToId();
session().api().sendMessage(std::move(message));
finishSending();
}
void RepliesWidget::confirmDeleteSelected() {
ConfirmDeleteSelectedItems(_inner);
}

View File

@ -126,6 +126,9 @@ public:
bool listElementHideReply(not_null<const Element*> view) override;
bool listElementShownUnread(not_null<const Element*> view) override;
bool listIsGoodForAroundPosition(not_null<const Element*> view) override;
void listSendBotCommand(
const QString &command,
const FullMsgId &context) override;
protected:
void resizeEvent(QResizeEvent *e) override;

View File

@ -182,15 +182,7 @@ void ScheduledWidget::setupComposeControls() {
_composeControls->sendCommandRequests(
) | rpl::start_with_next([=](const QString &command) {
const auto callback = [=](Api::SendOptions options) {
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { command };
message.action.options = options;
session().api().sendMessage(std::move(message));
};
Ui::show(
PrepareScheduleBox(this, sendMenuType(), callback),
Ui::LayerOption::KeepOther);
listSendBotCommand(command, FullMsgId());
}, lifetime());
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
@ -1185,6 +1177,21 @@ bool ScheduledWidget::listIsGoodForAroundPosition(
return true;
}
void ScheduledWidget::listSendBotCommand(
const QString &command,
const FullMsgId &context) {
const auto callback = [=](Api::SendOptions options) {
const auto text = WrapBotCommandInChat(_history->peer, command, context);
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { text };
message.action.options = options;
session().api().sendMessage(std::move(message));
};
Ui::show(
PrepareScheduleBox(this, sendMenuType(), callback),
Ui::LayerOption::KeepOther);
}
void ScheduledWidget::confirmSendNowSelected() {
ConfirmSendNowSelectedItems(_inner);
}

View File

@ -110,6 +110,9 @@ public:
bool listElementHideReply(not_null<const Element*> view) override;
bool listElementShownUnread(not_null<const Element*> view) override;
bool listIsGoodForAroundPosition(not_null<const Element *> view) override;
void listSendBotCommand(
const QString &command,
const FullMsgId &context) override;
protected:
void resizeEvent(QResizeEvent *e) override;

View File

@ -33,7 +33,7 @@ Game::Game(
, _title(st::msgMinWidth - st::webPageLeft)
, _description(st::msgMinWidth - st::webPageLeft) {
if (!consumed.text.isEmpty()) {
const auto context = Core::UiIntegration::Context{
const auto context = Core::MarkedTextContext{
.session = &history()->session()
};
_description.setMarkedText(
@ -418,7 +418,7 @@ void Game::parentTextUpdated() {
if (const auto media = _parent->data()->media()) {
const auto consumed = media->consumedMessageText();
if (!consumed.text.isEmpty()) {
const auto context = Core::UiIntegration::Context{
const auto context = Core::MarkedTextContext{
.session = &history()->session()
};
_description.setMarkedText(

View File

@ -139,7 +139,7 @@ Ui::Text::String Media::createCaption(
- st::msgPadding.left()
- st::msgPadding.right();
auto result = Ui::Text::String(minResizeWidth);
const auto context = Core::UiIntegration::Context{
const auto context = Core::MarkedTextContext{
.session = &history()->session()
};
result.setMarkedText(

View File

@ -201,11 +201,12 @@ QSize WebPage::countOptimalSize() {
- st::msgPadding.right()
- st::webPageLeft);
}
auto context = Core::UiIntegration::Context();
auto context = Core::MarkedTextContext();
using MarkedTextContext = Core::MarkedTextContext;
if (_data->siteName == qstr("Twitter")) {
context.type = Core::UiIntegration::HashtagMentionType::Twitter;
context.type = MarkedTextContext::HashtagMentionType::Twitter;
} else if (_data->siteName == qstr("Instagram")) {
context.type = Core::UiIntegration::HashtagMentionType::Instagram;
context.type = MarkedTextContext::HashtagMentionType::Instagram;
}
_description.setMarkedText(
st::webPageDescriptionStyle,

View File

@ -2018,7 +2018,7 @@ void OverlayWidget::refreshCaption(HistoryItem *item) {
const auto base = duration
? DocumentTimestampLinkBase(_document, item->fullId())
: QString();
const auto context = Core::UiIntegration::Context{
const auto context = Core::MarkedTextContext{
.session = &item->history()->session()
};
_caption.setMarkedText(