Start main menu bots.

This commit is contained in:
John Preston 2023-09-06 13:41:23 +04:00
parent 73f3110403
commit fbd8abc1c6
12 changed files with 369 additions and 154 deletions

View file

@ -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" = "Remove From Menu";
"lng_bot_remove_from_menu_sure" = "Remove {bot} from the attachment 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_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_settings" = "Settings";
"lng_bot_open" = "Open Bot"; "lng_bot_open" = "Open Bot";
"lng_bot_reload_page" = "Reload Page"; "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_title" = "Warning";
"lng_bot_close_warning" = "Changes that you made may not be saved."; "lng_bot_close_warning" = "Changes that you made may not be saved.";
"lng_bot_close_warning_sure" = "Close anyway"; "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_typing" = "typing";
"lng_user_typing" = "{user} is 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_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_title" = "Notification Sound";
"lng_ringtones_box_cloud_subtitle" = "Choose your tone"; "lng_ringtones_box_cloud_subtitle" = "Choose your tone";
"lng_ringtones_box_upload_choose" = "Choose ringtone"; "lng_ringtones_box_upload_choose" = "Choose ringtone";

View file

@ -392,7 +392,6 @@ bool ResolveUsernameOrPhone(
const auto storyParam = params.value(u"story"_q); const auto storyParam = params.value(u"story"_q);
const auto storyId = storyParam.toInt(); const auto storyId = storyParam.toInt();
const auto appname = webChannelPreviewLink ? QString() : appnameParam; const auto appname = webChannelPreviewLink ? QString() : appnameParam;
const auto appstart = params.value(u"startapp"_q);
const auto commentParam = params.value(u"comment"_q); const auto commentParam = params.value(u"comment"_q);
const auto commentId = commentParam.toInt(); const auto commentId = commentParam.toInt();
const auto topicParam = params.value(u"topic"_q); const auto topicParam = params.value(u"topic"_q);
@ -404,11 +403,11 @@ bool ResolveUsernameOrPhone(
startToken = gameParam; startToken = gameParam;
resolveType = ResolveType::ShareGame; resolveType = ResolveType::ShareGame;
} }
if (startToken.isEmpty() && params.contains(u"startapp"_q)) {
startToken = params.value(u"startapp"_q);
}
if (!appname.isEmpty()) { if (!appname.isEmpty()) {
resolveType = ResolveType::BotApp; resolveType = ResolveType::BotApp;
if (startToken.isEmpty() && params.contains(u"startapp"_q)) {
startToken = params.value(u"startapp"_q);
}
} }
const auto myContext = context.value<ClickHandlerContext>(); const auto myContext = context.value<ClickHandlerContext>();
using Navigation = Window::SessionNavigation; using Navigation = Window::SessionNavigation;
@ -437,6 +436,8 @@ bool ResolveUsernameOrPhone(
.attachBotToggleCommand = (params.contains(u"startattach"_q) .attachBotToggleCommand = (params.contains(u"startattach"_q)
? params.value(u"startattach"_q) ? params.value(u"startattach"_q)
: std::optional<QString>()), : std::optional<QString>()),
.attachBotMenuOpen = (appname.isEmpty()
&& params.contains(u"startapp"_q)),
.attachBotChooseTypes = InlineBots::ParseChooseTypes( .attachBotChooseTypes = InlineBots::ParseChooseTypes(
params.value(u"choose"_q)), params.value(u"choose"_q)),
.voicechatHash = (params.contains(u"livestream"_q) .voicechatHash = (params.contains(u"livestream"_q)

View file

@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h" #include "apiwrap.h"
#include "mainwidget.h" #include "mainwidget.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include <QSvgRenderer> #include <QSvgRenderer>
@ -56,11 +57,7 @@ namespace InlineBots {
namespace { namespace {
constexpr auto kProlongTimeout = 60 * crl::time(1000); constexpr auto kProlongTimeout = 60 * crl::time(1000);
constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
struct ParsedBot {
UserData *bot = nullptr;
bool inactive = false;
};
[[nodiscard]] DocumentData *ResolveIcon( [[nodiscard]] DocumentData *ResolveIcon(
not_null<Main::Session*> session, not_null<Main::Session*> session,
@ -113,8 +110,13 @@ struct ParsedBot {
.user = user, .user = user,
.icon = ResolveIcon(session, data), .icon = ResolveIcon(session, data),
.name = qs(data.vshort_name()), .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(), .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(), .hasSettings = data.is_has_settings(),
.requestWriteAccess = data.is_request_write_access(), .requestWriteAccess = data.is_request_write_access(),
} : std::optional<AttachWebViewBot>(); } : std::optional<AttachWebViewBot>();
@ -216,19 +218,18 @@ private:
int contentHeight() const override; int contentHeight() const override;
void prepare(); void prepare();
void validateIcon();
void paint(Painter &p); void paint(Painter &p);
const not_null<QAction*> _dummyAction; const not_null<QAction*> _dummyAction;
const style::Menu &_st; const style::Menu &_st;
const AttachWebViewBot _bot; const AttachWebViewBot _bot;
MenuBotIcon _icon;
base::unique_qptr<Ui::PopupMenu> _menu; base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<bool> _forceShown; rpl::event_stream<bool> _forceShown;
Ui::Text::String _text; Ui::Text::String _text;
QImage _mask;
QImage _icon;
int _textWidth = 0; int _textWidth = 0;
const int _height; const int _height;
@ -243,6 +244,7 @@ BotAction::BotAction(
, _dummyAction(new QAction(parent)) , _dummyAction(new QAction(parent))
, _st(st) , _st(st)
, _bot(bot) , _bot(bot)
, _icon(this, _bot.media)
, _height(_st.itemPadding.top() , _height(_st.itemPadding.top()
+ _st.itemStyle.font->height + _st.itemStyle.font->height
+ _st.itemPadding.bottom()) { + _st.itemPadding.bottom()) {
@ -250,55 +252,19 @@ BotAction::BotAction(
initResizeHook(parent->sizeValue()); initResizeHook(parent->sizeValue());
setClickedCallback(std::move(callback)); setClickedCallback(std::move(callback));
_icon.move(_st.itemIconPosition);
paintRequest( paintRequest(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
Painter p(this); Painter p(this);
paint(p); paint(p);
}, lifetime()); }, lifetime());
style::PaletteChanged(
) | rpl::start_with_next([=] {
_icon = QImage();
update();
}, lifetime());
enableMouseSelecting(); enableMouseSelecting();
prepare(); 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) { void BotAction::paint(Painter &p) {
validateIcon();
const auto selected = isSelected(); const auto selected = isSelected();
if (selected && _st.itemBgOver->c.alpha() < 255) { if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), _height, _st.itemBg); p.fillRect(0, 0, width(), _height, _st.itemBg);
@ -308,10 +274,6 @@ void BotAction::paint(Painter &p) {
paintRipple(p, 0, 0); paintRipple(p, 0, 0);
} }
if (!_icon.isNull()) {
p.drawImage(_st.itemIconPosition, _icon);
}
p.setPen(selected ? _st.itemFgOver : _st.itemFg); p.setPen(selected ? _st.itemFgOver : _st.itemFg);
_text.drawLeftElided( _text.drawLeftElided(
p, p,
@ -390,6 +352,53 @@ void BotAction::handleKeyPress(not_null<QKeyEvent*> e) {
} // namespace } // namespace
MenuBotIcon::MenuBotIcon(
QWidget *parent,
std::shared_ptr<Data::DocumentMedia> 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( bool PeerMatchesTypes(
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<UserData*> bot, not_null<UserData*> bot,
@ -427,11 +436,14 @@ struct AttachWebView::Context {
Dialogs::EntryState dialogsEntryState; Dialogs::EntryState dialogsEntryState;
Api::SendAction action; Api::SendAction action;
bool fromSwitch = false; bool fromSwitch = false;
bool fromMainMenu = false;
bool fromBotApp = false; bool fromBotApp = false;
}; };
AttachWebView::AttachWebView(not_null<Main::Session*> session) AttachWebView::AttachWebView(not_null<Main::Session*> session)
: _session(session) { : _session(session)
, _refreshTimer([=] { requestBots(); }) {
_refreshTimer.callEach(kRefreshBotsTimeout);
} }
AttachWebView::~AttachWebView() { AttachWebView::~AttachWebView() {
@ -526,6 +538,7 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {
} }
break; break;
case Button::RemoveFromMenu: case Button::RemoveFromMenu:
case Button::RemoveFromMainMenu:
const auto attached = ranges::find( const auto attached = ranges::find(
_attachBots, _attachBots,
not_null{ _bot }, not_null{ _bot },
@ -540,12 +553,15 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {
active->activate(); active->activate();
} }
}); });
const auto main = (button == Button::RemoveFromMainMenu);
_panel->showBox(Ui::MakeConfirmBox({ _panel->showBox(Ui::MakeConfirmBox({
tr::lng_bot_remove_from_menu_sure( (main
tr::now, ? tr::lng_bot_remove_from_side_menu_sure
lt_bot, : tr::lng_bot_remove_from_menu_sure)(
Ui::Text::Bold(name), tr::now,
Ui::Text::WithEntities), lt_bot,
Ui::Text::Bold(name),
Ui::Text::WithEntities),
done, done,
})); }));
break; break;
@ -556,6 +572,7 @@ void AttachWebView::botSendData(QByteArray data) {
if (!_context if (!_context
|| _context->fromSwitch || _context->fromSwitch
|| _context->fromBotApp || _context->fromBotApp
|| _context->fromMainMenu
|| _context->action.history->peer != _bot || _context->action.history->peer != _bot
|| _lastShownQueryId) { || _lastShownQueryId) {
return; return;
@ -687,6 +704,7 @@ bool AttachWebView::IsSame(
&& (a->controller == b.controller) && (a->controller == b.controller)
&& (a->dialogsEntryState == b.dialogsEntryState) && (a->dialogsEntryState == b.dialogsEntryState)
&& (a->fromSwitch == b.fromSwitch) && (a->fromSwitch == b.fromSwitch)
&& (a->fromMainMenu == b.fromMainMenu)
&& (a->action.history == b.action.history) && (a->action.history == b.action.history)
&& (a->action.replyTo == b.action.replyTo) && (a->action.replyTo == b.action.replyTo)
&& (a->action.options.sendAs == b.action.options.sendAs) && (a->action.options.sendAs == b.action.options.sendAs)
@ -702,7 +720,7 @@ void AttachWebView::request(
bot, bot,
button, button,
LookupContext(controller, action), LookupContext(controller, action),
button.fromMenu ? nullptr : controller.get()); button.fromAttachMenu ? nullptr : controller.get());
} }
void AttachWebView::requestWithOptionalConfirm( void AttachWebView::requestWithOptionalConfirm(
@ -762,7 +780,7 @@ void AttachWebView::request(const WebViewButton &button) {
data.vquery_id().v, data.vquery_id().v,
qs(data.vurl()), qs(data.vurl()),
button.text, button.text,
button.fromMenu || button.url.isEmpty()); button.fromAttachMenu || button.url.isEmpty());
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
_requestId = 0; _requestId = 0;
if (error.type() == u"BOT_INVALID"_q) { if (error.type() == u"BOT_INVALID"_q) {
@ -800,13 +818,11 @@ void AttachWebView::requestBots() {
_attachBots.reserve(data.vbots().v.size()); _attachBots.reserve(data.vbots().v.size());
for (const auto &bot : data.vbots().v) { for (const auto &bot : data.vbots().v) {
if (auto parsed = ParseAttachBot(_session, bot)) { if (auto parsed = ParseAttachBot(_session, bot)) {
if (!parsed->inactive) { if (const auto icon = parsed->icon) {
if (const auto icon = parsed->icon) { parsed->media = icon->createMediaView();
parsed->media = icon->createMediaView(); icon->save(Data::FileOrigin(), {});
icon->save(Data::FileOrigin(), {});
}
_attachBots.push_back(std::move(*parsed));
} }
_attachBots.push_back(std::move(*parsed));
} }
} }
_attachBotsUpdates.fire({}); _attachBotsUpdates.fire({});
@ -818,13 +834,13 @@ void AttachWebView::requestBots() {
void AttachWebView::requestAddToMenu( void AttachWebView::requestAddToMenu(
not_null<UserData*> bot, not_null<UserData*> bot,
const QString &startCommand) { std::optional<QString> startCommand) {
requestAddToMenu(bot, startCommand, nullptr, std::nullopt, PeerTypes()); requestAddToMenu(bot, startCommand, nullptr, std::nullopt, PeerTypes());
} }
void AttachWebView::requestAddToMenu( void AttachWebView::requestAddToMenu(
not_null<UserData*> bot, not_null<UserData*> bot,
const QString &startCommand, std::optional<QString> startCommand,
Window::SessionController *controller, Window::SessionController *controller,
std::optional<Api::SendAction> action, std::optional<Api::SendAction> action,
PeerTypes chooseTypes) { PeerTypes chooseTypes) {
@ -863,16 +879,22 @@ void AttachWebView::requestAddToMenu(
const auto open = [=](PeerTypes types) { const auto open = [=](PeerTypes types) {
const auto strong = chooseController.get(); const auto strong = chooseController.get();
if (!strong) { if (!strong) {
if (wasController) { if (wasController || !startCommand) {
// Just ignore the click if controller was destroyed. // Just ignore the click if controller was destroyed.
return true; return true;
} }
} else if (!startCommand) {
_bot = bot;
acceptDisclaimer(strong, [=] {
requestSimple(strong, bot, { .fromMainMenu = true });
});
return true;
} else if (const auto useTypes = chooseTypes & types) { } else if (const auto useTypes = chooseTypes & types) {
const auto done = [=](not_null<Data::Thread*> thread) { const auto done = [=](not_null<Data::Thread*> thread) {
strong->showThread(thread); strong->showThread(thread);
requestWithOptionalConfirm( requestWithOptionalConfirm(
bot, bot,
{ .startCommand = startCommand }, { .startCommand = *startCommand },
LookupContext(strong, Api::SendAction(thread))); LookupContext(strong, Api::SendAction(thread)));
}; };
ShowChooseBox(strong, useTypes, done); ShowChooseBox(strong, useTypes, done);
@ -883,7 +905,7 @@ void AttachWebView::requestAddToMenu(
} }
requestWithOptionalConfirm( requestWithOptionalConfirm(
bot, bot,
{ .startCommand = startCommand }, { .startCommand = *startCommand },
*context); *context);
return true; return true;
}; };
@ -891,6 +913,14 @@ void AttachWebView::requestAddToMenu(
_session->data().processUsers(data.vusers()); _session->data().processUsers(data.vusers());
if (const auto parsed = ParseAttachBot(_session, data.vbot())) { if (const auto parsed = ParseAttachBot(_session, data.vbot())) {
if (bot == parsed->user) { 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; const auto types = parsed->types;
if (parsed->inactive) { if (parsed->inactive) {
confirmAddToMenu(*parsed, [=] { confirmAddToMenu(*parsed, [=] {
@ -910,7 +940,7 @@ void AttachWebView::requestAddToMenu(
_addToMenuId = 0; _addToMenuId = 0;
_addToMenuBot = nullptr; _addToMenuBot = nullptr;
_addToMenuContext = nullptr; _addToMenuContext = nullptr;
_addToMenuStartCommand = QString(); _addToMenuStartCommand = std::nullopt;
showToast(tr::lng_bot_menu_not_supported(tr::now)); showToast(tr::lng_bot_menu_not_supported(tr::now));
}).send(); }).send();
} }
@ -983,18 +1013,27 @@ void AttachWebView::requestSimple(
controller, controller,
Api::SendAction(bot->owner().history(bot)))); Api::SendAction(bot->owner().history(bot))));
_context->fromSwitch = button.fromSwitch; _context->fromSwitch = button.fromSwitch;
confirmOpen(controller, [=] { _context->fromMainMenu = button.fromMainMenu;
requestSimple(button); if (button.fromMainMenu) {
}); acceptDisclaimer(controller, [=] {
requestSimple(button);
});
} else {
confirmOpen(controller, [=] {
requestSimple(button);
});
}
} }
void AttachWebView::requestSimple(const WebViewButton &button) { void AttachWebView::requestSimple(const WebViewButton &button) {
using Flag = MTPmessages_RequestSimpleWebView::Flag; using Flag = MTPmessages_RequestSimpleWebView::Flag;
_requestId = _session->api().request(MTPmessages_RequestSimpleWebView( _requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
MTP_flags(Flag::f_theme_params MTP_flags(Flag::f_theme_params
| (button.fromMainMenu ? Flag::f_from_side_menu : Flag::f_url)
| (button.fromSwitch ? Flag::f_from_switch_webview : Flag())), | (button.fromSwitch ? Flag::f_from_switch_webview : Flag())),
_bot->inputUser, _bot->inputUser,
MTP_bytes(button.url), MTP_bytes(button.url),
MTP_string(""), // start_param
MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
MTP_string("tdesktop") MTP_string("tdesktop")
)).done([=](const MTPSimpleWebViewResult &result) { )).done([=](const MTPSimpleWebViewResult &result) {
@ -1200,6 +1239,112 @@ void AttachWebView::confirmOpen(
})); }));
} }
void AttachWebView::acceptDisclaimer(
not_null<Window::SessionController*> controller,
Fn<void()> 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<Ui::GenericBox*> box) {
const auto updateCheck = std::make_shared<Fn<void()>>();
const auto validateCheck = std::make_shared<Fn<bool()>>();
const auto callback = [=](Fn<void()> 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<Ui::CheckView>(
st::defaultCheck,
false,
[=] { if (*updateCheck) { (*updateCheck)(); } });
const auto check = checkView.get();
const auto row = box->addRow(
object_ptr<Ui::Checkbox>(
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() { void AttachWebView::ClearAll() {
while (!ActiveWebViews().empty()) { while (!ActiveWebViews().empty()) {
ActiveWebViews().front()->cancel(); ActiveWebViews().front()->cancel();
@ -1227,10 +1372,14 @@ void AttachWebView::show(
const auto hasOpenBot = !_context const auto hasOpenBot = !_context
|| (_bot != _context->action.history->peer); || (_bot != _context->action.history->peer);
const auto hasRemoveFromMenu = (attached != end(_attachBots)) const auto hasRemoveFromMenu = (attached != end(_attachBots))
&& !attached->inactive; && (!attached->inactive || attached->inMainMenu);
const auto buttons = (hasSettings ? Button::Settings : Button::None) const auto buttons = (hasSettings ? Button::Settings : Button::None)
| (hasOpenBot ? Button::OpenBot : Button::None) | (hasOpenBot ? Button::OpenBot : Button::None)
| (hasRemoveFromMenu ? Button::RemoveFromMenu : Button::None); | (!hasRemoveFromMenu
? Button::None
: attached->inMainMenu
? Button::RemoveFromMainMenu
: Button::RemoveFromMenu);
_lastShownUrl = url; _lastShownUrl = url;
_lastShownQueryId = queryId; _lastShownQueryId = queryId;
@ -1318,16 +1467,20 @@ void AttachWebView::confirmAddToMenu(
if (callback) { if (callback) {
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(); close();
}; };
Ui::ConfirmBox(box, { Ui::ConfirmBox(box, {
tr::lng_bot_add_to_menu( (bot.inMainMenu
tr::now, ? tr::lng_bot_add_to_side_menu
lt_bot, : tr::lng_bot_add_to_menu)(
Ui::Text::Bold(bot.name), tr::now,
Ui::Text::WithEntities), lt_bot,
Ui::Text::Bold(bot.name),
Ui::Text::WithEntities),
done, done,
}); });
if (bot.requestWriteAccess) { if (bot.requestWriteAccess) {
@ -1406,7 +1559,8 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
}, &st::menuIconFile); }, &st::menuIconFile);
} }
for (const auto &bot : bots->attachBots()) { for (const auto &bot : bots->attachBots()) {
if (!PeerMatchesTypes(peer, bot.user, bot.types)) { if (!bot.inAttachMenu
|| !PeerMatchesTypes(peer, bot.user, bot.types)) {
continue; continue;
} }
const auto callback = [=] { const auto callback = [=] {
@ -1414,7 +1568,7 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
controller, controller,
actionFactory(), actionFactory(),
bot.user, bot.user,
{ .fromMenu = true }); { .fromAttachMenu = true });
}; };
auto action = base::make_unique_q<BotAction>( auto action = base::make_unique_q<BotAction>(
raw, raw,

View file

@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/weak_ptr.h"
#include "base/flags.h" #include "base/flags.h"
#include "base/timer.h"
#include "base/weak_ptr.h"
#include "mtproto/sender.h" #include "mtproto/sender.h"
#include "ui/chat/attach/attach_bot_webview.h" #include "ui/chat/attach/attach_bot_webview.h"
#include "ui/rp_widget.h"
namespace Api { namespace Api {
struct SendAction; struct SendAction;
@ -60,9 +62,12 @@ struct AttachWebViewBot {
std::shared_ptr<Data::DocumentMedia> media; std::shared_ptr<Data::DocumentMedia> media;
QString name; QString name;
PeerTypes types = 0; PeerTypes types = 0;
bool inactive = false; bool inactive : 1 = false;
bool hasSettings = false; bool inMainMenu : 1 = false;
bool requestWriteAccess = false; bool inAttachMenu : 1 = false;
bool disclaimerRequired : 1 = false;
bool hasSettings : 1 = false;
bool requestWriteAccess : 1 = false;
}; };
class AttachWebView final class AttachWebView final
@ -76,7 +81,8 @@ public:
QString text; QString text;
QString startCommand; QString startCommand;
QByteArray url; QByteArray url;
bool fromMenu = false; bool fromAttachMenu = false;
bool fromMainMenu = false;
bool fromSwitch = false; bool fromSwitch = false;
}; };
void request( void request(
@ -116,10 +122,10 @@ public:
void requestAddToMenu( void requestAddToMenu(
not_null<UserData*> bot, not_null<UserData*> bot,
const QString &startCommand); std::optional<QString> startCommand);
void requestAddToMenu( void requestAddToMenu(
not_null<UserData*> bot, not_null<UserData*> bot,
const QString &startCommand, std::optional<QString> startCommand,
Window::SessionController *controller, Window::SessionController *controller,
std::optional<Api::SendAction> action, std::optional<Api::SendAction> action,
PeerTypes chooseTypes); PeerTypes chooseTypes);
@ -171,6 +177,9 @@ private:
void confirmOpen( void confirmOpen(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
Fn<void()> done); Fn<void()> done);
void acceptDisclaimer(
not_null<Window::SessionController*> controller,
Fn<void()> done);
enum class ToggledState { enum class ToggledState {
Removed, Removed,
@ -200,6 +209,8 @@ private:
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
base::Timer _refreshTimer;
std::unique_ptr<Context> _context; std::unique_ptr<Context> _context;
std::unique_ptr<Context> _lastShownContext; std::unique_ptr<Context> _lastShownContext;
QString _lastShownUrl; QString _lastShownUrl;
@ -221,7 +232,7 @@ private:
std::unique_ptr<Context> _addToMenuContext; std::unique_ptr<Context> _addToMenuContext;
UserData *_addToMenuBot = nullptr; UserData *_addToMenuBot = nullptr;
mtpRequestId _addToMenuId = 0; mtpRequestId _addToMenuId = 0;
QString _addToMenuStartCommand; std::optional<QString> _addToMenuStartCommand;
base::weak_ptr<Window::SessionController> _addToMenuChooseController; base::weak_ptr<Window::SessionController> _addToMenuChooseController;
PeerTypes _addToMenuChooseTypes; PeerTypes _addToMenuChooseTypes;
@ -239,4 +250,21 @@ private:
Fn<Api::SendAction()> actionFactory, Fn<Api::SendAction()> actionFactory,
Fn<void(bool)> attach); Fn<void(bool)> attach);
class MenuBotIcon final : public Ui::RpWidget {
public:
MenuBotIcon(
QWidget *parent,
std::shared_ptr<Data::DocumentMedia> media);
private:
void paintEvent(QPaintEvent *e) override;
void validate();
std::shared_ptr<Data::DocumentMedia> _media;
QImage _image;
QImage _mask;
};
} // namespace InlineBots } // namespace InlineBots

View file

@ -735,41 +735,9 @@ void Panel::requestTermsAcceptance(
(*update) = [=] { row->update(); }; (*update) = [=] { row->update(); };
struct State { const auto showError = Ui::CheckView::PrepareNonToggledError(
bool error = false; check,
Ui::Animations::Simple errorAnimation; box->lifetime());
};
const auto state = box->lifetime().make_state<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());
box->addButton(tr::lng_payments_terms_accept(), [=] { box->addButton(tr::lng_payments_terms_accept(), [=] {
if (check->checked()) { if (check->checked()) {

View file

@ -2808,7 +2808,13 @@ void Account::writeTrustedBots() {
} }
void Account::readTrustedBots() { void Account::readTrustedBots() {
if (!_trustedBotsKey) return; if (_trustedBotsRead) {
return;
}
_trustedBotsRead = true;
if (!_trustedBotsKey) {
return;
}
FileReadDescriptor trusted; FileReadDescriptor trusted;
if (!ReadEncryptedFile(trusted, _trustedBotsKey, _basePath, _localKey)) { if (!ReadEncryptedFile(trusted, _trustedBotsKey, _basePath, _localKey)) {
@ -2845,10 +2851,7 @@ void Account::markBotTrustedOpenGame(PeerId botId) {
} }
bool Account::isBotTrustedOpenGame(PeerId botId) { bool Account::isBotTrustedOpenGame(PeerId botId) {
if (!_trustedBotsRead) { readTrustedBots();
readTrustedBots();
_trustedBotsRead = true;
}
const auto i = _trustedBots.find(botId); const auto i = _trustedBots.find(botId);
return (i != end(_trustedBots)) return (i != end(_trustedBots))
&& ((i->second & BotTrustFlag::NoOpenGame) == 0); && ((i->second & BotTrustFlag::NoOpenGame) == 0);
@ -2870,10 +2873,7 @@ void Account::markBotTrustedPayment(PeerId botId) {
} }
bool Account::isBotTrustedPayment(PeerId botId) { bool Account::isBotTrustedPayment(PeerId botId) {
if (!_trustedBotsRead) { readTrustedBots();
readTrustedBots();
_trustedBotsRead = true;
}
const auto i = _trustedBots.find(botId); const auto i = _trustedBots.find(botId);
return (i != end(_trustedBots)) return (i != end(_trustedBots))
&& ((i->second & BotTrustFlag::Payment) != 0); && ((i->second & BotTrustFlag::Payment) != 0);
@ -2895,10 +2895,7 @@ void Account::markBotTrustedOpenWebView(PeerId botId) {
} }
bool Account::isBotTrustedOpenWebView(PeerId botId) { bool Account::isBotTrustedOpenWebView(PeerId botId) {
if (!_trustedBotsRead) { readTrustedBots();
readTrustedBots();
_trustedBotsRead = true;
}
const auto i = _trustedBots.find(botId); const auto i = _trustedBots.find(botId);
return (i != end(_trustedBots)) return (i != end(_trustedBots))
&& ((i->second & BotTrustFlag::OpenWebView) != 0); && ((i->second & BotTrustFlag::OpenWebView) != 0);

View file

@ -184,9 +184,9 @@ private:
Failed, Failed,
}; };
enum class BotTrustFlag : uchar { enum class BotTrustFlag : uchar {
NoOpenGame = (1 << 0), NoOpenGame = (1 << 0),
Payment = (1 << 1), Payment = (1 << 1),
OpenWebView = (1 << 2), OpenWebView = (1 << 2),
}; };
friend inline constexpr bool is_flag_type(BotTrustFlag) { return true; }; friend inline constexpr bool is_flag_type(BotTrustFlag) { return true; };

View file

@ -535,12 +535,17 @@ bool Panel::showWebview(
callback(tr::lng_bot_reload_page(tr::now), [=] { callback(tr::lng_bot_reload_page(tr::now), [=] {
_webview->window.reload(); _webview->window.reload();
}, &st::menuIconRestore); }, &st::menuIconRestore);
if (_menuButtons & MenuButton::RemoveFromMenu) { const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu);
if (main || (_menuButtons & MenuButton::RemoveFromMenu)) {
const auto handler = [=] { const auto handler = [=] {
_delegate->botHandleMenuButton(MenuButton::RemoveFromMenu); _delegate->botHandleMenuButton(main
? MenuButton::RemoveFromMainMenu
: MenuButton::RemoveFromMenu);
}; };
callback({ 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, .handler = handler,
.icon = &st::menuIconDeleteAttention, .icon = &st::menuIconDeleteAttention,
.isAttention = true, .isAttention = true,

View file

@ -36,10 +36,11 @@ struct MainButtonArgs {
}; };
enum class MenuButton { enum class MenuButton {
None = 0x00, None = 0x00,
Settings = 0x01, Settings = 0x01,
OpenBot = 0x02, OpenBot = 0x02,
RemoveFromMenu = 0x04, RemoveFromMenu = 0x04,
RemoveFromMainMenu = 0x08,
}; };
inline constexpr bool is_flag_type(MenuButton) { return true; } inline constexpr bool is_flag_type(MenuButton) { return true; }
using MenuButtons = base::flags<MenuButton>; using MenuButtons = base::flags<MenuButton>;

View file

@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/empty_userpic.h" #include "ui/empty_userpic.h"
#include "ui/unread_badge_paint.h" #include "ui/unread_badge_paint.h"
#include "base/call_delayed.h" #include "base/call_delayed.h"
#include "inline_bots/bot_attach_web_view.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
#include "storage/storage_account.h" #include "storage/storage_account.h"
@ -198,6 +199,45 @@ void ShowCallsBox(not_null<Window::SessionController*> window) {
}) | rpl::flatten_latest(); }) | rpl::flatten_latest();
} }
void SetupMenuBots(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller) {
const auto wrap = container->add(
object_ptr<Ui::VerticalLayout>(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<InlineBots::MenuBotIcon>(
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 } // namespace
class MainMenu::ToggleAccountsButton final : public Ui::AbstractButton { class MainMenu::ToggleAccountsButton final : public Ui::AbstractButton {
@ -784,6 +824,8 @@ void MainMenu::setupMenu() {
Info::Stories::Make(controller->session().user())); Info::Stories::Make(controller->session().user()));
}); });
SetupMenuBots(_menu, controller);
addAction( addAction(
tr::lng_menu_contacts(), tr::lng_menu_contacts(),
{ &st::menuIconProfile } { &st::menuIconProfile }

View file

@ -596,6 +596,13 @@ void SessionNavigation::showPeerByLinkResolved(
contextUser->owner().history(contextUser)) contextUser->owner().history(contextUser))
: std::optional<Api::SendAction>()), : std::optional<Api::SendAction>()),
info.attachBotChooseTypes); info.attachBotChooseTypes);
} else if (bot && info.attachBotMenuOpen) {
bot->session().attachWebView().requestAddToMenu(
bot,
std::nullopt,
parentController(),
std::optional<Api::SendAction>(),
{});
} else { } else {
crl::on_main(this, [=] { crl::on_main(this, [=] {
showPeerHistory(peer, params, msgId); showPeerHistory(peer, params, msgId);

View file

@ -212,6 +212,7 @@ public:
bool botAppForceConfirmation = false; bool botAppForceConfirmation = false;
QString attachBotUsername; QString attachBotUsername;
std::optional<QString> attachBotToggleCommand; std::optional<QString> attachBotToggleCommand;
bool attachBotMenuOpen = false;
InlineBots::PeerTypes attachBotChooseTypes; InlineBots::PeerTypes attachBotChooseTypes;
std::optional<QString> voicechatHash; std::optional<QString> voicechatHash;
FullMsgId clickFromMessageId; FullMsgId clickFromMessageId;