From fbd8abc1c671eabe90e2da14bae4b8be386d2767 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 6 Sep 2023 13:41:23 +0400 Subject: [PATCH] Start main menu bots. --- Telegram/Resources/langs/lang.strings | 11 + .../SourceFiles/core/local_url_handlers.cpp | 9 +- .../inline_bots/bot_attach_web_view.cpp | 322 +++++++++++++----- .../inline_bots/bot_attach_web_view.h | 44 ++- .../payments/ui/payments_panel.cpp | 38 +-- .../SourceFiles/storage/storage_account.cpp | 23 +- .../SourceFiles/storage/storage_account.h | 6 +- .../ui/chat/attach/attach_bot_webview.cpp | 11 +- .../ui/chat/attach/attach_bot_webview.h | 9 +- .../SourceFiles/window/window_main_menu.cpp | 42 +++ .../window/window_session_controller.cpp | 7 + .../window/window_session_controller.h | 1 + 12 files changed, 369 insertions(+), 154 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d677c6b40..470156400 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2260,6 +2260,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_remove_from_menu" = "Remove From Menu"; "lng_bot_remove_from_menu_sure" = "Remove {bot} from the attachment menu?"; "lng_bot_remove_from_menu_done" = "Bot removed from the menu."; +"lng_bot_remove_from_side_menu" = "Remove From Menu"; +"lng_bot_remove_from_side_menu_sure" = "Remove {bot} from the main menu?"; +"lng_bot_remove_from_side_menu_done" = "Bot removed from the main menu."; "lng_bot_settings" = "Settings"; "lng_bot_open" = "Open Bot"; "lng_bot_reload_page" = "Reload Page"; @@ -2271,6 +2274,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_close_warning_title" = "Warning"; "lng_bot_close_warning" = "Changes that you made may not be saved."; "lng_bot_close_warning_sure" = "Close anyway"; +"lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time."; +"lng_bot_add_to_side_menu_done" = "Bot added to the main menu."; "lng_typing" = "typing"; "lng_user_typing" = "{user} is typing"; @@ -3834,6 +3839,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_telegram_features_url" = "https://t.me/TelegramTips"; +"lng_mini_apps_disclaimer_title" = "Warning"; +"lng_mini_apps_disclaimer_text" = "You are about to use a mini app operated by an independent party **not affiliated with Telegram**. You must agree to the Terms of Use of mini apps to continue."; +"lng_mini_apps_disclaimer_button" = "I agree to the {link}"; +"lng_mini_apps_disclaimer_link" = "Terms of Use"; +"lng_mini_apps_tos_url" = "https://telegram.org/tos/mini-apps"; + "lng_ringtones_box_title" = "Notification Sound"; "lng_ringtones_box_cloud_subtitle" = "Choose your tone"; "lng_ringtones_box_upload_choose" = "Choose ringtone"; diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 9daf74d62..8d39fafb6 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -392,7 +392,6 @@ bool ResolveUsernameOrPhone( const auto storyParam = params.value(u"story"_q); const auto storyId = storyParam.toInt(); const auto appname = webChannelPreviewLink ? QString() : appnameParam; - const auto appstart = params.value(u"startapp"_q); const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); const auto topicParam = params.value(u"topic"_q); @@ -404,11 +403,11 @@ bool ResolveUsernameOrPhone( startToken = gameParam; resolveType = ResolveType::ShareGame; } - if (startToken.isEmpty() && params.contains(u"startapp"_q)) { - startToken = params.value(u"startapp"_q); - } if (!appname.isEmpty()) { resolveType = ResolveType::BotApp; + if (startToken.isEmpty() && params.contains(u"startapp"_q)) { + startToken = params.value(u"startapp"_q); + } } const auto myContext = context.value(); using Navigation = Window::SessionNavigation; @@ -437,6 +436,8 @@ bool ResolveUsernameOrPhone( .attachBotToggleCommand = (params.contains(u"startattach"_q) ? params.value(u"startattach"_q) : std::optional()), + .attachBotMenuOpen = (appname.isEmpty() + && params.contains(u"startapp"_q)), .attachBotChooseTypes = InlineBots::ParseChooseTypes( params.value(u"choose"_q)), .voicechatHash = (params.contains(u"livestream"_q) diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 0a45d407d..3d1c241e4 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "mainwidget.h" #include "styles/style_boxes.h" +#include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include @@ -56,11 +57,7 @@ namespace InlineBots { namespace { constexpr auto kProlongTimeout = 60 * crl::time(1000); - -struct ParsedBot { - UserData *bot = nullptr; - bool inactive = false; -}; +constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000); [[nodiscard]] DocumentData *ResolveIcon( not_null session, @@ -113,8 +110,13 @@ struct ParsedBot { .user = user, .icon = ResolveIcon(session, data), .name = qs(data.vshort_name()), - .types = ResolvePeerTypes(data.vpeer_types().v), + .types = (data.vpeer_types() + ? ResolvePeerTypes(data.vpeer_types()->v) + : PeerTypes()), .inactive = data.is_inactive(), + .inMainMenu = data.is_show_in_side_menu(), + .inAttachMenu = data.is_show_in_attach_menu(), + .disclaimerRequired = data.is_side_menu_disclaimer_needed(), .hasSettings = data.is_has_settings(), .requestWriteAccess = data.is_request_write_access(), } : std::optional(); @@ -216,19 +218,18 @@ private: int contentHeight() const override; void prepare(); - void validateIcon(); void paint(Painter &p); const not_null _dummyAction; const style::Menu &_st; const AttachWebViewBot _bot; + MenuBotIcon _icon; + base::unique_qptr _menu; rpl::event_stream _forceShown; Ui::Text::String _text; - QImage _mask; - QImage _icon; int _textWidth = 0; const int _height; @@ -243,6 +244,7 @@ BotAction::BotAction( , _dummyAction(new QAction(parent)) , _st(st) , _bot(bot) +, _icon(this, _bot.media) , _height(_st.itemPadding.top() + _st.itemStyle.font->height + _st.itemPadding.bottom()) { @@ -250,55 +252,19 @@ BotAction::BotAction( initResizeHook(parent->sizeValue()); setClickedCallback(std::move(callback)); + _icon.move(_st.itemIconPosition); + paintRequest( ) | rpl::start_with_next([=] { Painter p(this); paint(p); }, lifetime()); - style::PaletteChanged( - ) | rpl::start_with_next([=] { - _icon = QImage(); - update(); - }, lifetime()); - enableMouseSelecting(); prepare(); } -void BotAction::validateIcon() { - if (_mask.isNull()) { - if (!_bot.media || !_bot.media->loaded()) { - return; - } - auto icon = QSvgRenderer(_bot.media->bytes()); - if (!icon.isValid()) { - _mask = QImage( - QSize(1, 1) * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - _mask.fill(Qt::transparent); - } else { - const auto size = style::ConvertScale(icon.defaultSize()); - _mask = QImage( - size * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - _mask.setDevicePixelRatio(style::DevicePixelRatio()); - _mask.fill(Qt::transparent); - { - auto p = QPainter(&_mask); - icon.render(&p, QRect(QPoint(), size)); - } - _mask = Images::Colored(std::move(_mask), QColor(255, 255, 255)); - } - } - if (_icon.isNull()) { - _icon = style::colorizeImage(_mask, st::menuIconColor); - } -} - void BotAction::paint(Painter &p) { - validateIcon(); - const auto selected = isSelected(); if (selected && _st.itemBgOver->c.alpha() < 255) { p.fillRect(0, 0, width(), _height, _st.itemBg); @@ -308,10 +274,6 @@ void BotAction::paint(Painter &p) { paintRipple(p, 0, 0); } - if (!_icon.isNull()) { - p.drawImage(_st.itemIconPosition, _icon); - } - p.setPen(selected ? _st.itemFgOver : _st.itemFg); _text.drawLeftElided( p, @@ -390,6 +352,53 @@ void BotAction::handleKeyPress(not_null e) { } // namespace +MenuBotIcon::MenuBotIcon( + QWidget *parent, + std::shared_ptr media) +: RpWidget(parent) +, _media(std::move(media)) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _image = QImage(); + update(); + }, lifetime()); + + setAttribute(Qt::WA_TransparentForMouseEvents); + resize(st::menuIconAdmin.size()); + show(); +} + +void MenuBotIcon::paintEvent(QPaintEvent *e) { + validate(); + if (!_image.isNull()) { + QPainter(this).drawImage(0, 0, _image); + } +} + +void MenuBotIcon::validate() { + const auto ratio = style::DevicePixelRatio(); + const auto wanted = size() * ratio; + if (_mask.size() != wanted) { + if (!_media || !_media->loaded()) { + return; + } + auto icon = QSvgRenderer(_media->bytes()); + _mask = QImage(wanted, QImage::Format_ARGB32_Premultiplied); + _mask.setDevicePixelRatio(style::DevicePixelRatio()); + _mask.fill(Qt::transparent); + if (icon.isValid()) { + auto p = QPainter(&_mask); + icon.render(&p, rect()); + p.end(); + + _mask = Images::Colored(std::move(_mask), Qt::white); + } + } + if (_image.isNull()) { + _image = style::colorizeImage(_mask, st::menuIconColor); + } +} + bool PeerMatchesTypes( not_null peer, not_null bot, @@ -427,11 +436,14 @@ struct AttachWebView::Context { Dialogs::EntryState dialogsEntryState; Api::SendAction action; bool fromSwitch = false; + bool fromMainMenu = false; bool fromBotApp = false; }; AttachWebView::AttachWebView(not_null session) -: _session(session) { +: _session(session) +, _refreshTimer([=] { requestBots(); }) { + _refreshTimer.callEach(kRefreshBotsTimeout); } AttachWebView::~AttachWebView() { @@ -526,6 +538,7 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { } break; case Button::RemoveFromMenu: + case Button::RemoveFromMainMenu: const auto attached = ranges::find( _attachBots, not_null{ _bot }, @@ -540,12 +553,15 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { active->activate(); } }); + const auto main = (button == Button::RemoveFromMainMenu); _panel->showBox(Ui::MakeConfirmBox({ - tr::lng_bot_remove_from_menu_sure( - tr::now, - lt_bot, - Ui::Text::Bold(name), - Ui::Text::WithEntities), + (main + ? tr::lng_bot_remove_from_side_menu_sure + : tr::lng_bot_remove_from_menu_sure)( + tr::now, + lt_bot, + Ui::Text::Bold(name), + Ui::Text::WithEntities), done, })); break; @@ -556,6 +572,7 @@ void AttachWebView::botSendData(QByteArray data) { if (!_context || _context->fromSwitch || _context->fromBotApp + || _context->fromMainMenu || _context->action.history->peer != _bot || _lastShownQueryId) { return; @@ -687,6 +704,7 @@ bool AttachWebView::IsSame( && (a->controller == b.controller) && (a->dialogsEntryState == b.dialogsEntryState) && (a->fromSwitch == b.fromSwitch) + && (a->fromMainMenu == b.fromMainMenu) && (a->action.history == b.action.history) && (a->action.replyTo == b.action.replyTo) && (a->action.options.sendAs == b.action.options.sendAs) @@ -702,7 +720,7 @@ void AttachWebView::request( bot, button, LookupContext(controller, action), - button.fromMenu ? nullptr : controller.get()); + button.fromAttachMenu ? nullptr : controller.get()); } void AttachWebView::requestWithOptionalConfirm( @@ -762,7 +780,7 @@ void AttachWebView::request(const WebViewButton &button) { data.vquery_id().v, qs(data.vurl()), button.text, - button.fromMenu || button.url.isEmpty()); + button.fromAttachMenu || button.url.isEmpty()); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"BOT_INVALID"_q) { @@ -800,13 +818,11 @@ void AttachWebView::requestBots() { _attachBots.reserve(data.vbots().v.size()); for (const auto &bot : data.vbots().v) { if (auto parsed = ParseAttachBot(_session, bot)) { - if (!parsed->inactive) { - if (const auto icon = parsed->icon) { - parsed->media = icon->createMediaView(); - icon->save(Data::FileOrigin(), {}); - } - _attachBots.push_back(std::move(*parsed)); + if (const auto icon = parsed->icon) { + parsed->media = icon->createMediaView(); + icon->save(Data::FileOrigin(), {}); } + _attachBots.push_back(std::move(*parsed)); } } _attachBotsUpdates.fire({}); @@ -818,13 +834,13 @@ void AttachWebView::requestBots() { void AttachWebView::requestAddToMenu( not_null bot, - const QString &startCommand) { + std::optional startCommand) { requestAddToMenu(bot, startCommand, nullptr, std::nullopt, PeerTypes()); } void AttachWebView::requestAddToMenu( not_null bot, - const QString &startCommand, + std::optional startCommand, Window::SessionController *controller, std::optional action, PeerTypes chooseTypes) { @@ -863,16 +879,22 @@ void AttachWebView::requestAddToMenu( const auto open = [=](PeerTypes types) { const auto strong = chooseController.get(); if (!strong) { - if (wasController) { + if (wasController || !startCommand) { // Just ignore the click if controller was destroyed. return true; } + } else if (!startCommand) { + _bot = bot; + acceptDisclaimer(strong, [=] { + requestSimple(strong, bot, { .fromMainMenu = true }); + }); + return true; } else if (const auto useTypes = chooseTypes & types) { const auto done = [=](not_null thread) { strong->showThread(thread); requestWithOptionalConfirm( bot, - { .startCommand = startCommand }, + { .startCommand = *startCommand }, LookupContext(strong, Api::SendAction(thread))); }; ShowChooseBox(strong, useTypes, done); @@ -883,7 +905,7 @@ void AttachWebView::requestAddToMenu( } requestWithOptionalConfirm( bot, - { .startCommand = startCommand }, + { .startCommand = *startCommand }, *context); return true; }; @@ -891,6 +913,14 @@ void AttachWebView::requestAddToMenu( _session->data().processUsers(data.vusers()); if (const auto parsed = ParseAttachBot(_session, data.vbot())) { if (bot == parsed->user) { + const auto i = ranges::find( + _attachBots, + not_null(bot), + &AttachWebViewBot::user); + if (i != end(_attachBots)) { + // Save flags in our list, like 'inactive'. + *i = *parsed; + } const auto types = parsed->types; if (parsed->inactive) { confirmAddToMenu(*parsed, [=] { @@ -910,7 +940,7 @@ void AttachWebView::requestAddToMenu( _addToMenuId = 0; _addToMenuBot = nullptr; _addToMenuContext = nullptr; - _addToMenuStartCommand = QString(); + _addToMenuStartCommand = std::nullopt; showToast(tr::lng_bot_menu_not_supported(tr::now)); }).send(); } @@ -983,18 +1013,27 @@ void AttachWebView::requestSimple( controller, Api::SendAction(bot->owner().history(bot)))); _context->fromSwitch = button.fromSwitch; - confirmOpen(controller, [=] { - requestSimple(button); - }); + _context->fromMainMenu = button.fromMainMenu; + if (button.fromMainMenu) { + acceptDisclaimer(controller, [=] { + requestSimple(button); + }); + } else { + confirmOpen(controller, [=] { + requestSimple(button); + }); + } } void AttachWebView::requestSimple(const WebViewButton &button) { using Flag = MTPmessages_RequestSimpleWebView::Flag; _requestId = _session->api().request(MTPmessages_RequestSimpleWebView( MTP_flags(Flag::f_theme_params + | (button.fromMainMenu ? Flag::f_from_side_menu : Flag::f_url) | (button.fromSwitch ? Flag::f_from_switch_webview : Flag())), _bot->inputUser, MTP_bytes(button.url), + MTP_string(""), // start_param MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop") )).done([=](const MTPSimpleWebViewResult &result) { @@ -1200,6 +1239,112 @@ void AttachWebView::confirmOpen( })); } +void AttachWebView::acceptDisclaimer( + not_null controller, + Fn done) { + const auto local = _bot ? &_bot->session().local() : nullptr; + if (!local) { + return; + } + const auto i = ranges::find( + _attachBots, + not_null(_bot), + &AttachWebViewBot::user); + if (i == end(_attachBots)) { + _attachBotsUpdates.fire({}); + return; + } else if (i->inactive) { + requestAddToMenu(_bot, {}, controller, {}, {}); + return; + } else if (!i->disclaimerRequired) { + done(); + return; + } + + const auto weak = base::make_weak(this); + controller->show(Box([=](not_null box) { + const auto updateCheck = std::make_shared>(); + const auto validateCheck = std::make_shared>(); + + const auto callback = [=](Fn close) { + if (validateCheck && (*validateCheck)() && weak) { + const auto i = ranges::find( + _attachBots, + not_null(_bot), + &AttachWebViewBot::user); + if (i == end(_attachBots)) { + _attachBotsUpdates.fire({}); + } else if (i->inactive) { + requestAddToMenu(_bot, std::nullopt); + } else { + i->disclaimerRequired = false; + requestBots(); + done(); + } + close(); + } + }; + + Ui::ConfirmBox(box, { + .text = tr::lng_mini_apps_disclaimer_text( + tr::now, + Ui::Text::RichLangValue), + .confirmed = callback, + .confirmText = tr::lng_box_ok(), + .title = tr::lng_mini_apps_disclaimer_title(), + }); + + auto checkView = std::make_unique( + st::defaultCheck, + false, + [=] { if (*updateCheck) { (*updateCheck)(); } }); + const auto check = checkView.get(); + const auto row = box->addRow( + object_ptr( + box.get(), + tr::lng_mini_apps_disclaimer_button( + lt_link, + rpl::single(Ui::Text::Link( + tr::lng_mini_apps_disclaimer_link(tr::now), + tr::lng_mini_apps_tos_url(tr::now))), + Ui::Text::WithEntities), + st::defaultBoxCheckbox, + std::move(checkView)), + { + st::boxRowPadding.left(), + st::boxRowPadding.left(), + st::boxRowPadding.right(), + st::defaultBoxCheckbox.margin.bottom(), + }); + row->setAllowTextLines(5); + row->setClickHandlerFilter([=]( + const ClickHandlerPtr &link, + Qt::MouseButton button) { + ActivateClickHandler(row, link, ClickContext{ + .button = button, + .other = QVariant::fromValue(ClickHandlerContext{ + .show = box->uiShow(), + }) + }); + return false; + }); + + (*updateCheck) = [=] { row->update(); }; + + const auto showError = Ui::CheckView::PrepareNonToggledError( + check, + box->lifetime()); + + (*validateCheck) = [=] { + if (check->checked()) { + return true; + } + showError(); + return false; + }; + })); +} + void AttachWebView::ClearAll() { while (!ActiveWebViews().empty()) { ActiveWebViews().front()->cancel(); @@ -1227,10 +1372,14 @@ void AttachWebView::show( const auto hasOpenBot = !_context || (_bot != _context->action.history->peer); const auto hasRemoveFromMenu = (attached != end(_attachBots)) - && !attached->inactive; + && (!attached->inactive || attached->inMainMenu); const auto buttons = (hasSettings ? Button::Settings : Button::None) | (hasOpenBot ? Button::OpenBot : Button::None) - | (hasRemoveFromMenu ? Button::RemoveFromMenu : Button::None); + | (!hasRemoveFromMenu + ? Button::None + : attached->inMainMenu + ? Button::RemoveFromMainMenu + : Button::RemoveFromMenu); _lastShownUrl = url; _lastShownQueryId = queryId; @@ -1318,16 +1467,20 @@ void AttachWebView::confirmAddToMenu( if (callback) { callback(); } - showToast(tr::lng_bot_add_to_menu_done(tr::now)); + showToast((bot.inMainMenu + ? tr::lng_bot_add_to_side_menu_done + : tr::lng_bot_add_to_menu_done)(tr::now)); }); close(); }; Ui::ConfirmBox(box, { - tr::lng_bot_add_to_menu( - tr::now, - lt_bot, - Ui::Text::Bold(bot.name), - Ui::Text::WithEntities), + (bot.inMainMenu + ? tr::lng_bot_add_to_side_menu + : tr::lng_bot_add_to_menu)( + tr::now, + lt_bot, + Ui::Text::Bold(bot.name), + Ui::Text::WithEntities), done, }); if (bot.requestWriteAccess) { @@ -1406,7 +1559,8 @@ std::unique_ptr MakeAttachBotsMenu( }, &st::menuIconFile); } for (const auto &bot : bots->attachBots()) { - if (!PeerMatchesTypes(peer, bot.user, bot.types)) { + if (!bot.inAttachMenu + || !PeerMatchesTypes(peer, bot.user, bot.types)) { continue; } const auto callback = [=] { @@ -1414,7 +1568,7 @@ std::unique_ptr MakeAttachBotsMenu( controller, actionFactory(), bot.user, - { .fromMenu = true }); + { .fromAttachMenu = true }); }; auto action = base::make_unique_q( raw, diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index a6c254ef8..926312ea5 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "base/weak_ptr.h" #include "base/flags.h" +#include "base/timer.h" +#include "base/weak_ptr.h" #include "mtproto/sender.h" #include "ui/chat/attach/attach_bot_webview.h" +#include "ui/rp_widget.h" namespace Api { struct SendAction; @@ -60,9 +62,12 @@ struct AttachWebViewBot { std::shared_ptr media; QString name; PeerTypes types = 0; - bool inactive = false; - bool hasSettings = false; - bool requestWriteAccess = false; + bool inactive : 1 = false; + bool inMainMenu : 1 = false; + bool inAttachMenu : 1 = false; + bool disclaimerRequired : 1 = false; + bool hasSettings : 1 = false; + bool requestWriteAccess : 1 = false; }; class AttachWebView final @@ -76,7 +81,8 @@ public: QString text; QString startCommand; QByteArray url; - bool fromMenu = false; + bool fromAttachMenu = false; + bool fromMainMenu = false; bool fromSwitch = false; }; void request( @@ -116,10 +122,10 @@ public: void requestAddToMenu( not_null bot, - const QString &startCommand); + std::optional startCommand); void requestAddToMenu( not_null bot, - const QString &startCommand, + std::optional startCommand, Window::SessionController *controller, std::optional action, PeerTypes chooseTypes); @@ -171,6 +177,9 @@ private: void confirmOpen( not_null controller, Fn done); + void acceptDisclaimer( + not_null controller, + Fn done); enum class ToggledState { Removed, @@ -200,6 +209,8 @@ private: const not_null _session; + base::Timer _refreshTimer; + std::unique_ptr _context; std::unique_ptr _lastShownContext; QString _lastShownUrl; @@ -221,7 +232,7 @@ private: std::unique_ptr _addToMenuContext; UserData *_addToMenuBot = nullptr; mtpRequestId _addToMenuId = 0; - QString _addToMenuStartCommand; + std::optional _addToMenuStartCommand; base::weak_ptr _addToMenuChooseController; PeerTypes _addToMenuChooseTypes; @@ -239,4 +250,21 @@ private: Fn actionFactory, Fn attach); +class MenuBotIcon final : public Ui::RpWidget { +public: + MenuBotIcon( + QWidget *parent, + std::shared_ptr media); + +private: + void paintEvent(QPaintEvent *e) override; + + void validate(); + + std::shared_ptr _media; + QImage _image; + QImage _mask; + +}; + } // namespace InlineBots diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 0ee46d64a..d077fbe8d 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -735,41 +735,9 @@ void Panel::requestTermsAcceptance( (*update) = [=] { row->update(); }; - struct State { - bool error = false; - Ui::Animations::Simple errorAnimation; - }; - const auto state = box->lifetime().make_state(); - const auto showError = [=] { - const auto callback = [=] { - const auto error = state->errorAnimation.value( - state->error ? 1. : 0.); - if (error == 0.) { - check->setUntoggledOverride(std::nullopt); - } else { - const auto color = anim::color( - st::defaultCheck.untoggledFg, - st::boxTextFgError, - error); - check->setUntoggledOverride(color); - } - }; - state->error = true; - state->errorAnimation.stop(); - state->errorAnimation.start( - callback, - 0., - 1., - st::defaultCheck.duration); - }; - - row->checkedChanges( - ) | rpl::filter([=](bool checked) { - return checked; - }) | rpl::start_with_next([=] { - state->error = false; - check->setUntoggledOverride(std::nullopt); - }, row->lifetime()); + const auto showError = Ui::CheckView::PrepareNonToggledError( + check, + box->lifetime()); box->addButton(tr::lng_payments_terms_accept(), [=] { if (check->checked()) { diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index f0bda9aa6..60fb45382 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -2808,7 +2808,13 @@ void Account::writeTrustedBots() { } void Account::readTrustedBots() { - if (!_trustedBotsKey) return; + if (_trustedBotsRead) { + return; + } + _trustedBotsRead = true; + if (!_trustedBotsKey) { + return; + } FileReadDescriptor trusted; if (!ReadEncryptedFile(trusted, _trustedBotsKey, _basePath, _localKey)) { @@ -2845,10 +2851,7 @@ void Account::markBotTrustedOpenGame(PeerId botId) { } bool Account::isBotTrustedOpenGame(PeerId botId) { - if (!_trustedBotsRead) { - readTrustedBots(); - _trustedBotsRead = true; - } + readTrustedBots(); const auto i = _trustedBots.find(botId); return (i != end(_trustedBots)) && ((i->second & BotTrustFlag::NoOpenGame) == 0); @@ -2870,10 +2873,7 @@ void Account::markBotTrustedPayment(PeerId botId) { } bool Account::isBotTrustedPayment(PeerId botId) { - if (!_trustedBotsRead) { - readTrustedBots(); - _trustedBotsRead = true; - } + readTrustedBots(); const auto i = _trustedBots.find(botId); return (i != end(_trustedBots)) && ((i->second & BotTrustFlag::Payment) != 0); @@ -2895,10 +2895,7 @@ void Account::markBotTrustedOpenWebView(PeerId botId) { } bool Account::isBotTrustedOpenWebView(PeerId botId) { - if (!_trustedBotsRead) { - readTrustedBots(); - _trustedBotsRead = true; - } + readTrustedBots(); const auto i = _trustedBots.find(botId); return (i != end(_trustedBots)) && ((i->second & BotTrustFlag::OpenWebView) != 0); diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index c96ba84f8..3c1525ed7 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -184,9 +184,9 @@ private: Failed, }; enum class BotTrustFlag : uchar { - NoOpenGame = (1 << 0), - Payment = (1 << 1), - OpenWebView = (1 << 2), + NoOpenGame = (1 << 0), + Payment = (1 << 1), + OpenWebView = (1 << 2), }; friend inline constexpr bool is_flag_type(BotTrustFlag) { return true; }; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index f25340450..6a29bba46 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -535,12 +535,17 @@ bool Panel::showWebview( callback(tr::lng_bot_reload_page(tr::now), [=] { _webview->window.reload(); }, &st::menuIconRestore); - if (_menuButtons & MenuButton::RemoveFromMenu) { + const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu); + if (main || (_menuButtons & MenuButton::RemoveFromMenu)) { const auto handler = [=] { - _delegate->botHandleMenuButton(MenuButton::RemoveFromMenu); + _delegate->botHandleMenuButton(main + ? MenuButton::RemoveFromMainMenu + : MenuButton::RemoveFromMenu); }; callback({ - .text = tr::lng_bot_remove_from_menu(tr::now), + .text = (main + ? tr::lng_bot_remove_from_side_menu + : tr::lng_bot_remove_from_menu)(tr::now), .handler = handler, .icon = &st::menuIconDeleteAttention, .isAttention = true, diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 54bf92aa9..b4709ac20 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -36,10 +36,11 @@ struct MainButtonArgs { }; enum class MenuButton { - None = 0x00, - Settings = 0x01, - OpenBot = 0x02, - RemoveFromMenu = 0x04, + None = 0x00, + Settings = 0x01, + OpenBot = 0x02, + RemoveFromMenu = 0x04, + RemoveFromMainMenu = 0x08, }; inline constexpr bool is_flag_type(MenuButton) { return true; } using MenuButtons = base::flags; diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 52c3e67e3..e64a9b768 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/empty_userpic.h" #include "ui/unread_badge_paint.h" #include "base/call_delayed.h" +#include "inline_bots/bot_attach_web_view.h" #include "mainwindow.h" #include "storage/localstorage.h" #include "storage/storage_account.h" @@ -198,6 +199,45 @@ void ShowCallsBox(not_null window) { }) | rpl::flatten_latest(); } +void SetupMenuBots( + not_null container, + not_null controller) { + const auto wrap = container->add( + object_ptr(container)); + const auto bots = &controller->session().attachWebView(); + + rpl::single( + rpl::empty + ) | rpl::then( + bots->attachBotsUpdates() + ) | rpl::start_with_next([=] { + wrap->clear(); + for (const auto &bot : bots->attachBots()) { + if (!bot.inMainMenu) { + continue; + } + const auto button = Settings::AddButton( + container, + rpl::single(bot.name), + st::mainMenuButton); + const auto icon = Ui::CreateChild( + button.get(), + bot.media); + button->heightValue( + ) | rpl::start_with_next([=](int height) { + icon->move( + st::mainMenuButton.iconLeft, + (height - icon->height()) / 2); + }, button->lifetime()); + button->setClickedCallback([=] { + bots->requestSimple(controller, bot.user, { + .fromMainMenu = true, + }); + }); + } + }, wrap->lifetime()); +} + } // namespace class MainMenu::ToggleAccountsButton final : public Ui::AbstractButton { @@ -784,6 +824,8 @@ void MainMenu::setupMenu() { Info::Stories::Make(controller->session().user())); }); + SetupMenuBots(_menu, controller); + addAction( tr::lng_menu_contacts(), { &st::menuIconProfile } diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 268518eed..3e5a1f85e 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -596,6 +596,13 @@ void SessionNavigation::showPeerByLinkResolved( contextUser->owner().history(contextUser)) : std::optional()), info.attachBotChooseTypes); + } else if (bot && info.attachBotMenuOpen) { + bot->session().attachWebView().requestAddToMenu( + bot, + std::nullopt, + parentController(), + std::optional(), + {}); } else { crl::on_main(this, [=] { showPeerHistory(peer, params, msgId); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 7f541cf5e..8001aef05 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -212,6 +212,7 @@ public: bool botAppForceConfirmation = false; QString attachBotUsername; std::optional attachBotToggleCommand; + bool attachBotMenuOpen = false; InlineBots::PeerTypes attachBotChooseTypes; std::optional voicechatHash; FullMsgId clickFromMessageId;