From da426ae03b7dc1ee9eca4b096889bf39cefc107d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 27 Jan 2025 17:57:12 +0400
Subject: [PATCH] Introduce fast-buttons-bots for support mode.

---
 .../history/history_item_components.cpp       | 40 ++++++++-
 .../history/history_item_components.h         |  2 +
 .../SourceFiles/history/history_widget.cpp    | 40 +++++++++
 Telegram/SourceFiles/history/history_widget.h |  1 +
 .../history/view/history_view_message.cpp     | 31 +++----
 .../info/profile/info_profile_actions.cpp     | 33 +++++++-
 .../SourceFiles/storage/storage_account.cpp   |  4 +
 .../SourceFiles/storage/storage_account.h     |  1 +
 .../SourceFiles/support/support_helper.cpp    | 84 +++++++++++++++++++
 Telegram/SourceFiles/support/support_helper.h | 26 ++++--
 10 files changed, 238 insertions(+), 24 deletions(-)

diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index 2fe99415b..c57dfc6f3 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "main/main_session.h"
 #include "window/window_session_controller.h"
 #include "api/api_bot.h"
+#include "support/support_helper.h"
 #include "styles/style_boxes.h"
 #include "styles/style_chat.h"
 #include "styles/style_dialogs.h" // dialogsMiniReplyStory.
@@ -880,12 +881,17 @@ void ReplyKeyboard::paint(
 	Assert(_width > 0);
 
 	_st->startPaint(p, st);
+	auto number = hasFastButtonMode() ? 1 : 0;
 	for (auto y = 0, rowsCount = int(_rows.size()); y != rowsCount; ++y) {
 		for (auto x = 0, count = int(_rows[y].size()); x != count; ++x) {
+			const auto guard = gsl::finally([&] { if (number) ++number; });
 			const auto &button = _rows[y][x];
 			const auto rect = button.rect;
-			if (rect.y() >= clip.y() + clip.height()) return;
-			if (rect.y() + rect.height() < clip.y()) continue;
+			if (rect.y() >= clip.y() + clip.height()) {
+				return;
+			} else if (rect.y() + rect.height() < clip.y()) {
+				continue;
+			}
 
 			// just ignore the buttons that didn't layout well
 			if (rect.x() + rect.width() > _width) break;
@@ -904,10 +910,27 @@ void ReplyKeyboard::paint(
 				? Corner::Large
 				: Corner::Small;
 			_st->paintButton(p, st, outerWidth, button, buttonRounding);
+
+			if (number) {
+				p.setFont(st::dialogsUnreadFont);
+				p.setPen(st->msgServiceFg());
+				p.drawText(
+					rect.x() + st::msgBotKbIconPadding,
+					rect.y() + st::dialogsUnreadFont->ascent,
+					QString::number(number));
+			}
 		}
 	}
 }
 
+bool ReplyKeyboard::hasFastButtonMode() const {
+	return _item->inlineReplyKeyboard()
+		&& (_item == _item->history()->lastMessage())
+		&& _item->history()->session().supportMode()
+		&& _item->history()->session().supportHelper().fastButtonMode(
+			_item->history()->peer);
+}
+
 ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const {
 	Assert(_width > 0);
 
@@ -927,6 +950,19 @@ ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const {
 	return ClickHandlerPtr();
 }
 
+ClickHandlerPtr ReplyKeyboard::getLinkByIndex(int index) const {
+	auto number = 1;
+	for (const auto &row : _rows) {
+		for (const auto &button : row) {
+			if (number == index + 1) {
+				return button.link;
+			}
+			++number;
+		}
+	}
+	return ClickHandlerPtr();
+}
+
 void ReplyKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
 	if (!p) return;
 
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 8b38c294d..c2281496b 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -504,6 +504,7 @@ public:
 		int outerWidth,
 		const QRect &clip) const;
 	ClickHandlerPtr getLink(QPoint point) const;
+	ClickHandlerPtr getLinkByIndex(int index) const;
 
 	void clickHandlerActiveChanged(
 		const ClickHandlerPtr &p,
@@ -537,6 +538,7 @@ private:
 	};
 
 	void startAnimation(int i, int j, int direction);
+	[[nodiscard]] bool hasFastButtonMode() const;
 
 	ButtonCoords findButtonCoordsByClickHandler(const ClickHandlerPtr &p);
 
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 7eab91e35..66d3080a1 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -166,6 +166,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/ui_integration.h"
 #include "support/support_common.h"
 #include "support/support_autocomplete.h"
+#include "support/support_helper.h"
 #include "support/support_preload.h"
 #include "dialogs/dialogs_key.h"
 #include "calls/calls_instance.h"
@@ -467,6 +468,8 @@ HistoryWidget::HistoryWidget(
 	});
 	InitMessageFieldFade(_field, st::historyComposeField.textBg);
 
+	setupFastButtonMode();
+
 	_fieldCharsCountManager.limitExceeds(
 	) | rpl::start_with_next([=] {
 		const auto hide = _fieldCharsCountManager.isLimitExceeded();
@@ -2888,6 +2891,43 @@ void HistoryWidget::refreshSilentToggle() {
 	}
 }
 
+void HistoryWidget::setupFastButtonMode() {
+	if (!session().supportMode()) {
+		return;
+	}
+	const auto field = _field->rawTextEdit();
+	base::install_event_filter(field, [=](not_null<QEvent*> e) {
+		if (e->type() != QEvent::KeyPress
+			|| !_history
+			|| !session().supportHelper().fastButtonMode(_history->peer)
+			|| !_field->getLastText().isEmpty()) {
+			return base::EventFilterResult::Continue;
+		}
+		const auto k = static_cast<QKeyEvent*>(e.get());
+		const auto key = k->key();
+		if (key < Qt::Key_1 || key > Qt::Key_9 || k->modifiers()) {
+			return base::EventFilterResult::Continue;
+		}
+		const auto item = _history ? _history->lastMessage() : nullptr;
+		const auto markup = item ? item->inlineReplyKeyboard() : nullptr;
+		const auto link = markup
+			? markup->getLinkByIndex(key - Qt::Key_1)
+			: nullptr;
+		if (!link) {
+			return base::EventFilterResult::Continue;
+		}
+		const auto id = item->fullId();
+		ActivateClickHandler(window(), link, {
+			Qt::LeftButton,
+			QVariant::fromValue(ClickHandlerContext{
+				.itemId = item->fullId(),
+				.sessionWindow = base::make_weak(controller()),
+			}),
+		});
+		return base::EventFilterResult::Cancel;
+	});
+}
+
 void HistoryWidget::setupScheduledToggle() {
 	controller()->activeChatValue(
 	) | rpl::map([=](Dialogs::Key key) -> rpl::producer<> {
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index ad9c1b74c..7fa51470f 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -653,6 +653,7 @@ private:
 	[[nodiscard]] bool showRecordButton() const;
 	[[nodiscard]] bool showInlineBotCancel() const;
 	void refreshSilentToggle();
+	void setupFastButtonMode();
 
 	[[nodiscard]] bool isChoosingTheme() const;
 
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index c28069331..e323f3553 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -55,7 +55,7 @@ const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
 
 class KeyboardStyle : public ReplyKeyboard::Style {
 public:
-	KeyboardStyle(const style::BotKeyboardButton &st);
+	KeyboardStyle(const style::BotKeyboardButton &st, Fn<void()> repaint);
 
 	Images::CornersMaskRef buttonRounding(
 		Ui::BubbleRounding outer,
@@ -93,12 +93,16 @@ private:
 	mutable base::flat_map<BubbleRoundingKey, QImage> _cachedBg;
 	mutable base::flat_map<BubbleRoundingKey, QPainterPath> _cachedOutline;
 	mutable std::unique_ptr<Ui::GlareEffect> _glare;
+	Fn<void()> _repaint;
 	rpl::lifetime _lifetime;
 
 };
 
-KeyboardStyle::KeyboardStyle(const style::BotKeyboardButton &st)
-: ReplyKeyboard::Style(st) {
+KeyboardStyle::KeyboardStyle(
+	const style::BotKeyboardButton &st,
+	Fn<void()> repaint)
+: ReplyKeyboard::Style(st)
+, _repaint(std::move(repaint)) {
 	style::PaletteChanged(
 	) | rpl::start_with_next([=] {
 		_cachedBg = {};
@@ -120,15 +124,6 @@ const style::TextStyle &KeyboardStyle::textStyle() const {
 
 void KeyboardStyle::repaint(not_null<const HistoryItem*> item) const {
 	item->history()->owner().requestItemRepaint(item);
-	if (_glare && !_glare->glare.birthTime) {
-		constexpr auto kTimeout = crl::time(0);
-		constexpr auto kDuration = crl::time(1100);
-		_glare->validate(
-			st::premiumButtonFg->c,
-			[=] { repaint(item); },
-			kTimeout,
-			kDuration);
-	}
 }
 
 Images::CornersMaskRef KeyboardStyle::buttonRounding(
@@ -306,6 +301,11 @@ void KeyboardStyle::paintButtonLoading(
 		} else {
 			_glare = std::make_unique<Ui::GlareEffect>();
 			_glare->width = outerWidth;
+
+			constexpr auto kTimeout = crl::time(0);
+			constexpr auto kDuration = crl::time(1100);
+			const auto color = st::premiumButtonFg->c;
+			_glare->validate(color, _repaint, kTimeout, kDuration);
 		}
 	}
 }
@@ -3400,9 +3400,12 @@ void Message::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) {
 		|| markup->hiddenBy(data()->media())) {
 		return;
 	}
+	const auto item = data();
 	markup->inlineKeyboard = std::make_unique<ReplyKeyboard>(
-		data(),
-		std::make_unique<KeyboardStyle>(st::msgBotKbButton));
+		item,
+		std::make_unique<KeyboardStyle>(
+			st::msgBotKbButton,
+			[=] { item->history()->owner().requestItemRepaint(item); }));
 }
 
 void Message::validateFromNameText(PeerData *from) const {
diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
index ffa2472ca..bedf78abf 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -740,8 +740,7 @@ auto AddActionButton(
 		ToggleOn &&toggleOn,
 		Callback &&callback,
 		const style::icon *icon,
-		const style::SettingsButton &st
-			= st::infoSharedMediaButton) {
+		const style::SettingsButton &st = st::infoSharedMediaButton) {
 	auto result = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
 		parent,
 		object_ptr<Ui::SettingsButton>(
@@ -1018,6 +1017,7 @@ private:
 	void addEditContactAction(not_null<UserData*> user);
 	void addDeleteContactAction(not_null<UserData*> user);
 	void addBotCommandActions(not_null<UserData*> user);
+	void addFastButtonsMode(not_null<UserData*> user);
 	void addReportAction();
 	void addBlockAction(not_null<UserData*> user);
 	void addLeaveChannelAction(not_null<ChannelData*> channel);
@@ -2320,7 +2320,36 @@ void ActionsFiller::addDeleteContactAction(not_null<UserData*> user) {
 		&st::infoIconDelete);
 }
 
+void ActionsFiller::addFastButtonsMode(not_null<UserData*> user) {
+	Expects(user->isBot());
+
+	const auto helper = &user->session().supportHelper();
+	const auto button = _wrap->add(object_ptr<Ui::SettingsButton>(
+		_wrap,
+		rpl::single(u"Fast buttons mode"_q),
+		st::infoSharedMediaButton));
+	object_ptr<Info::Profile::FloatingIcon>(
+		button,
+		st::infoIconMediaBot,
+		st::infoSharedMediaButtonIconPosition);
+
+	AddSkip(_wrap);
+	AddDivider(_wrap);
+	AddSkip(_wrap);
+
+	button->toggleOn(helper->fastButtonModeValue(user));
+	button->toggledValue(
+	) | rpl::filter([=](bool value) {
+		return value != helper->fastButtonMode(user);
+	}) | rpl::start_with_next([=](bool value) {
+		helper->setFastButtonMode(user, value);
+	}, button->lifetime());
+}
+
 void ActionsFiller::addBotCommandActions(not_null<UserData*> user) {
+	if (user->session().supportMode()) {
+		addFastButtonsMode(user);
+	}
 	const auto window = _controller->parentController();
 	const auto findBotCommand = [user](const QString &command) {
 		if (!user->isBot()) {
diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp
index afa677ed4..468c291b3 100644
--- a/Telegram/SourceFiles/storage/storage_account.cpp
+++ b/Telegram/SourceFiles/storage/storage_account.cpp
@@ -162,6 +162,10 @@ QString Account::tempDirectory() const {
 	return _tempPath;
 }
 
+QString Account::supportModePath() const {
+	return _databasePath + u"support"_q;
+}
+
 StartResult Account::legacyStart(const QByteArray &passcode) {
 	const auto result = readMapWith(MTP::AuthKeyPtr(), passcode);
 	if (result == ReadMapResult::Failed) {
diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h
index a044b0a74..5ac7194b3 100644
--- a/Telegram/SourceFiles/storage/storage_account.h
+++ b/Telegram/SourceFiles/storage/storage_account.h
@@ -76,6 +76,7 @@ public:
 	}
 
 	[[nodiscard]] QString tempDirectory() const;
+	[[nodiscard]] QString supportModePath() const;
 
 	[[nodiscard]] MTP::AuthKeyPtr peekLegacyLocalKey() const {
 		return _localKey;
diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp
index 286f1fc6c..87a13da1b 100644
--- a/Telegram/SourceFiles/support/support_helper.cpp
+++ b/Telegram/SourceFiles/support/support_helper.cpp
@@ -28,16 +28,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/unixtime.h"
 #include "lang/lang_keys.h"
 #include "window/window_session_controller.h"
+#include "storage/storage_account.h"
 #include "storage/storage_media_prepare.h"
 #include "storage/localimageloader.h"
 #include "core/launcher.h"
 #include "core/application.h"
 #include "core/core_settings.h"
+#include "main/main_account.h"
 #include "main/main_session.h"
 #include "apiwrap.h"
 #include "styles/style_layers.h"
 #include "styles/style_boxes.h"
 
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonArray>
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -256,6 +261,12 @@ TimeId OccupiedBySomeoneTill(History *history) {
 	return valid ? result : 0;
 }
 
+QString FastButtonModeIdsPath(not_null<Main::Session*> session) {
+	const auto base = session->account().local().supportModePath();
+	QDir().mkpath(base);
+	return base + u"/fast_button_mode_ids.json"_q;
+}
+
 } // namespace
 
 Helper::Helper(not_null<Main::Session*> session)
@@ -472,6 +483,79 @@ UserInfo Helper::infoCurrent(not_null<UserData*> user) const {
 	return (i != end(_userInformation)) ? i->second : UserInfo();
 }
 
+void Helper::readFastButtonModeBots() {
+	_readFastButtonModeBots = true;
+
+	auto f = QFile(FastButtonModeIdsPath(_session));
+	if (!f.open(QIODevice::ReadOnly)) {
+		return;
+	}
+	const auto data = f.readAll();
+	const auto json = QJsonDocument::fromJson(data);
+	if (!json.isObject()) {
+		return;
+	}
+	const auto object = json.object();
+	const auto array = object.value(u"ids"_q).toArray();
+	for (const auto &value : array) {
+		const auto bareId = value.toString().toULongLong();
+		_fastButtonModeBots.emplace(PeerId(bareId));
+	}
+}
+
+void Helper::writeFastButtonModeBots() {
+	auto array = QJsonArray();
+	for (const auto &id : _fastButtonModeBots) {
+		array.append(QString::number(id.value));
+	}
+	auto object = QJsonObject();
+	object[u"ids"_q] = array;
+	auto f = QFile(FastButtonModeIdsPath(_session));
+	if (f.open(QIODevice::WriteOnly)) {
+		f.write(QJsonDocument(object).toJson(QJsonDocument::Indented));
+	}
+}
+
+bool Helper::fastButtonMode(not_null<PeerData*> peer) const {
+	if (!_readFastButtonModeBots) {
+		const_cast<Helper*>(this)->readFastButtonModeBots();
+	}
+	return _fastButtonModeBots.contains(peer->id);
+}
+
+rpl::producer<bool> Helper::fastButtonModeValue(
+		not_null<PeerData*> peer) const {
+	return rpl::single(
+		fastButtonMode(peer)
+	) | rpl::then(_fastButtonModeBotsChanges.events(
+	) | rpl::filter([=](PeerId id) {
+		return (peer->id == id);
+	}) | rpl::map([=] {
+		return fastButtonMode(peer);
+	}));
+}
+
+void Helper::setFastButtonMode(not_null<PeerData*> peer, bool fast) {
+	if (fast == fastButtonMode(peer)) {
+		return;
+	} else if (fast) {
+		_fastButtonModeBots.emplace(peer->id);
+	} else {
+		_fastButtonModeBots.remove(peer->id);
+	}
+	if (_fastButtonModeBots.empty()) {
+		QFile(FastButtonModeIdsPath(_session)).remove();
+	} else {
+		writeFastButtonModeBots();
+	}
+	_fastButtonModeBotsChanges.fire_copy(peer->id);
+	if (const auto history = peer->owner().history(peer)) {
+		if (const auto item = history->lastMessage()) {
+			history->owner().requestItemRepaint(item);
+		}
+	}
+}
+
 void Helper::editInfo(
 		not_null<Window::SessionController*> controller,
 		not_null<UserData*> user) {
diff --git a/Telegram/SourceFiles/support/support_helper.h b/Telegram/SourceFiles/support/support_helper.h
index fd46acd38..55c4f3199 100644
--- a/Telegram/SourceFiles/support/support_helper.h
+++ b/Telegram/SourceFiles/support/support_helper.h
@@ -50,19 +50,26 @@ public:
 
 	void chatOccupiedUpdated(not_null<History*> history);
 
-	bool isOccupiedByMe(History *history) const;
-	bool isOccupiedBySomeone(History *history) const;
+	[[nodiscard]] bool isOccupiedByMe(History *history) const;
+	[[nodiscard]] bool isOccupiedBySomeone(History *history) const;
 
 	void refreshInfo(not_null<UserData*> user);
-	rpl::producer<UserInfo> infoValue(not_null<UserData*> user) const;
-	rpl::producer<QString> infoLabelValue(not_null<UserData*> user) const;
-	rpl::producer<TextWithEntities> infoTextValue(
+	[[nodiscard]] rpl::producer<UserInfo> infoValue(
 		not_null<UserData*> user) const;
-	UserInfo infoCurrent(not_null<UserData*> user) const;
+	[[nodiscard]] rpl::producer<QString> infoLabelValue(
+		not_null<UserData*> user) const;
+	[[nodiscard]] rpl::producer<TextWithEntities> infoTextValue(
+		not_null<UserData*> user) const;
+	[[nodiscard]] UserInfo infoCurrent(not_null<UserData*> user) const;
 	void editInfo(
 		not_null<Window::SessionController*> controller,
 		not_null<UserData*> user);
 
+	[[nodiscard]] bool fastButtonMode(not_null<PeerData*> peer) const;
+	[[nodiscard]] rpl::producer<bool> fastButtonModeValue(
+		not_null<PeerData*> peer) const;
+	void setFastButtonMode(not_null<PeerData*> peer, bool fast);
+
 	Templates &templates();
 
 private:
@@ -90,6 +97,9 @@ private:
 		TextWithEntities text,
 		Fn<void(bool success)> done);
 
+	void writeFastButtonModeBots();
+	void readFastButtonModeBots();
+
 	not_null<Main::Session*> _session;
 	MTP::Sender _api;
 	Templates _templates;
@@ -107,6 +117,10 @@ private:
 		base::weak_ptr<Window::SessionController>> _userInfoEditPending;
 	base::flat_map<not_null<UserData*>, SavingInfo> _userInfoSaving;
 
+	base::flat_set<PeerId> _fastButtonModeBots;
+	rpl::event_stream<PeerId> _fastButtonModeBotsChanges;
+	bool _readFastButtonModeBots = false;
+
 	rpl::lifetime _lifetime;
 
 };