From ef969df86e37d16c262bb0dc391b11c17ac6b51e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 8 Sep 2023 21:55:28 +0400
Subject: [PATCH] Improve main menu bots disclaimer acceptance.

---
 Telegram/Resources/langs/lang.strings         |   2 +
 Telegram/SourceFiles/data/data_bot_app.h      |   1 +
 .../inline_bots/bot_attach_web_view.cpp       | 248 ++++++++++--------
 .../inline_bots/bot_attach_web_view.h         |   5 +-
 Telegram/SourceFiles/ui/boxes/confirm_box.cpp |   4 +-
 Telegram/SourceFiles/ui/boxes/confirm_box.h   |   1 +
 .../ui/chat/attach/attach_bot_webview.cpp     |   4 -
 .../SourceFiles/window/window_main_menu.cpp   |  61 ++++-
 8 files changed, 205 insertions(+), 121 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 4701564006..0d0c1e25d2 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2268,6 +2268,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_bot_reload_page" = "Reload Page";
 "lng_bot_add_to_menu" = "{bot} asks your permission to be added as an option to your attachments menu so you can access it from any chat.";
 "lng_bot_add_to_menu_done" = "Bot added to the menu.";
+"lng_bot_will_be_added" = "{bot} shortcuts will be added to the attachment options and the main menu.";
+"lng_bot_side_menu_new" = "NEW";
 "lng_bot_menu_not_supported" = "This bot isn't supported in the attach menu.";
 "lng_bot_menu_already_added" = "This bot is already added in your attach menu.";
 "lng_bot_menu_button" = "Menu";
diff --git a/Telegram/SourceFiles/data/data_bot_app.h b/Telegram/SourceFiles/data/data_bot_app.h
index 7de4aefe86..715fb03c06 100644
--- a/Telegram/SourceFiles/data/data_bot_app.h
+++ b/Telegram/SourceFiles/data/data_bot_app.h
@@ -24,4 +24,5 @@ struct BotAppData {
 
 	uint64 accessHash = 0;
 	uint64 hash = 0;
+	bool hasSettings = false;
 };
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index 3d1c241e4d..876dadf873 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -124,6 +124,10 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
 	if (result && result->icon) {
 		result->icon->forceToCache(true);
 	}
+	if (const auto icon = result->icon) {
+		result->media = icon->createMediaView();
+		icon->save(Data::FileOrigin(), {});
+	}
 	return result;
 }
 
@@ -194,6 +198,78 @@ void ShowChooseBox(
 	return result;
 }
 
+void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
+	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)()) {
+			done();
+			close();
+		}
+	};
+
+	const auto padding = st::boxRowPadding;
+	Ui::ConfirmBox(box, {
+		.text = tr::lng_mini_apps_disclaimer_text(
+			tr::now,
+			Ui::Text::RichLangValue),
+		.confirmed = callback,
+		.confirmText = tr::lng_box_ok(),
+		.labelPadding = QMargins(padding.left(), 0, padding.right(), 0),
+		.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(),
+			0,
+		});
+	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;
+	};
+}
+
 class BotAction final : public Ui::Menu::ItemBase {
 public:
 	BotAction(
@@ -818,10 +894,6 @@ void AttachWebView::requestBots() {
 			_attachBots.reserve(data.vbots().v.size());
 			for (const auto &bot : data.vbots().v) {
 				if (auto parsed = ParseAttachBot(_session, bot)) {
-					if (const auto icon = parsed->icon) {
-						parsed->media = icon->createMediaView();
-						icon->save(Data::FileOrigin(), {});
-					}
 					_attachBots.push_back(std::move(*parsed));
 				}
 			}
@@ -832,6 +904,11 @@ void AttachWebView::requestBots() {
 	}).send();
 }
 
+bool AttachWebView::showingDisclaimer(const AttachWebViewBot &bot) const {
+	return bot.disclaimerRequired
+		&& !_disclaimerAccepted.contains(bot.user);
+}
+
 void AttachWebView::requestAddToMenu(
 		not_null<UserData*> bot,
 		std::optional<QString> startCommand) {
@@ -885,9 +962,7 @@ void AttachWebView::requestAddToMenu(
 				}
 			} else if (!startCommand) {
 				_bot = bot;
-				acceptDisclaimer(strong, [=] {
-					requestSimple(strong, bot, { .fromMainMenu = true });
-				});
+				requestSimple(strong, bot, { .fromMainMenu = true });
 				return true;
 			} else if (const auto useTypes = chooseTypes & types) {
 				const auto done = [=](not_null<Data::Thread*> thread) {
@@ -1128,6 +1203,7 @@ void AttachWebView::requestApp(
 			_bot->id,
 			data.vapp());
 		_app = received ? received : already;
+		_app->hasSettings = data.is_has_settings();
 		if (!_app) {
 			cancel();
 			showToast(tr::lng_username_app_not_found(tr::now));
@@ -1140,8 +1216,8 @@ void AttachWebView::requestApp(
 			requestAppView(false);
 		}
 	}).fail([=] {
-		cancel();
 		showToast(tr::lng_username_app_not_found(tr::now));
+		cancel();
 	}).send();
 }
 
@@ -1190,13 +1266,14 @@ void AttachWebView::requestAppView(bool allowWrite) {
 		return;
 	}
 	using Flag = MTPmessages_RequestAppWebView::Flag;
+	const auto app = _app;
 	const auto flags = Flag::f_theme_params
 		| (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param)
 		| (allowWrite ? Flag::f_write_allowed : Flag(0));
 	_requestId = _session->api().request(MTPmessages_RequestAppWebView(
 		MTP_flags(flags),
 		_context->action.history->peer->input,
-		MTP_inputBotAppID(MTP_long(_app->id), MTP_long(_app->accessHash)),
+		MTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)),
 		MTP_string(_startCommand),
 		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
 		MTP_string("tdesktop")
@@ -1204,7 +1281,7 @@ void AttachWebView::requestAppView(bool allowWrite) {
 		_requestId = 0;
 		const auto &data = result.data();
 		const auto queryId = uint64();
-		show(queryId, qs(data.vurl()));
+		show(queryId, qs(data.vurl()), QString(), false, app);
 	}).fail([=](const MTP::Error &error) {
 		_requestId = 0;
 		if (error.type() == u"BOT_INVALID"_q) {
@@ -1256,93 +1333,17 @@ void AttachWebView::acceptDisclaimer(
 	} else if (i->inactive) {
 		requestAddToMenu(_bot, {}, controller, {}, {});
 		return;
-	} else if (!i->disclaimerRequired) {
+	} else if (!showingDisclaimer(*i)) {
 		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;
-		};
-	}));
+	controller->show(Box(FillDisclaimerBox, crl::guard(this, [=] {
+		_disclaimerAccepted.emplace(_bot);
+		_attachBotsUpdates.fire({});
+		done();
+	})));
 }
 
 void AttachWebView::ClearAll() {
@@ -1355,7 +1356,8 @@ void AttachWebView::show(
 		uint64 queryId,
 		const QString &url,
 		const QString &buttonText,
-		bool allowClipboardRead) {
+		bool allowClipboardRead,
+		const BotAppData *app) {
 	Expects(_bot != nullptr && _context != nullptr);
 
 	auto title = Info::Profile::NameValue(_bot);
@@ -1366,12 +1368,15 @@ void AttachWebView::show(
 		_attachBots,
 		not_null{ _bot },
 		&AttachWebViewBot::user);
-	const auto hasSettings = (attached != end(_attachBots))
-		&& !attached->inactive
-		&& attached->hasSettings;
+	const auto hasSettings = app
+		? app->hasSettings
+		: ((attached != end(_attachBots))
+			&& !attached->inactive
+			&& attached->hasSettings);
 	const auto hasOpenBot = !_context
 		|| (_bot != _context->action.history->peer);
-	const auto hasRemoveFromMenu = (attached != end(_attachBots))
+	const auto hasRemoveFromMenu = !app
+		&& (attached != end(_attachBots))
 		&& (!attached->inactive || attached->inMainMenu);
 	const auto buttons = (hasSettings ? Button::Settings : Button::None)
 		| (hasOpenBot ? Button::OpenBot : Button::None)
@@ -1473,16 +1478,25 @@ void AttachWebView::confirmAddToMenu(
 			});
 			close();
 		};
-		Ui::ConfirmBox(box, {
-			(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,
-		});
+		const auto disclaimer = showingDisclaimer(bot);
+		if (disclaimer) {
+			FillDisclaimerBox(box, [=] {
+				_disclaimerAccepted.emplace(bot.user);
+				_attachBotsUpdates.fire({});
+				done([] {});
+			});
+		} else {
+			Ui::ConfirmBox(box, {
+				(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) {
 			(*allowed) = box->addRow(
 				object_ptr<Ui::Checkbox>(
@@ -1496,11 +1510,27 @@ void AttachWebView::confirmAddToMenu(
 					st::urlAuthCheckbox),
 				style::margins(
 					st::boxRowPadding.left(),
-					st::boxPhotoCaptionSkip,
+					(disclaimer
+						? st::boxPhotoCaptionSkip
+						: st::boxRowPadding.left()),
 					st::boxRowPadding.right(),
-					st::boxPhotoCaptionSkip));
+					st::boxRowPadding.left()));
 			(*allowed)->setAllowTextLines();
 		}
+		if (disclaimer) {
+			if (!bot.requestWriteAccess) {
+				box->addRow(object_ptr<Ui::FixedHeightWidget>(
+					box,
+					st::boxRowPadding.left()));
+			}
+			box->addRow(object_ptr<Ui::FlatLabel>(
+				box,
+				tr::lng_bot_will_be_added(
+					lt_bot,
+					rpl::single(Ui::Text::Bold(bot.name)),
+					Ui::Text::WithEntities),
+				st::boxLabel));
+		}
 	}));
 }
 
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index 926312ea53..665000755e 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -119,6 +119,7 @@ public:
 	[[nodiscard]] rpl::producer<> attachBotsUpdates() const {
 		return _attachBotsUpdates.events();
 	}
+	[[nodiscard]] bool showingDisclaimer(const AttachWebViewBot &bot) const;
 
 	void requestAddToMenu(
 		not_null<UserData*> bot,
@@ -195,7 +196,8 @@ private:
 		uint64 queryId,
 		const QString &url,
 		const QString &buttonText = QString(),
-		bool allowClipboardRead = false);
+		bool allowClipboardRead = false,
+		const BotAppData *app = nullptr);
 	void confirmAddToMenu(
 		AttachWebViewBot bot,
 		Fn<void()> callback = nullptr);
@@ -238,6 +240,7 @@ private:
 
 	std::vector<AttachWebViewBot> _attachBots;
 	rpl::event_stream<> _attachBotsUpdates;
+	base::flat_set<not_null<UserData*>> _disclaimerAccepted;
 
 	std::unique_ptr<Ui::BotWebView::Panel> _panel;
 
diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp
index 070f84b294..22b2b39875 100644
--- a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp
+++ b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp
@@ -24,7 +24,9 @@ void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {
 
 	if (!v::is_null(args.text)) {
 		const auto padding = st::boxPadding;
-		const auto use = withTitle
+		const auto use = args.labelPadding
+			? *args.labelPadding
+			: withTitle
 			? QMargins(padding.left(), 0, padding.right(), padding.bottom())
 			: padding;
 		const auto label = box->addRow(
diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.h b/Telegram/SourceFiles/ui/boxes/confirm_box.h
index b66be3d3a7..b65a04361f 100644
--- a/Telegram/SourceFiles/ui/boxes/confirm_box.h
+++ b/Telegram/SourceFiles/ui/boxes/confirm_box.h
@@ -30,6 +30,7 @@ struct ConfirmBoxArgs {
 
 	const style::FlatLabel *labelStyle = nullptr;
 	Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)> labelFilter;
+	std::optional<QMargins> labelPadding;
 
 	v::text::data title = v::null;
 
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index 45762cf002..97e9bb74b0 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -561,10 +561,6 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
 	_widget->showInner(std::move(outer));
 	_webviewParent = container;
 
-	container->paintRequest() | rpl::start_with_next([=] {
-		QPainter(container).fillRect(container->rect(), QColor(0, 128, 0, 255));
-	}, container->lifetime());
-
 	_webviewBottom = std::make_unique<RpWidget>(_widget.get());
 	const auto bottom = _webviewBottom.get();
 	bottom->show();
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index e64a9b7680..7974b64a4c 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -211,15 +211,19 @@ void SetupMenuBots(
 	) | rpl::then(
 		bots->attachBotsUpdates()
 	) | rpl::start_with_next([=] {
+		const auto width = wrap->widthNoMargins();
 		wrap->clear();
 		for (const auto &bot : bots->attachBots()) {
 			if (!bot.inMainMenu) {
 				continue;
 			}
 			const auto button = Settings::AddButton(
-				container,
+				wrap,
 				rpl::single(bot.name),
 				st::mainMenuButton);
+			const auto menu = button->lifetime().make_state<
+				base::unique_qptr<Ui::PopupMenu>
+			>();
 			const auto icon = Ui::CreateChild<InlineBots::MenuBotIcon>(
 				button.get(),
 				bot.media);
@@ -229,12 +233,57 @@ void SetupMenuBots(
 					st::mainMenuButton.iconLeft,
 					(height - icon->height()) / 2);
 			}, button->lifetime());
-			button->setClickedCallback([=] {
-				bots->requestSimple(controller, bot.user, {
-					.fromMainMenu = true,
-				});
-			});
+			const auto user = bot.user;
+			button->setAcceptBoth(true);
+			button->clicks(
+			) | rpl::start_with_next([=](Qt::MouseButton which) {
+				if (which == Qt::LeftButton) {
+					bots->requestSimple(controller, user, {
+						.fromMainMenu = true,
+					});
+				} else {
+					(*menu) = nullptr;
+					(*menu) = base::make_unique_q<Ui::PopupMenu>(
+						button,
+						st::popupMenuWithIcons);
+					(*menu)->addAction(
+						tr::lng_bot_remove_from_menu(tr::now),
+						[=] { bots->removeFromMenu(user); },
+						&st::menuIconDelete);
+					(*menu)->popup(QCursor::pos());
+				}
+			}, button->lifetime());
+
+			const auto badge = bots->showingDisclaimer(bot)
+				? Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
+					button.get(),
+					object_ptr<Ui::FlatLabel>(
+						button,
+						tr::lng_bot_side_menu_new(),
+						st::settingsPremiumNewBadge),
+					st::settingsPremiumNewBadgePadding)
+				: nullptr;
+			if (badge) {
+				badge->setAttribute(Qt::WA_TransparentForMouseEvents);
+				badge->paintRequest() | rpl::start_with_next([=] {
+					auto p = QPainter(badge);
+					auto hq = PainterHighQualityEnabler(p);
+					p.setPen(Qt::NoPen);
+					p.setBrush(st::windowBgActive);
+					const auto r = st::settingsPremiumNewBadgePadding.left();
+					p.drawRoundedRect(badge->rect(), r, r);
+				}, badge->lifetime());
+
+				button->sizeValue(
+				) | rpl::start_with_next([=](QSize size) {
+					badge->moveToRight(
+						st::mainMenuButton.padding.right(),
+						(size.height() - badge->height()) / 2,
+						size.width());
+				}, badge->lifetime());
+			}
 		}
+		wrap->resizeToWidth(width);
 	}, wrap->lifetime());
 }