diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 21bfa0489..b74b2147a 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -1631,7 +1631,7 @@ messages.getUnreadReactions#e85bae1a peer:InputPeer offset_id:int add_offset:int messages.readReactions#82e251d7 peer:InputPeer = messages.AffectedHistory; messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; -messages.addBotToAttachMenu#f319bd48 bot:InputUser = Bool; +messages.toggleBotInAttachMenu#1aee33af bot:InputUser enabled:Bool = Bool; messages.requestWebView#a17c10db flags:# peer:InputPeer bot:InputUser url:flags.0?string theme_params:flags.1?DataJSON = WebViewResult; messages.setWebViewResult#e41cd11d query_id:long result:InputBotInlineResult = Bool; messages.getWebViewResult#22b6c214 peer:InputPeer bot:InputUser query_id:long = messages.WebViewResult; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index c6546de9d..35dd3ee18 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1021,8 +1021,17 @@ void HistoryWidget::initTabbedSelector() { sendExistingPhoto(data.photo, data.options); }, lifetime()); - selector->inlineResultChosen( - ) | filter | rpl::start_with_next([=](Selector::InlineChosen data) { + rpl::merge( + selector->inlineResultChosen(), + controller()->inlineResultConfirmed() + ) | filter | rpl::filter([=](const Selector::InlineChosen &data) { + if (!data.recipientOverride) { + return true; + } else if (data.recipientOverride != _peer) { + showHistory(data.recipientOverride->id, ShowAtTheEndMsgId); + } + return (data.recipientOverride == _peer); + }) | rpl::start_with_next([=](Selector::InlineChosen data) { sendInlineResult(data); }, lifetime()); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index e95b6ddf7..c92801bca 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -132,6 +132,7 @@ private: struct ResultSelected { not_null result; not_null bot; + PeerData *recipientOverride = nullptr; Api::SendOptions options; Ui::MessageSendingAnimationFrom messageSendingFrom; // Open in OverlayWidget; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp new file mode 100644 index 000000000..a2ee10388 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -0,0 +1,437 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/chat/attach/attach_bot_webview.h" + +#include "core/file_utilities.h" +#include "ui/effects/radial_animation.h" +#include "ui/layers/box_content.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/separate_panel.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/fade_wrap.h" +#include "lang/lang_keys.h" +#include "webview/webview_embed.h" +#include "webview/webview_interface.h" +#include "base/debug_log.h" +#include "styles/style_payments.h" +#include "styles/style_layers.h" + +#include +#include +#include + +namespace Ui::BotWebView { +namespace { + +constexpr auto kProgressDuration = crl::time(200); +constexpr auto kProgressOpacity = 0.3; + +} // namespace + +struct Panel::Progress { + Progress(QWidget *parent, Fn rect); + + RpWidget widget; + InfiniteRadialAnimation animation; + Animations::Simple shownAnimation; + bool shown = true; + rpl::lifetime geometryLifetime; +}; + +struct Panel::WebviewWithLifetime { + WebviewWithLifetime( + QWidget *parent = nullptr, + Webview::WindowConfig config = Webview::WindowConfig()); + + Webview::Window window; + QPointer lastHidingBox; + rpl::lifetime lifetime; +}; + +Panel::WebviewWithLifetime::WebviewWithLifetime( + QWidget *parent, + Webview::WindowConfig config) +: window(parent, std::move(config)) { +} + +Panel::Progress::Progress(QWidget *parent, Fn rect) +: widget(parent) +, animation( + [=] { if (!anim::Disabled()) widget.update(rect()); }, + st::paymentsLoading) { +} + +Panel::Panel(const QString &userDataPath, Fn send, Fn close) +: _userDataPath(userDataPath) +, _send(std::move(send)) +, _close(std::move(close)) +, _widget(std::make_unique()) { + _widget->setInnerSize(st::paymentsPanelSize); + _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); + + _widget->closeRequests( + ) | rpl::start_with_next(close, _widget->lifetime()); + + _widget->closeEvents( + ) | rpl::start_with_next(close, _widget->lifetime()); + + setTitle(rpl::single(u"Title"_q)); +} + +Panel::~Panel() { + _webview = nullptr; + _progress = nullptr; + _widget = nullptr; +} + +void Panel::requestActivate() { + _widget->showAndActivate(); +} + +void Panel::toggleProgress(bool shown) { + if (!_progress) { + if (!shown) { + return; + } + _progress = std::make_unique( + _widget.get(), + [=] { return progressRect(); }); + _progress->widget.paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(&_progress->widget); + p.setOpacity( + _progress->shownAnimation.value(_progress->shown ? 1. : 0.)); + auto thickness = st::paymentsLoading.thickness; + if (progressWithBackground()) { + auto color = st::windowBg->c; + color.setAlphaF(kProgressOpacity); + p.fillRect(clip, color); + } + const auto rect = progressRect().marginsRemoved( + { thickness, thickness, thickness, thickness }); + InfiniteRadialAnimation::Draw( + p, + _progress->animation.computeState(), + rect.topLeft(), + rect.size() - QSize(), + _progress->widget.width(), + st::paymentsLoading.color, + thickness); + }, _progress->widget.lifetime()); + _progress->widget.show(); + _progress->animation.start(); + } else if (_progress->shown == shown) { + return; + } + const auto callback = [=] { + if (!_progress->shownAnimation.animating() && !_progress->shown) { + _progress = nullptr; + } else { + _progress->widget.update(); + } + }; + _progress->shown = shown; + _progress->shownAnimation.start( + callback, + shown ? 0. : 1., + shown ? 1. : 0., + kProgressDuration); + if (shown) { + setupProgressGeometry(); + } +} + +bool Panel::progressWithBackground() const { + return (_progress->widget.width() == _widget->innerGeometry().width()); +} + +QRect Panel::progressRect() const { + const auto rect = _progress->widget.rect(); + if (!progressWithBackground()) { + return rect; + } + const auto size = st::defaultBoxButton.height; + return QRect( + rect.x() + (rect.width() - size) / 2, + rect.y() + (rect.height() - size) / 2, + size, + size); +} + +void Panel::setupProgressGeometry() { + if (!_progress || !_progress->shown) { + return; + } + _progress->geometryLifetime.destroy(); + if (_webviewBottom) { + _webviewBottom->geometryValue( + ) | rpl::start_with_next([=](QRect bottom) { + const auto height = bottom.height(); + const auto size = st::paymentsLoading.size; + const auto skip = (height - size.height()) / 2; + const auto inner = _widget->innerGeometry(); + const auto right = inner.x() + inner.width(); + const auto top = inner.y() + inner.height() - height; + // This doesn't work, because first we get the correct bottom + // geometry and after that we get the previous event (which + // triggered the 'fire' of correct geometry before getting here). + //const auto right = bottom.x() + bottom.width(); + //const auto top = bottom.y(); + _progress->widget.setGeometry(QRect{ + QPoint(right - skip - size.width(), top + skip), + size }); + }, _progress->geometryLifetime); + } + _progress->widget.show(); + _progress->widget.raise(); + if (_progress->shown) { + _progress->widget.setFocus(); + } +} + +void Panel::showWebviewProgress() { + if (_webviewProgress && _progress && _progress->shown) { + return; + } + _webviewProgress = true; + toggleProgress(true); +} + +void Panel::hideWebviewProgress() { + if (!_webviewProgress) { + return; + } + _webviewProgress = false; + toggleProgress(false); +} + +bool Panel::showWebview( + const QString &url, + rpl::producer bottomText) { + if (!_webview && !createWebview()) { + return false; + } + const auto allowBack = false; + showWebviewProgress(); + _widget->destroyLayer(); + _webview->window.navigate(url); + _widget->setBackAllowed(allowBack); + if (bottomText) { + const auto &padding = st::paymentsPanelPadding; + const auto label = CreateChild( + _webviewBottom.get(), + std::move(bottomText), + st::paymentsWebviewBottom); + const auto height = padding.top() + + label->heightNoMargins() + + padding.bottom(); + rpl::combine( + _webviewBottom->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outerWidth, int width) { + label->move((outerWidth - width) / 2, padding.top()); + }, label->lifetime()); + label->show(); + _webviewBottom->resize(_webviewBottom->width(), height); + } + return true; +} + +bool Panel::createWebview() { + auto container = base::make_unique_q(_widget.get()); + + _webviewBottom = std::make_unique(_widget.get()); + const auto bottom = _webviewBottom.get(); + bottom->show(); + + bottom->heightValue( + ) | rpl::start_with_next([=, raw = container.get()](int height) { + const auto inner = _widget->innerGeometry(); + bottom->move(inner.x(), inner.y() + inner.height() - height); + raw->resize(inner.width(), inner.height() - height); + bottom->resizeToWidth(inner.width()); + }, bottom->lifetime()); + container->show(); + + _webview = std::make_unique( + container.get(), + Webview::WindowConfig{ + .userDataPath = _userDataPath, + }); + const auto raw = &_webview->window; + QObject::connect(container.get(), &QObject::destroyed, [=] { + if (_webview && &_webview->window == raw) { + _webview = nullptr; + if (_webviewProgress) { + hideWebviewProgress(); + if (_progress && !_progress->shown) { + _progress = nullptr; + } + } + } + if (_webviewBottom.get() == bottom) { + _webviewBottom = nullptr; + } + }); + if (!raw->widget()) { + return false; + } + + container->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + raw->widget()->setGeometry(geometry); + }, _webview->lifetime); + + raw->setMessageHandler([=](const QJsonDocument &message) { + if (!message.isArray()) { + LOG(("BotWebView Error: " + "Not an array received in buy_callback arguments.")); + return; + } + const auto command = message.array().at(0).toString(); + if (command == "webview_close") { + _close(); + } else if (command == "webview_send_result_message") { + _send(); + } + }); + + raw->setNavigationStartHandler([=](const QString &uri) { + showWebviewProgress(); + return true; + }); + raw->setNavigationDoneHandler([=](bool success) { + hideWebviewProgress(); + }); + + raw->init(R"( +window.TelegramWebviewProxy = { +postEvent: function(eventType, eventData) { + if (window.external && window.external.invoke) { + window.external.invoke(JSON.stringify([eventType, eventData])); + } +} +};)"); + + _widget->showInner(std::move(container)); + + setupProgressGeometry(); + + return true; +} + +void Panel::setTitle(rpl::producer title) { + _widget->setTitle(std::move(title)); +} + +void Panel::showBox(object_ptr box) { + if (const auto widget = _webview ? _webview->window.widget() : nullptr) { + const auto hideNow = !widget->isHidden(); + if (hideNow || _webview->lastHidingBox) { + const auto raw = _webview->lastHidingBox = box.data(); + box->boxClosing( + ) | rpl::start_with_next([=] { + const auto widget = _webview + ? _webview->window.widget() + : nullptr; + if (widget + && widget->isHidden() + && _webview->lastHidingBox == raw) { + widget->show(); + } + }, _webview->lifetime); + if (hideNow) { + widget->hide(); + } + } + } + _widget->showBox( + std::move(box), + LayerOption::KeepOther, + anim::type::normal); +} + +void Panel::showToast(const TextWithEntities &text) { + _widget->showToast(text); +} + +void Panel::showCriticalError(const TextWithEntities &text) { + _progress = nullptr; + _webviewProgress = false; + auto error = base::make_unique_q>( + _widget.get(), + object_ptr( + _widget.get(), + rpl::single(text), + st::paymentsCriticalError), + st::paymentsCriticalErrorPadding); + error->entity()->setClickHandlerFilter([=]( + const ClickHandlerPtr &handler, + Qt::MouseButton) { + const auto entity = handler->getTextEntity(); + if (entity.type != EntityType::CustomUrl) { + return true; + } + File::OpenUrl(entity.data); + return false; + }); + _widget->showInner(std::move(error)); +} + +void Panel::showWebviewError( + const QString &text, + const Webview::Available &information) { + using Error = Webview::Available::Error; + Expects(information.error != Error::None); + + auto rich = TextWithEntities{ text }; + rich.append("\n\n"); + switch (information.error) { + case Error::NoWebview2: { + const auto command = QString(QChar(TextCommand)); + const auto text = tr::lng_payments_webview_install_edge( + tr::now, + lt_link, + command); + const auto parts = text.split(command); + rich.append(parts.value(0)) + .append(Text::Link( + "Microsoft Edge WebView2 Runtime", + "https://go.microsoft.com/fwlink/p/?LinkId=2124703")) + .append(parts.value(1)); + } break; + case Error::NoGtkOrWebkit2Gtk: + rich.append(tr::lng_payments_webview_install_webkit(tr::now)); + break; + case Error::MutterWM: + rich.append(tr::lng_payments_webview_switch_mutter(tr::now)); + break; + case Error::Wayland: + rich.append(tr::lng_payments_webview_switch_wayland(tr::now)); + break; + default: + rich.append(QString::fromStdString(information.details)); + break; + } + showCriticalError(rich); +} + +rpl::lifetime &Panel::lifetime() { + return _widget->lifetime(); +} + +std::unique_ptr Show(Args &&args) { + auto result = std::make_unique( + args.userDataPath, + std::move(args.send), + std::move(args.close)); + result->showWebview(args.url, rpl::single(u"smth"_q)); + return result; +} + +} // namespace Ui::BotWebView diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h new file mode 100644 index 000000000..cc9be61f8 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -0,0 +1,77 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" + +namespace Ui { +class BoxContent; +class RpWidget; +class SeparatePanel; +} // namespace Ui + +namespace Webview { +struct Available; +} // namespace Webview + +namespace Ui::BotWebView { + +class Panel final { +public: + Panel(const QString &userDataPath, Fn send, Fn close); + ~Panel(); + + void requestActivate(); + void toggleProgress(bool shown); + + bool showWebview( + const QString &url, + rpl::producer bottomText); + + void showBox(object_ptr box); + void showToast(const TextWithEntities &text); + void showCriticalError(const TextWithEntities &text); + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + struct Progress; + struct WebviewWithLifetime; + + bool createWebview(); + void showWebviewProgress(); + void hideWebviewProgress(); + void showWebviewError( + const QString &text, + const Webview::Available &information); + void setTitle(rpl::producer title); + + [[nodiscard]] bool progressWithBackground() const; + [[nodiscard]] QRect progressRect() const; + void setupProgressGeometry(); + + QString _userDataPath; + Fn _send; + Fn _close; + std::unique_ptr _widget; + std::unique_ptr _webview; + std::unique_ptr _webviewBottom; + std::unique_ptr _progress; + bool _webviewProgress = false; + +}; + +struct Args { + QString url; + QString userDataPath; + Fn send; + Fn close; +}; +[[nodiscard]] std::unique_ptr Show(Args &&args); + +} // namespace Ui::BotWebView diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 33c7223b9..39bfd1cad 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/text/format_values.h" // Ui::FormatPhone. #include "ui/delayed_activation.h" +#include "ui/chat/attach/attach_bot_webview.h" #include "ui/chat/message_bubble.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -60,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toasts/common_toasts.h" #include "calls/calls_instance.h" // Core::App().calls().inCall(). #include "calls/group/calls_group_call.h" +#include "inline_bots/inline_bot_result.h" #include "ui/boxes/calendar_box.h" #include "ui/boxes/confirm_box.h" #include "mainwidget.h" @@ -72,6 +74,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "support/support_helper.h" #include "storage/file_upload.h" +#include "storage/storage_domain.h" #include "facades.h" #include "window/themes/window_theme.h" #include "styles/style_window.h" @@ -88,8 +91,8 @@ constexpr auto kDayBaseFile = ":/gui/day-custom-base.tdesktop-theme"_cs; constexpr auto kNightBaseFile = ":/gui/night-custom-base.tdesktop-theme"_cs; [[nodiscard]] Fn PreparePaletteCallback( - bool dark, - std::optional accent) { + bool dark, + std::optional accent) { return [=](style::palette &palette) { using namespace Theme; const auto &embedded = EmbeddedThemes(); @@ -116,8 +119,8 @@ constexpr auto kNightBaseFile = ":/gui/night-custom-base.tdesktop-theme"_cs; } [[nodiscard]] Ui::ChatThemeBubblesData PrepareBubblesData( - const Data::CloudTheme &theme, - Data::CloudThemeType type) { + const Data::CloudTheme &theme, + Data::CloudThemeType type) { const auto i = theme.settings.find(type); return { .colors = (i != end(theme.settings) @@ -129,6 +132,17 @@ constexpr auto kNightBaseFile = ":/gui/night-custom-base.tdesktop-theme"_cs; }; } +[[nodiscard]] UserData *ParseAttachBot( + not_null session, + const MTPAttachMenuBot &bot) { + return bot.match([&](const MTPDattachMenuBot &data) { + const auto user = session->data().userLoaded(UserId(data.vbot_id())); + return (user && user->isBot() && user->botInfo->supportsAttachMenu) + ? user + : nullptr; + }); +} + } // namespace void ActivateWindow(not_null controller) { @@ -147,8 +161,8 @@ bool operator!=(const PeerThemeOverride &a, const PeerThemeOverride &b) { } DateClickHandler::DateClickHandler(Dialogs::Key chat, QDate date) -: _chat(chat) -, _date(date) { + : _chat(chat) + , _date(date) { } void DateClickHandler::setDate(QDate date) { @@ -163,13 +177,11 @@ void DateClickHandler::onClick(ClickContext context) const { } SessionNavigation::SessionNavigation(not_null session) -: _session(session) { + : _session(session) + , _api(&_session->mtp()) { } -SessionNavigation::~SessionNavigation() { - _session->api().request(base::take(_showingRepliesRequestId)).cancel(); - _session->api().request(base::take(_resolveRequestId)).cancel(); -} +SessionNavigation::~SessionNavigation() = default; Main::Session &SessionNavigation::session() const { return *_session; @@ -193,14 +205,14 @@ void SessionNavigation::showPeerByLink(const PeerByLinkInfo &info) { } void SessionNavigation::resolvePhone( - const QString &phone, - Fn)> done) { + const QString &phone, + Fn)> done) { if (const auto peer = _session->data().userByPhone(phone)) { done(peer); return; } - _session->api().request(base::take(_resolveRequestId)).cancel(); - _resolveRequestId = _session->api().request(MTPcontacts_ResolvePhone( + _api.request(base::take(_resolveRequestId)).cancel(); + _resolveRequestId = _api.request(MTPcontacts_ResolvePhone( MTP_string(phone) )).done([=](const MTPcontacts_ResolvedPeer &result) { resolveDone(result, done); @@ -216,14 +228,14 @@ void SessionNavigation::resolvePhone( } void SessionNavigation::resolveUsername( - const QString &username, - Fn)> done) { + const QString &username, + Fn)> done) { if (const auto peer = _session->data().peerByUsername(username)) { done(peer); return; } - _session->api().request(base::take(_resolveRequestId)).cancel(); - _resolveRequestId = _session->api().request(MTPcontacts_ResolveUsername( + _api.request(base::take(_resolveRequestId)).cancel(); + _resolveRequestId = _api.request(MTPcontacts_ResolveUsername( MTP_string(username) )).done([=](const MTPcontacts_ResolvedPeer &result) { resolveDone(result, done); @@ -237,8 +249,8 @@ void SessionNavigation::resolveUsername( } void SessionNavigation::resolveDone( - const MTPcontacts_ResolvedPeer &result, - Fn)> done) { + const MTPcontacts_ResolvedPeer &result, + Fn)> done) { _resolveRequestId = 0; Ui::hideLayer(); result.match([&](const MTPDcontacts_resolvedPeer &data) { @@ -251,8 +263,8 @@ void SessionNavigation::resolveDone( } void SessionNavigation::resolveChannelById( - ChannelId channelId, - Fn)> done) { + ChannelId channelId, + Fn)> done) { if (const auto channel = _session->data().channelLoaded(channelId)) { done(channel); return; @@ -260,10 +272,10 @@ void SessionNavigation::resolveChannelById( const auto fail = [=] { Ui::ShowMultilineToast({ .text = { tr::lng_error_post_link_invalid(tr::now) } - }); + }); }; - _session->api().request(base::take(_resolveRequestId)).cancel(); - _resolveRequestId = _session->api().request(MTPchannels_GetChannels( + _api.request(base::take(_resolveRequestId)).cancel(); + _resolveRequestId = _api.request(MTPchannels_GetChannels( MTP_vector( 1, MTP_inputChannel(MTP_long(channelId.bare), MTP_long(0))) @@ -280,8 +292,8 @@ void SessionNavigation::resolveChannelById( } void SessionNavigation::showPeerByLinkResolved( - not_null peer, - const PeerByLinkInfo &info) { + not_null peer, + const PeerByLinkInfo &info) { auto params = SectionShow{ SectionShow::Way::Forward }; @@ -298,11 +310,11 @@ void SessionNavigation::showPeerByLinkResolved( const auto bad = [=] { Ui::ShowMultilineToast({ .text = { tr::lng_group_invite_bad_link(tr::now) } - }); + }); }; const auto hash = *info.voicechatHash; - _session->api().request(base::take(_resolveRequestId)).cancel(); - _resolveRequestId = _session->api().request( + _api.request(base::take(_resolveRequestId)).cancel(); + _resolveRequestId = _api.request( MTPchannels_GetFullChannel(peer->asChannel()->inputChannel) ).done([=](const MTPmessages_ChatFull &result) { _session->api().processFullPeer(peer, result); @@ -322,7 +334,7 @@ void SessionNavigation::showPeerByLinkResolved( } const auto id = call->id(); const auto limit = 5; - _resolveRequestId = _session->api().request( + _resolveRequestId = _api.request( MTPphone_GetGroupCall(call->input(), MTP_int(limit)) ).done([=](const MTPphone_GroupCall &result) { if (const auto now = peer->groupCall() @@ -391,14 +403,14 @@ void SessionNavigation::showPeerByLinkResolved( } crl::on_main(this, [=] { showPeerHistory(peer->id, params, msgId); - showAttachWebview(peer, attachBotUsername); + resolveAttachWebview(peer, attachBotUsername); }); } } -void SessionNavigation::showAttachWebview( - not_null peer, - const QString &botUsername) { +void SessionNavigation::resolveAttachWebview( + not_null peer, + const QString &botUsername) { if (!peer->isUser() || botUsername.isEmpty()) { return; } @@ -408,28 +420,108 @@ void SessionNavigation::showAttachWebview( Ui::ShowMultilineToast({ // #TODO webview lang .text = { u"This bot isn't supported in the attach menu."_q } - }); + }); return; } + requestAttachWebview(peer, user); + }); +} - // #TODO webview cancel request in destructor - session().api().request(MTPmessages_RequestWebView( - MTP_flags(0), +void SessionNavigation::requestAttachWebview( + not_null peer, + not_null bot) { + _api.request(MTPmessages_RequestWebView( + MTP_flags(0), + peer->input, + bot->inputUser, + MTPstring(), // start_param + MTPDataJSON() // theme_params + )).done([=](const MTPWebViewResult &result) { + result.match([&](const MTPDwebViewResultUrl &data) { + const auto url = qs(data.vurl()); + showAttachWebview(peer, bot, data.vquery_id().v, url); + }, [&](const MTPDwebViewResultConfirmationRequired &data) { + session().data().processUsers(data.vusers()); + const auto &received = data.vbot(); + if (const auto bot = ParseAttachBot(&session(), received)) { + requestAddToMenu(bot, [=] { + requestAttachWebview(peer, bot); + }); + } + }); + }).fail([=](const MTP::Error &error) { + int a = error.code(); + }).send(); +} + +void SessionNavigation::showAttachWebview( + not_null peer, + not_null bot, + uint64 queryId, + const QString &url) { + const auto close = crl::guard(this, [=] { + _botWebView = nullptr; + }); + const auto send = crl::guard(this, [=] { + _api.request(MTPmessages_GetWebViewResult( peer->input, - user->inputUser, - MTPstring(), // start_param - MTPDataJSON() // theme_params - )).done([=](const MTPWebViewResult &result) { - result.match([&](const MTPDwebViewResultUrl &data) { - int b = 0; - }, [&](const MTPDwebViewResultConfirmationRequired &data) { + bot->inputUser, + MTP_long(queryId) + )).done([=](const MTPmessages_WebViewResult &result) { + result.match([&](const MTPDmessages_webViewResult &data) { session().data().processUsers(data.vusers()); - int a = 0; + auto result = InlineBots::Result::Create( + &session(), + queryId, + data.vresult()); + _inlineResultConfirmed.fire({ + .result = result.get(), + .bot = bot, + .recipientOverride = peer, + //.options = + }); + close(); }); - }).fail([=](const MTP::Error &error) { - int a = error.code(); }).send(); }); + _botWebView = Ui::BotWebView::Show({ + .url = url, + .userDataPath = session().domain().local().webviewDataPath(), + .send = send, + .close = close, + }); +} + +void SessionNavigation::requestAddToMenu( + not_null bot, + Fn callback) { + const auto done = [=](Fn close) { + toggleInMenu(bot, true, [=] { + callback(); + close(); + }); + }; + show(Ui::MakeConfirmBox({ + u"Do you want to? "_q + bot->name, + done, + })); +} + +void SessionNavigation::toggleInMenu( + not_null bot, + bool enabled, + Fn callback) { + _api.request(MTPmessages_ToggleBotInAttachMenu( + bot->inputUser, + MTP_bool(enabled) + )).done([=](const MTPBool &result) { + callback(); + }).send(); +} + +auto SessionNavigation::inlineResultConfirmed() const +-> rpl::producer { + return _inlineResultConfirmed.events(); } void SessionNavigation::showRepliesForMessage( @@ -445,7 +537,7 @@ void SessionNavigation::showRepliesForMessage( // HistoryView::RepliesWidget right now handles only channels. return; } - _session->api().request(base::take(_showingRepliesRequestId)).cancel(); + _api.request(base::take(_showingRepliesRequestId)).cancel(); const auto postPeer = history->peer; //const auto item = _session->data().message(postPeer, rootId); @@ -461,7 +553,7 @@ void SessionNavigation::showRepliesForMessage( //} _showingRepliesHistory = history; _showingRepliesRootId = rootId; - _showingRepliesRequestId = _session->api().request( + _showingRepliesRequestId = _api.request( MTPmessages_GetDiscussionMessage( history->peer->input, MTP_int(rootId)) diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index d83953ad6..8c3cf4241 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/layer_widget.h" #include "ui/layers/show.h" #include "window/window_adaptive.h" +#include "mtproto/sender.h" class PhotoData; class MainWidget; @@ -60,6 +61,14 @@ struct ChatThemeBackgroundData; class MessageSendingAnimationController; } // namespace Ui +namespace Ui::BotWebView { +class Panel; +} // namespace Ui::BotWebView + +namespace InlineBots { +struct ResultSelected; +} // namespace InlineBots + namespace Data { struct CloudTheme; enum class CloudThemeType; @@ -195,9 +204,12 @@ public: FullMsgId clickFromMessageId; }; void showPeerByLink(const PeerByLinkInfo &info); - void showAttachWebview( + + void resolveAttachWebview( not_null peer, const QString &botUsername); + [[nodiscard]] auto inlineResultConfirmed() const + -> rpl::producer; void showRepliesForMessage( not_null history, @@ -266,14 +278,34 @@ private: not_null peer, const PeerByLinkInfo &info); + void requestAttachWebview( + not_null peer, + not_null bot); + void requestAddToMenu(not_null bot, Fn callback); + void showAttachWebview( + not_null peer, + not_null bot, + uint64 queryId, + const QString &url); + + void toggleInMenu( + not_null bot, + bool enabled, + Fn callback); + const not_null _session; + MTP::Sender _api; + mtpRequestId _resolveRequestId = 0; History *_showingRepliesHistory = nullptr; MsgId _showingRepliesRootId = 0; mtpRequestId _showingRepliesRequestId = 0; + std::unique_ptr _botWebView; + rpl::event_stream _inlineResultConfirmed; + }; class SessionController : public SessionNavigation { diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index bd62b61c4..487d06eb0 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -157,6 +157,8 @@ PRIVATE ui/chat/attach/attach_album_preview.h ui/chat/attach/attach_album_thumbnail.cpp ui/chat/attach/attach_album_thumbnail.h + ui/chat/attach/attach_bot_webview.cpp + ui/chat/attach/attach_bot_webview.h ui/chat/attach/attach_controls.cpp ui/chat/attach/attach_controls.h ui/chat/attach/attach_extensions.cpp