From 9032489786f7cb39b1ddbf31dcf5c558bc5dbb22 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 28 Feb 2025 17:26:02 +0400
Subject: [PATCH] Add pays-me status bar in chat.

---
 .../chat_helpers/chat_helpers.style           |   4 +
 Telegram/SourceFiles/data/data_changes.h      |  31 ++--
 Telegram/SourceFiles/data/data_peer.cpp       |  34 +++-
 Telegram/SourceFiles/data/data_peer.h         |   3 +
 .../SourceFiles/history/history_widget.cpp    |  29 +++-
 Telegram/SourceFiles/history/history_widget.h |   2 +
 .../view/history_view_contact_status.cpp      | 164 +++++++++++++++++-
 .../view/history_view_contact_status.h        |  37 +++-
 8 files changed, 284 insertions(+), 20 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index d0d59bb5a..8d265ab8a 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -918,6 +918,10 @@ historyBusinessBotSettings: IconButton(defaultIconButton) {
 	height: 58px;
 	width: 48px;
 }
+paysStatusLabel: FlatLabel(historyBusinessBotStatus) {
+	align: align(top);
+	minWidth: 240px;
+}
 
 historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }};
 historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }};
diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h
index 9e19fc617..f44ae72bb 100644
--- a/Telegram/SourceFiles/data/data_changes.h
+++ b/Telegram/SourceFiles/data/data_changes.h
@@ -95,27 +95,28 @@ struct PeerUpdate {
 		Birthday            = (1ULL << 33),
 		PersonalChannel     = (1ULL << 34),
 		StarRefProgram      = (1ULL << 35),
+		PaysPerMessage      = (1ULL << 36),
 
 		// For chats and channels
-		InviteLinks         = (1ULL << 36),
-		Members             = (1ULL << 37),
-		Admins              = (1ULL << 38),
-		BannedUsers         = (1ULL << 39),
-		Rights              = (1ULL << 40),
-		PendingRequests     = (1ULL << 41),
-		Reactions           = (1ULL << 42),
+		InviteLinks         = (1ULL << 37),
+		Members             = (1ULL << 38),
+		Admins              = (1ULL << 39),
+		BannedUsers         = (1ULL << 40),
+		Rights              = (1ULL << 41),
+		PendingRequests     = (1ULL << 42),
+		Reactions           = (1ULL << 43),
 
 		// For channels
-		ChannelAmIn         = (1ULL << 43),
-		StickersSet         = (1ULL << 44),
-		EmojiSet            = (1ULL << 45),
-		ChannelLinkedChat   = (1ULL << 46),
-		ChannelLocation     = (1ULL << 47),
-		Slowmode            = (1ULL << 48),
-		GroupCall           = (1ULL << 49),
+		ChannelAmIn         = (1ULL << 44),
+		StickersSet         = (1ULL << 45),
+		EmojiSet            = (1ULL << 46),
+		ChannelLinkedChat   = (1ULL << 47),
+		ChannelLocation     = (1ULL << 48),
+		Slowmode            = (1ULL << 49),
+		GroupCall           = (1ULL << 50),
 
 		// For iteration
-		LastUsedBit         = (1ULL << 49),
+		LastUsedBit         = (1ULL << 50),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index ad9449593..988c12dd8 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -734,7 +734,7 @@ void PeerData::checkFolder(FolderId folderId) {
 
 void PeerData::clearBusinessBot() {
 	if (const auto details = _barDetails.get()) {
-		if (details->requestChatDate) {
+		if (details->requestChatDate || details->paysPerMessage) {
 			details->businessBot = nullptr;
 			details->businessBotManageUrl = QString();
 		} else {
@@ -777,7 +777,10 @@ void PeerData::saveTranslationDisabled(bool disabled) {
 
 void PeerData::setBarSettings(const MTPPeerSettings &data) {
 	data.match([&](const MTPDpeerSettings &data) {
-		if (!data.vbusiness_bot_id() && !data.vrequest_chat_title()) {
+		const auto wasPaysPerMessage = paysPerMessage();
+		if (!data.vbusiness_bot_id()
+			&& !data.vrequest_chat_title()
+			&& !data.vcharge_paid_message_stars()) {
 			_barDetails = nullptr;
 		} else if (!_barDetails) {
 			_barDetails = std::make_unique<PeerBarDetails>();
@@ -792,6 +795,8 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) {
 				: nullptr;
 			_barDetails->businessBotManageUrl
 				= qs(data.vbusiness_bot_manage_url().value_or_empty());
+			_barDetails->paysPerMessage
+				= data.vcharge_paid_message_stars().value_or_empty();
 		}
 		using Flag = PeerBarSetting;
 		setBarSettings((data.is_add_contact() ? Flag::AddContact : Flag())
@@ -815,8 +820,33 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) {
 			| (data.is_business_bot_can_reply()
 				? Flag::BusinessBotCanReply
 				: Flag()));
+		if (wasPaysPerMessage != paysPerMessage()) {
+			session().changes().peerUpdated(
+				this,
+				UpdateFlag::PaysPerMessage);
+		}
 	});
 }
+
+int PeerData::paysPerMessage() const {
+	return _barDetails ? _barDetails->paysPerMessage : 0;
+}
+
+void PeerData::clearPaysPerMessage() {
+	if (const auto details = _barDetails.get()) {
+		if (details->paysPerMessage) {
+			if (details->businessBot || details->requestChatDate) {
+				details->paysPerMessage = 0;
+			} else {
+				_barDetails = nullptr;
+			}
+			session().changes().peerUpdated(
+				this,
+				UpdateFlag::PaysPerMessage);
+		}
+	}
+}
+
 QString PeerData::requestChatTitle() const {
 	return _barDetails ? _barDetails->requestChatTitle : QString();
 }
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index dcec75edf..d0b6d687a 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -177,6 +177,7 @@ struct PeerBarDetails {
 	TimeId requestChatDate;
 	UserData *businessBot = nullptr;
 	QString businessBotManageUrl;
+	int paysPerMessage = 0;
 };
 
 class PeerData {
@@ -412,6 +413,8 @@ public:
 			? _barSettings.changes()
 			: (_barSettings.value() | rpl::type_erased());
 	}
+	[[nodiscard]] int paysPerMessage() const;
+	void clearPaysPerMessage();
 	[[nodiscard]] QString requestChatTitle() const;
 	[[nodiscard]] TimeId requestChatDate() const;
 	[[nodiscard]] UserData *businessBot() const;
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index c862ccc5e..c7c55f463 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -1693,6 +1693,9 @@ void HistoryWidget::orderWidgets() {
 	if (_contactStatus) {
 		_contactStatus->bar().raise();
 	}
+	if (_paysStatus) {
+		_paysStatus->bar().raise();
+	}
 	if (_translateBar) {
 		_translateBar->raise();
 	}
@@ -2416,6 +2419,7 @@ void HistoryWidget::showHistory(
 	_showAtMsgId = showAtMsgId;
 	_showAtMsgParams = params;
 	_historyInited = false;
+	_paysStatus = nullptr;
 	_contactStatus = nullptr;
 	_businessBotStatus = nullptr;
 
@@ -2436,6 +2440,14 @@ void HistoryWidget::showHistory(
 
 		refreshGiftToChannelShown();
 		if (const auto user = _peer->asUser()) {
+			_paysStatus = std::make_unique<PaysStatus>(
+				controller(),
+				this,
+				user);
+			_paysStatus->bar().heightValue(
+			) | rpl::start_with_next([=] {
+				updateControlsGeometry();
+			}, _paysStatus->bar().lifetime());
 			_businessBotStatus = std::make_unique<BusinessBotStatus>(
 				controller(),
 				this,
@@ -3077,6 +3089,9 @@ void HistoryWidget::updateControlsVisibility() {
 	if (_requestsBar) {
 		_requestsBar->show();
 	}
+	if (_paysStatus) {
+		_paysStatus->show();
+	}
 	if (_contactStatus) {
 		_contactStatus->show();
 	}
@@ -4305,6 +4320,9 @@ void HistoryWidget::hideChildWidgets() {
 	if (_chooseTheme) {
 		_chooseTheme->hide();
 	}
+	if (_paysStatus) {
+		_paysStatus->hide();
+	}
 	if (_contactStatus) {
 		_contactStatus->hide();
 	}
@@ -6266,8 +6284,13 @@ void HistoryWidget::updateControlsGeometry() {
 		_translateBar->move(0, translateTop);
 		_translateBar->resizeToWidth(width());
 	}
-	const auto contactStatusTop = translateTop
+	const auto paysStatusTop = translateTop
 		+ (_translateBar ? _translateBar->height() : 0);
+	if (_paysStatus) {
+		_paysStatus->bar().move(0, paysStatusTop);
+	}
+	const auto contactStatusTop = paysStatusTop
+		+ (_paysStatus ? _paysStatus->bar().height() : 0);
 	if (_contactStatus) {
 		_contactStatus->bar().move(0, contactStatusTop);
 	}
@@ -6518,6 +6541,9 @@ void HistoryWidget::updateHistoryGeometry(
 	if (_requestsBar) {
 		newScrollHeight -= _requestsBar->height();
 	}
+	if (_paysStatus) {
+		newScrollHeight -= _paysStatus->bar().height();
+	}
 	if (_contactStatus) {
 		newScrollHeight -= _contactStatus->bar().height();
 	}
@@ -6940,6 +6966,7 @@ void HistoryWidget::botCallbackSent(not_null<HistoryItem*> item) {
 int HistoryWidget::computeMaxFieldHeight() const {
 	const auto available = height()
 		- _topBar->height()
+		- (_paysStatus ? _paysStatus->bar().height() : 0)
 		- (_contactStatus ? _contactStatus->bar().height() : 0)
 		- (_businessBotStatus ? _businessBotStatus->bar().height() : 0)
 		- (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0)
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index 2b956c2c1..e8dbd4f8c 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -100,6 +100,7 @@ namespace HistoryView {
 class StickerToast;
 class PaidReactionToast;
 class TopBarWidget;
+class PaysStatus;
 class ContactStatus;
 class BusinessBotStatus;
 class Element;
@@ -782,6 +783,7 @@ private:
 
 	Webrtc::RecordAvailability _recordAvailability = {};
 
+	std::unique_ptr<HistoryView::PaysStatus> _paysStatus;
 	std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
 	std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;
 
diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp
index a25ebeed0..60e1b4e86 100644
--- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text/text_utilities.h"
 #include "ui/boxes/confirm_box.h"
 #include "ui/layers/generic_box.h"
+#include "chat_helpers/message_field.h" // PaidSendButtonText
 #include "core/click_handler_types.h"
 #include "core/ui_integration.h"
 #include "data/business/data_business_chatbots.h"
@@ -595,9 +596,9 @@ auto ContactStatus::PeerState(not_null<PeerData*> peer)
 				return {
 					.type = Type::RequestChatInfo,
 					.requestChatName = peer->requestChatTitle(),
+					.requestDate = peer->requestChatDate(),
 					.requestChatIsBroadcast = !!(settings.value
 						& PeerBarSetting::RequestChatIsBroadcast),
-					.requestDate = peer->requestChatDate(),
 				};
 			} else if (settings.value & PeerBarSetting::AutoArchived) {
 				return { Type::UnarchiveOrBlock };
@@ -1131,4 +1132,165 @@ void TopicReopenBar::setupHandler() {
 	});
 }
 
+class PaysStatus::Bar final : public Ui::RpWidget {
+public:
+	Bar(QWidget *parent, not_null<PeerData*> peer);
+
+	void showState(State state);
+
+	[[nodiscard]] rpl::producer<> removeClicks() const;
+
+private:
+	void paintEvent(QPaintEvent *e) override;
+	int resizeGetHeight(int newWidth) override;
+
+	not_null<PeerData*> _peer;
+	object_ptr<Ui::FlatLabel> _label;
+	object_ptr<Ui::LinkButton> _remove;
+	rpl::event_stream<> _removeClicks;
+
+};
+
+PaysStatus::Bar::Bar(QWidget *parent, not_null<PeerData*> peer)
+: RpWidget(parent)
+, _peer(peer)
+, _label(this, st::paysStatusLabel)
+, _remove(this, tr::lng_payment_bar_button(tr::now)) {
+	_label->setAttribute(Qt::WA_TransparentForMouseEvents);
+}
+
+void PaysStatus::Bar::showState(State state) {
+	_label->setMarkedText(tr::lng_payment_bar_text(
+		tr::now,
+		lt_name,
+		TextWithEntities{ _peer->shortName() },
+		lt_cost,
+		PaidSendButtonText(tr::now, state.perMessage),
+		Ui::Text::WithEntities));
+	resizeToWidth(width());
+}
+
+rpl::producer<> PaysStatus::Bar::removeClicks() const {
+	return _remove->clicks() | rpl::to_empty;
+}
+
+void PaysStatus::Bar::paintEvent(QPaintEvent *e) {
+	QPainter p(this);
+	p.fillRect(e->rect(), st::historyContactStatusButton.bgColor);
+}
+
+int PaysStatus::Bar::resizeGetHeight(int newWidth) {
+	const auto skip = st::defaultPeerListItem.photoPosition.y();
+	_label->resizeToWidth(newWidth - skip);
+	_label->moveToLeft(skip, skip, newWidth);
+	_remove->move(
+		(newWidth - _remove->width()) / 2,
+		skip + _label->height() + skip);
+	return _remove->y() + _remove->height() + skip;
+}
+
+PaysStatus::PaysStatus(
+	not_null<Window::SessionController*> window,
+	not_null<Ui::RpWidget*> parent,
+	not_null<UserData*> user)
+: _controller(window)
+, _user(user)
+, _inner(Ui::CreateChild<Bar>(parent.get(), user))
+, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) {
+	setupState();
+	setupHandlers();
+}
+
+void PaysStatus::setupState() {
+	_user->session().api().requestPeerSettings(_user);
+
+	_user->session().changes().peerFlagsValue(
+		_user,
+		Data::PeerUpdate::Flag::PaysPerMessage
+	) | rpl::start_with_next([=] {
+		_state = State{ _user->paysPerMessage() };
+		if (_state.perMessage > 0) {
+			_inner->showState(_state);
+			_bar.toggleContent(true);
+		} else {
+			_bar.toggleContent(false);
+		}
+	}, _bar.lifetime());
+}
+
+void PaysStatus::setupHandlers() {
+	_inner->removeClicks(
+	) | rpl::start_with_next([=] {
+		const auto user = _user;
+		const auto exception = [=](bool refund) {
+			using Flag = MTPaccount_AddNoPaidMessagesException::Flag;
+			const auto api = &user->session().api();
+			api->request(MTPaccount_AddNoPaidMessagesException(
+				MTP_flags(refund ? Flag::f_refund_charged : Flag()),
+				user->inputUser
+			)).done([=] {
+				user->clearPaysPerMessage();
+			}).send();
+		};
+		_controller->show(Box([=](not_null<Ui::GenericBox*> box) {
+			const auto refund = std::make_shared<QPointer<Ui::Checkbox>>();
+			Ui::ConfirmBox(box, {
+				.text = tr::lng_payment_refund_text(
+					tr::now,
+					lt_name,
+					Ui::Text::Bold(user->shortName()),
+					Ui::Text::WithEntities),
+				.confirmed = [=](Fn<void()> close) {
+					exception(*refund && (*refund)->checked());
+					close();
+				},
+				.confirmText = tr::lng_payment_refund_confirm(tr::now),
+				.title = tr::lng_payment_refund_title(tr::now),
+			});
+			const auto paid = box->lifetime().make_state<
+				rpl::variable<int>
+			>();
+			*paid = _paidAlready.value();
+			paid->value() | rpl::start_with_next([=](int already) {
+				if (!already) {
+					delete base::take(*refund);
+				} else if (!*refund) {
+					const auto skip = st::defaultCheckbox.margin.top();
+					*refund = box->addRow(
+						object_ptr<Ui::Checkbox>(
+							box,
+							tr::lng_payment_refund_also(
+								lt_count,
+								paid->value() | tr::to_count()),
+							false,
+							st::defaultCheckbox),
+						st::boxRowPadding + QMargins(0, skip, 0, skip));
+				}
+			}, box->lifetime());
+
+			user->session().api().request(MTPaccount_GetPaidMessagesRevenue(
+				user->inputUser
+			)).done(crl::guard(_inner, [=](
+					const MTPaccount_PaidMessagesRevenue &result) {
+				_paidAlready = result.data().vstars_amount().v;
+			})).send();
+		}));
+	}, _bar.lifetime());
+}
+
+void PaysStatus::show() {
+	if (!_shown) {
+		_shown = true;
+		if (_state.perMessage > 0) {
+			_inner->showState(_state);
+			_bar.toggleContent(true);
+		}
+	}
+	_bar.show();
+}
+
+void PaysStatus::hide() {
+	_bar.hide();
+}
+
 } // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.h b/Telegram/SourceFiles/history/view/history_view_contact_status.h
index aafc23a6a..56be475a8 100644
--- a/Telegram/SourceFiles/history/view/history_view_contact_status.h
+++ b/Telegram/SourceFiles/history/view/history_view_contact_status.h
@@ -95,9 +95,10 @@ private:
 			RequestChatInfo,
 		};
 		Type type = Type::None;
+		int starsPerMessage = 0;
 		QString requestChatName;
-		bool requestChatIsBroadcast = false;
 		TimeId requestDate = 0;
+		bool requestChatIsBroadcast = false;
 	};
 
 	void setupState(not_null<PeerData*> peer, bool showInForum);
@@ -181,4 +182,38 @@ private:
 
 };
 
+class PaysStatus final {
+public:
+	PaysStatus(
+		not_null<Window::SessionController*> controller,
+		not_null<Ui::RpWidget*> parent,
+		not_null<UserData*> user);
+
+	void show();
+	void hide();
+
+	[[nodiscard]] SlidingBar &bar() {
+		return _bar;
+	}
+
+private:
+	class Bar;
+
+	struct State {
+		int perMessage = 0;
+	};
+
+	void setupState();
+	void setupHandlers();
+
+	const not_null<Window::SessionController*> _controller;
+	const not_null<UserData*> _user;
+	rpl::variable<int> _paidAlready;
+	State _state;
+	QPointer<Bar> _inner;
+	SlidingBar _bar;
+	bool _shown = false;
+
+};
+
 } // namespace HistoryView