From 1684465e04c2232a26f46f5f9731bc4543782dbc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 25 Feb 2025 18:59:16 +0400
Subject: [PATCH] Add sending paid stories replies.

---
 Telegram/Resources/langs/lang.strings         |   6 +-
 .../history/history_item_helpers.cpp          |  19 +-
 .../history/history_item_helpers.h            |   8 +-
 .../SourceFiles/history/history_widget.cpp    | 118 ++++-------
 Telegram/SourceFiles/history/history_widget.h |  21 +-
 .../history_view_compose_controls.cpp         |  15 +-
 .../history/view/history_view_element.cpp     |   3 +
 .../history/view/history_view_message.cpp     |  12 +-
 .../view/history_view_replies_section.cpp     | 138 +++++++++++--
 .../view/history_view_replies_section.h       |  14 +-
 .../media/stories/media_stories_reply.cpp     | 192 +++++++++++++++---
 .../media/stories/media_stories_reply.h       |  16 +-
 .../ui/chat/attach/attach_prepare.cpp         |  21 ++
 .../ui/chat/attach/attach_prepare.h           |  15 ++
 14 files changed, 442 insertions(+), 156 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 8f6a32ad8..294adcadb 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2167,14 +2167,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?";
 "lng_action_payment_refunded" = "{peer} refunded {amount}";
 "lng_action_paid_message_sent#one" = "You paid {count} Star to {action}";
-"lng_action_paid_message_sent#other" = "You paid {count} Star to {action}";
-"lng_action_paid_message_group#one" = "{from} paid {count} Star to {action}";
-"lng_action_paid_message_group#other" = "{from} paid {count} Star to {action}";
+"lng_action_paid_message_sent#other" = "You paid {count} Stars to {action}";
 "lng_action_paid_message_one" = "send a message";
 "lng_action_paid_message_some#one" = "send {count} message";
 "lng_action_paid_message_some#other" = "send {count} messages";
 "lng_action_paid_message_got#one" = "You received {count} Star from {name}";
 "lng_action_paid_message_got#other" = "You received {count} Stars from {name}";
+"lng_you_paid_stars#one" = "You paid {count} Star.";
+"lng_you_paid_stars#other" = "You paid {count} Stars.";
 
 "lng_similar_channels_title" = "Similar channels";
 "lng_similar_channels_view_all" = "View all";
diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp
index 49fcebcb2..5c601a700 100644
--- a/Telegram/SourceFiles/history/history_item_helpers.cpp
+++ b/Telegram/SourceFiles/history/history_item_helpers.cpp
@@ -382,13 +382,15 @@ bool SendPaymentHelper::check(
 		not_null<PeerData*> peer,
 		int messagesCount,
 		int starsApproved,
-		Fn<void(int)> resend) {
+		Fn<void(int)> resend,
+		PaidConfirmStyles styles) {
 	return check(
 		navigation->uiShow(),
 		peer,
 		messagesCount,
 		starsApproved,
-		std::move(resend));
+		std::move(resend),
+		styles);
 }
 
 bool SendPaymentHelper::check(
@@ -396,8 +398,10 @@ bool SendPaymentHelper::check(
 		not_null<PeerData*> peer,
 		int messagesCount,
 		int starsApproved,
-		Fn<void(int)> resend) {
-	_lifetime.destroy();
+		Fn<void(int)> resend,
+		PaidConfirmStyles styles) {
+	clear();
+
 	const auto details = ComputePaymentDetails(peer, messagesCount);
 	if (!details) {
 		_resend = [=] { resend(starsApproved); };
@@ -426,12 +430,17 @@ bool SendPaymentHelper::check(
 	} else if (const auto stars = details->stars; stars > starsApproved) {
 		ShowSendPaidConfirm(show, peer, *details, [=] {
 			resend(stars);
-		});
+		}, styles);
 		return false;
 	}
 	return true;
 }
 
+void SendPaymentHelper::clear() {
+	_lifetime.destroy();
+	_resend = nullptr;
+}
+
 void RequestDependentMessageItem(
 		not_null<HistoryItem*> item,
 		PeerId peerId,
diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h
index c5dc8795e..dfe2c737c 100644
--- a/Telegram/SourceFiles/history/history_item_helpers.h
+++ b/Telegram/SourceFiles/history/history_item_helpers.h
@@ -179,13 +179,17 @@ public:
 		not_null<PeerData*> peer,
 		int messagesCount,
 		int starsApproved,
-		Fn<void(int)> resend);
+		Fn<void(int)> resend,
+		PaidConfirmStyles styles = {});
 	[[nodiscard]] bool check(
 		std::shared_ptr<Main::SessionShow> show,
 		not_null<PeerData*> peer,
 		int messagesCount,
 		int starsApproved,
-		Fn<void(int)> resend);
+		Fn<void(int)> resend,
+		PaidConfirmStyles styles = {});
+
+	void clear();
 
 private:
 	Fn<void()> _resend;
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index e72412884..964c08888 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -225,16 +225,6 @@ const auto kPsaAboutPrefix = "cloud_lng_about_psa_";
 
 } // namespace
 
-struct HistoryWidget::SendingFiles {
-	std::vector<Ui::PreparedGroup> groups;
-	Ui::SendFilesWay way;
-	TextWithTags caption;
-	Api::SendOptions options;
-	int totalCount = 0;
-	bool sendComment = false;
-	bool ctrlShiftEnter = false;
-};
-
 HistoryWidget::HistoryWidget(
 	QWidget *parent,
 	not_null<Window::SessionController*> controller)
@@ -898,17 +888,6 @@ HistoryWidget::HistoryWidget(
 		}
 	}, lifetime());
 
-	if (!session().credits().loaded()) {
-		session().credits().loadedValue(
-		) | rpl::filter(
-			rpl::mappers::_1
-		) | rpl::take(1) | rpl::start_with_next([=] {
-			if (const auto callback = base::take(_resendOnFullUpdated)) {
-				callback();
-			}
-		}, lifetime());
-	}
-
 	using Type = Data::DefaultNotify;
 	rpl::merge(
 		session().data().notifySettings().defaultUpdates(Type::User),
@@ -2400,7 +2379,7 @@ void HistoryWidget::showHistory(
 		setHistory(nullptr);
 		_list = nullptr;
 		_peer = nullptr;
-		_resendOnFullUpdated = nullptr;
+		_sendPayment.clear();
 		_topicsRequested.clear();
 		_canSendMessages = false;
 		_canSendTexts = false;
@@ -4412,7 +4391,7 @@ void HistoryWidget::send(Api::SendOptions options) {
 	if (showSendMessageError(
 			message.textWithTags,
 			ignoreSlowmodeCountdown,
-			crl::guard(this, withPaymentApproved),
+			withPaymentApproved,
 			options.starsApproved)) {
 		return;
 	}
@@ -4735,6 +4714,19 @@ FullMsgId HistoryWidget::cornerButtonsCurrentId() {
 		: FullMsgId();
 }
 
+bool HistoryWidget::checkSendPayment(
+		int messagesCount,
+		int starsApproved,
+		Fn<void(int)> withPaymentApproved) {
+	return _peer
+		&& _sendPayment.check(
+			controller(),
+			_peer,
+			messagesCount,
+			starsApproved,
+			std::move(withPaymentApproved));
+}
+
 void HistoryWidget::checkSuggestToGigagroup() {
 	const auto group = _peer ? _peer->asMegagroup() : nullptr;
 	if (!group || !group->owner().suggestToGigagroup(group)) {
@@ -5890,7 +5882,7 @@ Data::ForumTopic *HistoryWidget::resolveReplyToTopic() {
 bool HistoryWidget::showSendMessageError(
 		const TextWithTags &textWithTags,
 		bool ignoreSlowmodeCountdown,
-		Fn<void(int starsApproved)> resend,
+		Fn<void(int starsApproved)> withPaymentApproved,
 		int starsApproved) {
 	if (!_canSendMessages) {
 		return false;
@@ -5908,25 +5900,12 @@ bool HistoryWidget::showSendMessageError(
 		Data::ShowSendErrorToast(controller(), _peer, error);
 		return true;
 	}
-	return resend
-		&& !checkSendPayment(request.messagesCount, starsApproved, resend);
-}
 
-bool HistoryWidget::checkSendPayment(
-		int messagesCount,
-		int starsApproved,
-		Fn<void(int starsApproved)> resend) {
-	const auto details = ComputePaymentDetails(_peer, messagesCount);
-	if (!details) {
-		_resendOnFullUpdated = [=] { resend(starsApproved); };
-		return false;
-	} else if (const auto stars = details->stars; stars > starsApproved) {
-		ShowSendPaidConfirm(controller(), _peer, *details, [=] {
-			resend(stars);
-		});
-		return false;
-	}
-	return true;
+	return withPaymentApproved
+		&& !checkSendPayment(
+			request.messagesCount,
+			starsApproved,
+			withPaymentApproved);
 }
 
 bool HistoryWidget::confirmSendingFiles(const QStringList &files) {
@@ -6012,11 +5991,11 @@ bool HistoryWidget::confirmSendingFiles(
 }
 
 void HistoryWidget::sendingFilesConfirmed(
-	Ui::PreparedList &&list,
-	Ui::SendFilesWay way,
-	TextWithTags &&caption,
-	Api::SendOptions options,
-	bool ctrlShiftEnter) {
+		Ui::PreparedList &&list,
+		Ui::SendFilesWay way,
+		TextWithTags &&caption,
+		Api::SendOptions options,
+		bool ctrlShiftEnter) {
 	Expects(list.filesToProcess.empty());
 
 	const auto compress = way.sendImagesAsPhotos();
@@ -6024,55 +6003,51 @@ void HistoryWidget::sendingFilesConfirmed(
 		return;
 	}
 
-	const auto filesCount = int(list.files.size());
 	auto groups = DivideByGroups(
 		std::move(list),
 		way,
 		_peer->slowmodeApplied());
-	const auto sendComment = !caption.text.isEmpty()
-		&& (groups.size() != 1 || !groups.front().sentWithCaption());
-	sendingFilesConfirmed(std::make_shared<SendingFiles>(SendingFiles{
-		.groups = std::move(groups),
-		.way = way,
-		.caption = std::move(caption),
-		.options = options,
-		.totalCount = filesCount + (sendComment ? 1 : 0),
-		.sendComment = sendComment,
-		.ctrlShiftEnter = ctrlShiftEnter,
-	}));
+	auto bundle = PrepareFilesBundle(
+		std::move(groups),
+		way,
+		std::move(caption),
+		ctrlShiftEnter);
+	sendingFilesConfirmed(std::move(bundle), options);
 }
 
 void HistoryWidget::sendingFilesConfirmed(
-		std::shared_ptr<SendingFiles> args) {
+		std::shared_ptr<Ui::PreparedBundle> bundle,
+		Api::SendOptions options) {
 	const auto withPaymentApproved = [=](int approved) {
-		args->options.starsApproved = approved;
-		sendingFilesConfirmed(args);
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendingFilesConfirmed(bundle, copy);
 	};
 	const auto checked = checkSendPayment(
-		args->totalCount,
-		args->options.starsApproved,
+		bundle->totalCount,
+		options.starsApproved,
 		withPaymentApproved);
 	if (!checked) {
 		return;
 	}
 
-	const auto compress = args->way.sendImagesAsPhotos();
+	const auto compress = bundle->way.sendImagesAsPhotos();
 	const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
-	auto action = prepareSendAction(args->options);
+	auto action = prepareSendAction(options);
 	action.clearDraft = false;
-	if (args->sendComment) {
+	if (bundle->sendComment) {
 		auto message = Api::MessageToSend(action);
-		message.textWithTags = base::take(args->caption);
+		message.textWithTags = base::take(bundle->caption);
 		session().api().sendMessage(std::move(message));
 	}
-	for (auto &group : args->groups) {
+	for (auto &group : bundle->groups) {
 		const auto album = (group.type != Ui::AlbumType::None)
 			? std::make_shared<SendingAlbum>()
 			: nullptr;
 		session().api().sendFiles(
 			std::move(group.list),
 			type,
-			base::take(args->caption),
+			base::take(bundle->caption),
 			album,
 			action);
 	}
@@ -8545,9 +8520,6 @@ void HistoryWidget::fullInfoUpdated() {
 		updateControlsVisibility();
 		updateControlsGeometry();
 	}
-	if (const auto callback = base::take(_resendOnFullUpdated)) {
-		callback();
-	}
 }
 
 void HistoryWidget::handlePeerUpdate() {
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index f52dd3086..2b956c2c1 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/view/controls/history_view_compose_media_edit_manager.h"
 #include "history/view/history_view_corner_buttons.h"
 #include "history/history_drag_area.h"
+#include "history/history_item_helpers.h"
 #include "history/history_view_highlight_manager.h"
 #include "history/history_view_top_toast.h"
 #include "history/history.h"
@@ -69,6 +70,7 @@ class PinnedBar;
 class GroupCallBar;
 class RequestsBar;
 struct PreparedList;
+struct PreparedBundle;
 class SendFilesWay;
 class SendAsButton;
 class SpoilerAnimation;
@@ -320,7 +322,6 @@ private:
 	using TabbedPanel = ChatHelpers::TabbedPanel;
 	using TabbedSelector = ChatHelpers::TabbedSelector;
 	using VoiceToSend = HistoryView::Controls::VoiceToSend;
-	struct SendingFiles;
 	enum ScrollChangeType {
 		ScrollChangeNone,
 
@@ -359,6 +360,11 @@ private:
 	bool cornerButtonsUnreadMayBeShown() override;
 	bool cornerButtonsHas(HistoryView::CornerButtonType type) override;
 
+	[[nodiscard]] bool checkSendPayment(
+		int messagesCount,
+		int starsApproved,
+		Fn<void(int)> withPaymentApproved);
+
 	void checkSuggestToGigagroup();
 	void processReply();
 	void setReplyFieldsFromProcessing();
@@ -471,12 +477,8 @@ private:
 	bool showSendMessageError(
 		const TextWithTags &textWithTags,
 		bool ignoreSlowmodeCountdown,
-		Fn<void(int starsApproved)> resend = nullptr,
+		Fn<void(int starsApproved)> withPaymentApproved = nullptr,
 		int starsApproved = 0);
-	bool checkSendPayment(
-		int messagesCount,
-		int starsApproved,
-		Fn<void(int starsApproved)> resend);
 
 	void sendingFilesConfirmed(
 		Ui::PreparedList &&list,
@@ -484,7 +486,9 @@ private:
 		TextWithTags &&caption,
 		Api::SendOptions options,
 		bool ctrlShiftEnter);
-	void sendingFilesConfirmed(std::shared_ptr<SendingFiles> args);
+	void sendingFilesConfirmed(
+		std::shared_ptr<Ui::PreparedBundle> bundle,
+		Api::SendOptions options);
 
 	void uploadFile(const QByteArray &fileContent, SendMediaType type);
 	void itemRemoved(not_null<const HistoryItem*> item);
@@ -769,7 +773,6 @@ private:
 	std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
 	std::unique_ptr<Ui::Emoji::SuggestionsController> _emojiSuggestions;
 	object_ptr<Support::Autocomplete> _supportAutocomplete;
-	Fn<void()> _resendOnFullUpdated;
 
 	UserData *_inlineBot = nullptr;
 	QString _inlineBotUsername;
@@ -874,6 +877,8 @@ private:
 
 	int _topDelta = 0;
 
+	SendPaymentHelper _sendPayment;
+
 	rpl::event_stream<> _cancelRequests;
 
 };
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
index 1f88adeec..dc323ba7d 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -1722,13 +1722,20 @@ void ComposeControls::updateFieldPlaceholder() {
 	}
 
 	_field->setPlaceholder([&] {
+		const auto peer = _history ? _history->peer.get() : nullptr;
 		if (_fieldCustomPlaceholder) {
 			return rpl::duplicate(_fieldCustomPlaceholder);
 		} else if (isEditingMessage()) {
 			return tr::lng_edit_message_text();
-		} else if (!_history) {
+		} else if (!peer) {
 			return tr::lng_message_ph();
-		} else if (const auto channel = _history->peer->asChannel()) {
+		} else if (const auto stars = peer->starsPerMessageChecked()) {
+			return tr::lng_message_paid_ph(
+				lt_amount,
+				tr::lng_prize_credits_amount(
+					lt_count,
+					rpl::single(stars * 1.)));
+		} else if (const auto channel = peer->asChannel()) {
 			if (channel->isBroadcast()) {
 				return session().data().notifySettings().silentPosts(channel)
 					? tr::lng_broadcast_silent_ph()
@@ -3120,6 +3127,7 @@ void ComposeControls::initWebpageProcess() {
 		| Data::PeerUpdate::Flag::Notifications
 		| Data::PeerUpdate::Flag::MessagesTTL
 		| Data::PeerUpdate::Flag::FullInfo
+		| Data::PeerUpdate::Flag::StarsPerMessage
 	) | rpl::filter([peer = _history->peer](const Data::PeerUpdate &update) {
 		return (update.peer.get() == peer);
 	}) | rpl::map([](const Data::PeerUpdate &update) {
@@ -3135,6 +3143,9 @@ void ComposeControls::initWebpageProcess() {
 		if (flags & Data::PeerUpdate::Flag::MessagesTTL) {
 			updateMessagesTTLShown();
 		}
+		if (flags & Data::PeerUpdate::Flag::StarsPerMessage) {
+			updateFieldPlaceholder();
+		}
 		if (flags & Data::PeerUpdate::Flag::FullInfo) {
 			if (updateBotCommandShown()) {
 				updateControlsVisibility();
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index 4dddabdfb..44e9e658f 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -384,6 +384,9 @@ QString DateTooltipText(not_null<Element*> view) {
 	if (item->isScheduled() && item->isSilent()) {
 		dateText += '\n' + QChar(0xD83D) + QChar(0xDD15);
 	}
+	if (const auto stars = item->out() ? item->starsPaid() : 0) {
+		dateText += '\n' + tr::lng_you_paid_stars(tr::now, lt_count, stars);
+	}
 	return dateText;
 }
 
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 19d3ea733..b9a4b0a1e 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -476,22 +476,12 @@ void Message::initPaidInformation() {
 				lt_action,
 				action(),
 				Ui::Text::WithEntities)
-			: history()->peer->isUser()
-			? tr::lng_action_paid_message_got(
+			: tr::lng_action_paid_message_got(
 				tr::now,
 				lt_count,
 				info.stars,
 				lt_name,
 				Ui::Text::Link(item->from()->shortName(), 1),
-				Ui::Text::WithEntities)
-			: tr::lng_action_paid_message_group(
-				tr::now,
-				lt_count,
-				info.stars,
-				lt_from,
-				Ui::Text::Link(item->from()->shortName(), 1),
-				lt_action,
-				action(),
 				Ui::Text::WithEntities),
 	};
 	if (!item->out()) {
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index 3ffc1348e..88f2b44a5 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -733,8 +733,8 @@ void RepliesWidget::setupComposeControls() {
 	}, lifetime());
 
 	_composeControls->sendVoiceRequests(
-	) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) {
-		sendVoice(std::move(data));
+	) | rpl::start_with_next([=](const ComposeControls::VoiceToSend &data) {
+		sendVoice(data);
 	}, lifetime());
 
 	_composeControls->sendCommandRequests(
@@ -1070,25 +1070,59 @@ void RepliesWidget::sendingFilesConfirmed(
 		std::move(list),
 		way,
 		_history->peer->slowmodeApplied());
-	const auto type = way.sendImagesAsPhotos()
-		? SendMediaType::Photo
-		: SendMediaType::File;
+	auto bundle = PrepareFilesBundle(
+		std::move(groups),
+		way,
+		std::move(caption),
+		ctrlShiftEnter);
+	sendingFilesConfirmed(std::move(bundle), options);
+}
+
+bool RepliesWidget::checkSendPayment(
+		int messagesCount,
+		int starsApproved,
+		Fn<void(int)> withPaymentApproved) {
+	return _sendPayment.check(
+		controller(),
+		_history->peer,
+		messagesCount,
+		starsApproved,
+		std::move(withPaymentApproved));
+}
+
+void RepliesWidget::sendingFilesConfirmed(
+		std::shared_ptr<Ui::PreparedBundle> bundle,
+		Api::SendOptions options) {
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendingFilesConfirmed(bundle, copy);
+	};
+	const auto checked = checkSendPayment(
+		bundle->totalCount,
+		options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return;
+	}
+
+	const auto compress = bundle->way.sendImagesAsPhotos();
+	const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
 	auto action = prepareSendAction(options);
 	action.clearDraft = false;
-	if ((groups.size() != 1 || !groups.front().sentWithCaption())
-		&& !caption.text.isEmpty()) {
+	if (bundle->sendComment) {
 		auto message = Api::MessageToSend(action);
-		message.textWithTags = base::take(caption);
+		message.textWithTags = base::take(bundle->caption);
 		session().api().sendMessage(std::move(message));
 	}
-	for (auto &group : groups) {
+	for (auto &group : bundle->groups) {
 		const auto album = (group.type != Ui::AlbumType::None)
 			? std::make_shared<SendingAlbum>()
 			: nullptr;
 		session().api().sendFiles(
 			std::move(group.list),
 			type,
-			base::take(caption),
+			base::take(bundle->caption),
 			album,
 			action);
 	}
@@ -1227,7 +1261,20 @@ void RepliesWidget::send() {
 	send({});
 }
 
-void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
+void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) {
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = data;
+		copy.options.starsApproved = approved;
+		sendVoice(copy);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		data.options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return;
+	}
+
 	auto action = prepareSendAction(data.options);
 	session().api().sendVoiceMessage(
 		data.bytes,
@@ -1254,19 +1301,32 @@ void RepliesWidget::send(Api::SendOptions options) {
 	message.textWithTags = _composeControls->getTextWithAppliedMarkdown();
 	message.webPage = _composeControls->webPageDraft();
 
-	const auto error = GetErrorForSending(
-		_history->peer,
-		{
-			.topicRootId = _topic ? _topic->rootId() : MsgId(0),
-			.forward = &_composeControls->forwardItems(),
-			.text = &message.textWithTags,
-			.ignoreSlowmodeCountdown = (options.scheduled != 0),
-		});
+	auto request = SendingErrorRequest{
+		.topicRootId = _topic ? _topic->rootId() : MsgId(0),
+		.forward = &_composeControls->forwardItems(),
+		.text = &message.textWithTags,
+		.ignoreSlowmodeCountdown = (options.scheduled != 0),
+	};
+	request.messagesCount = ComputeSendingMessagesCount(_history, request);
+	const auto error = GetErrorForSending(_history->peer, request);
 	if (error) {
 		Data::ShowSendErrorToast(controller(), _history->peer, error);
 		return;
 	}
-
+	if (!options.scheduled) {
+		const auto withPaymentApproved = [=](int approved) {
+			auto copy = options;
+			copy.starsApproved = approved;
+			send(copy);
+		};
+		const auto checked = checkSendPayment(
+			request.messagesCount,
+			options.starsApproved,
+			withPaymentApproved);
+		if (!checked) {
+			return;
+		}
+	}
 	session().api().sendMessage(std::move(message));
 
 	_composeControls->clear();
@@ -1420,6 +1480,18 @@ bool RepliesWidget::sendExistingDocument(
 		|| ShowSendPremiumError(controller(), document)) {
 		return false;
 	}
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = messageToSend;
+		copy.action.options.starsApproved = approved;
+		sendExistingDocument(document, std::move(copy), localId);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		messageToSend.action.options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return false;
+	}
 
 	Api::SendExistingDocument(
 		std::move(messageToSend),
@@ -1448,6 +1520,19 @@ bool RepliesWidget::sendExistingPhoto(
 		return false;
 	}
 
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendExistingPhoto(photo, copy);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return false;
+	}
+
 	Api::SendExistingPhoto(
 		Api::MessageToSend(prepareSendAction(options)),
 		photo);
@@ -1478,6 +1563,19 @@ void RepliesWidget::sendInlineResult(
 		not_null<UserData*> bot,
 		Api::SendOptions options,
 		std::optional<MsgId> localMessageId) {
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendInlineResult(result, bot, copy, localMessageId);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return;
+	}
+
 	auto action = prepareSendAction(options);
 	action.generateLocal = true;
 	session().api().sendInlineResult(
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h
index 7f3a6f4dc..d03f5c834 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.h
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/section_memento.h"
 #include "history/view/history_view_corner_buttons.h"
 #include "history/view/history_view_list_widget.h"
+#include "history/history_item_helpers.h"
 #include "history/history_view_swipe_data.h"
 #include "data/data_messages.h"
 #include "base/timer.h"
@@ -38,6 +39,7 @@ class PlainShadow;
 class FlatButton;
 class PinnedBar;
 struct PreparedList;
+struct PreparedBundle;
 class SendFilesWay;
 } // namespace Ui
 
@@ -207,6 +209,11 @@ private:
 	void checkActivation() override;
 	void doSetInnerFocus() override;
 
+	[[nodiscard]] bool checkSendPayment(
+		int messagesCount,
+		int starsApproved,
+		Fn<void(int)> withPaymentApproved);
+
 	void onScroll();
 	void updateInnerVisibleArea();
 	void updateControlsGeometry();
@@ -251,7 +258,7 @@ private:
 		Api::SendOptions options) const;
 	void send();
 	void send(Api::SendOptions options);
-	void sendVoice(Controls::VoiceToSend &&data);
+	void sendVoice(const Controls::VoiceToSend &data);
 	void edit(
 		not_null<HistoryItem*> item,
 		Api::SendOptions options,
@@ -308,6 +315,9 @@ private:
 		TextWithTags &&caption,
 		Api::SendOptions options,
 		bool ctrlShiftEnter);
+	void sendingFilesConfirmed(
+		std::shared_ptr<Ui::PreparedBundle> bundle,
+		Api::SendOptions options);
 
 	bool sendExistingDocument(
 		not_null<DocumentData*> document,
@@ -380,6 +390,8 @@ private:
 
 	HistoryView::ChatPaintGestureHorizontalData _gestureHorizontal;
 
+	SendPaymentHelper _sendPayment;
+
 	int _lastScrollTop = 0;
 	int _topicReopenBarHeight = 0;
 	int _scrollTopDelta = 0;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index ad06525c3..21ae90050 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -15,11 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/unixtime.h"
 #include "boxes/premium_limits_box.h"
 #include "boxes/send_files_box.h"
+#include "boxes/share_box.h" // ShareBoxStyleOverrides
 #include "chat_helpers/compose/compose_show.h"
 #include "chat_helpers/tabbed_selector.h"
 #include "core/file_utilities.h"
 #include "core/mime_type.h"
 #include "data/stickers/data_custom_emoji.h"
+#include "data/data_changes.h"
 #include "data/data_chat_participant_status.h"
 #include "data/data_document.h"
 #include "data/data_message_reaction_id.h"
@@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_user.h"
 #include "history/view/controls/compose_controls_common.h"
 #include "history/view/controls/history_view_compose_controls.h"
+#include "history/view/history_view_schedule_box.h" // ScheduleBoxStyleArgs
 #include "history/history_item_helpers.h"
 #include "history/history.h"
 #include "inline_bots/inline_bot_result.h"
@@ -36,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_controller.h"
 #include "media/stories/media_stories_stealth.h"
 #include "menu/menu_send.h"
+#include "settings/settings_credits_graphics.h" // DarkCreditsEntryBoxStyle
 #include "storage/localimageloader.h"
 #include "storage/storage_account.h"
 #include "storage/storage_media_prepare.h"
@@ -52,14 +56,19 @@ namespace {
 
 [[nodiscard]] rpl::producer<QString> PlaceholderText(
 		const std::shared_ptr<ChatHelpers::Show> &show,
-		rpl::producer<bool> isComment) {
+		rpl::producer<bool> isComment,
+		rpl::producer<int> starsPerMessage) {
 	return rpl::combine(
 		show->session().data().stories().stealthModeValue(),
-		std::move(isComment)
-	) | rpl::map([](Data::StealthMode value, bool isComment) {
-		return std::tuple(value.enabledTill, isComment);
+		std::move(isComment),
+		std::move(starsPerMessage)
+	) | rpl::map([](
+			Data::StealthMode value,
+			bool isComment,
+			int starsPerMessage) {
+		return std::tuple(value.enabledTill, isComment, starsPerMessage);
 	}) | rpl::distinct_until_changed(
-	) | rpl::map([](TimeId till, bool isComment) {
+	) | rpl::map([](TimeId till, bool isComment, int starsPerMessage) {
 		return rpl::single(
 			rpl::empty
 		) | rpl::then(
@@ -71,7 +80,13 @@ namespace {
 		}) | rpl::then(
 			rpl::single(0)
 		) | rpl::map([=](TimeId left) {
-			return left
+			return starsPerMessage
+				? tr::lng_message_paid_ph(
+					lt_amount,
+					tr::lng_prize_credits_amount(
+						lt_count,
+						rpl::single(starsPerMessage * 1.)))
+				: left
 				? tr::lng_stealth_mode_countdown(
 					lt_left,
 					rpl::single(TimeLeftText(left)))
@@ -128,7 +143,8 @@ ReplyArea::ReplyArea(not_null<Controller*> controller)
 		.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),
 		.customPlaceholder = PlaceholderText(
 			_controller->uiShow(),
-			rpl::deferred([=] { return _isComment.value(); })),
+			rpl::deferred([=] { return _isComment.value(); }),
+			rpl::deferred([=] { return _starsForMessage.value(); })),
 		.voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now),
 		.voiceLockFromBottom = true,
 		.features = {
@@ -199,7 +215,7 @@ bool ReplyArea::sendReaction(const Data::ReactionId &id) {
 		}
 	}
 	return !message.textWithTags.empty()
-		&& send(std::move(message), {}, true);
+		&& send(std::move(message), true);
 }
 
 void ReplyArea::send(Api::SendOptions options) {
@@ -209,29 +225,45 @@ void ReplyArea::send(Api::SendOptions options) {
 	message.textWithTags = _controls->getTextWithAppliedMarkdown();
 	message.webPage = webPageDraft;
 
-	send(std::move(message), options);
+	send(std::move(message));
 }
 
 bool ReplyArea::send(
 		Api::MessageToSend message,
-		Api::SendOptions options,
 		bool skipToast) {
-	if (!options.scheduled && showSlowmodeError()) {
+	if (!message.action.options.scheduled && showSlowmodeError()) {
 		return false;
 	}
 
-	const auto error = GetErrorForSending(
-		_data.peer,
-		{
-			.topicRootId = MsgId(0),
-			.text = &message.textWithTags,
-			.ignoreSlowmodeCountdown = (options.scheduled != 0),
-		});
+	auto request = SendingErrorRequest{
+		.topicRootId = MsgId(0),
+		.text = &message.textWithTags,
+		.ignoreSlowmodeCountdown = (message.action.options.scheduled != 0),
+	};
+	request.messagesCount = ComputeSendingMessagesCount(
+		message.action.history,
+		request);
+	const auto error = GetErrorForSending(_data.peer, request);
 	if (error) {
 		Data::ShowSendErrorToast(_controller->uiShow(), _data.peer, error);
 		return false;
 	}
 
+	if (!message.action.options.scheduled) {
+		const auto withPaymentApproved = [=](int approved) {
+			auto copy = message;
+			copy.action.options.starsApproved = approved;
+			send(copy);
+		};
+		const auto checked = checkSendPayment(
+			request.messagesCount,
+			message.action.options.starsApproved,
+			withPaymentApproved);
+		if (!checked) {
+			return false;
+		}
+	}
+
 	session().api().sendMessage(std::move(message));
 
 	finishSending(skipToast);
@@ -239,7 +271,40 @@ bool ReplyArea::send(
 	return true;
 }
 
-void ReplyArea::sendVoice(VoiceToSend &&data) {
+bool ReplyArea::checkSendPayment(
+		int messagesCount,
+		int starsApproved,
+		Fn<void(int)> withPaymentApproved) {
+	const auto st1 = ::Settings::DarkCreditsEntryBoxStyle();
+	const auto st2 = st1.shareBox.get();
+	const auto st3 = st2 ? st2->scheduleBox.get() : nullptr;
+	return _data.peer
+		&& _sendPayment.check(
+			_controller->uiShow(),
+			_data.peer,
+			messagesCount,
+			starsApproved,
+			std::move(withPaymentApproved),
+			{
+				.label = st3 ? st3->chooseDateTimeArgs.labelStyle : nullptr,
+				.checkbox = st2 ? st2->checkbox : nullptr,
+			});
+}
+
+void ReplyArea::sendVoice(const VoiceToSend &data) {
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = data;
+		copy.options.starsApproved = approved;
+		sendVoice(copy);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		data.options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return;
+	}
+
 	auto action = prepareSendAction(data.options);
 	session().api().sendVoiceMessage(
 		data.bytes,
@@ -269,6 +334,18 @@ bool ReplyArea::sendExistingDocument(
 		|| Window::ShowSendPremiumError(show, document)) {
 		return false;
 	}
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = messageToSend;
+		copy.action.options.starsApproved = approved;
+		sendExistingDocument(document, std::move(copy), localId);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		messageToSend.action.options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return false;
+	}
 
 	Api::SendExistingDocument(std::move(messageToSend), document, localId);
 
@@ -296,6 +373,18 @@ bool ReplyArea::sendExistingPhoto(
 	} else if (showSlowmodeError()) {
 		return false;
 	}
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendExistingPhoto(photo, copy);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return false;
+	}
 
 	Api::SendExistingPhoto(
 		Api::MessageToSend(prepareSendAction(options)),
@@ -322,6 +411,19 @@ void ReplyArea::sendInlineResult(
 		not_null<UserData*> bot,
 		Api::SendOptions options,
 		std::optional<MsgId> localMessageId) {
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendInlineResult(result, bot, copy, localMessageId);
+	};
+	const auto checked = checkSendPayment(
+		1,
+		options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return;
+	}
+
 	auto action = prepareSendAction(options);
 	action.generateLocal = true;
 	session().api().sendInlineResult(
@@ -564,25 +666,47 @@ void ReplyArea::sendingFilesConfirmed(
 		std::move(list),
 		way,
 		_data.peer->slowmodeApplied());
-	const auto type = way.sendImagesAsPhotos()
-		? SendMediaType::Photo
-		: SendMediaType::File;
+	auto bundle = PrepareFilesBundle(
+		std::move(groups),
+		way,
+		std::move(caption),
+		ctrlShiftEnter);
+	sendingFilesConfirmed(std::move(bundle), options);
+}
+
+void ReplyArea::sendingFilesConfirmed(
+		std::shared_ptr<Ui::PreparedBundle> bundle,
+		Api::SendOptions options) {
+	const auto withPaymentApproved = [=](int approved) {
+		auto copy = options;
+		copy.starsApproved = approved;
+		sendingFilesConfirmed(bundle, copy);
+	};
+	const auto checked = checkSendPayment(
+		bundle->totalCount,
+		options.starsApproved,
+		withPaymentApproved);
+	if (!checked) {
+		return;
+	}
+
+	const auto compress = bundle->way.sendImagesAsPhotos();
+	const auto type = compress ? SendMediaType::Photo : SendMediaType::File;
 	auto action = prepareSendAction(options);
 	action.clearDraft = false;
-	if ((groups.size() != 1 || !groups.front().sentWithCaption())
-		&& !caption.text.isEmpty()) {
+	if (bundle->sendComment) {
 		auto message = Api::MessageToSend(action);
-		message.textWithTags = base::take(caption);
+		message.textWithTags = base::take(bundle->caption);
 		session().api().sendMessage(std::move(message));
 	}
-	for (auto &group : groups) {
+	for (auto &group : bundle->groups) {
 		const auto album = (group.type != Ui::AlbumType::None)
 			? std::make_shared<SendingAlbum>()
 			: nullptr;
 		session().api().sendFiles(
 			std::move(group.list),
 			type,
-			base::take(caption),
+			base::take(bundle->caption),
 			album,
 			action);
 	}
@@ -618,8 +742,8 @@ void ReplyArea::initActions() {
 	}, _lifetime);
 
 	_controls->sendVoiceRequests(
-	) | rpl::start_with_next([=](VoiceToSend &&data) {
-		sendVoice(std::move(data));
+	) | rpl::start_with_next([=](const VoiceToSend &data) {
+		sendVoice(data);
 	}, _lifetime);
 
 	_controls->attachRequests(
@@ -697,6 +821,16 @@ void ReplyArea::show(
 			_controls->clear();
 		}
 		return;
+	} else if (const auto peer = _data.peer) {
+		using Flag = Data::PeerUpdate::Flag;
+		_starsForMessage = peer->session().changes().peerFlagsValue(
+			peer,
+			Flag::StarsPerMessage | Flag::FullInfo
+		) | rpl::map([=] {
+			return peer->starsPerMessageChecked();
+		});
+	} else {
+		_starsForMessage = 0;
 	}
 	invalidate_weak_ptrs(&_shownPeerGuard);
 	const auto peer = data.peer;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index d3cdec47d..bb7fe15f0 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "base/weak_ptr.h"
+#include "history/history_item_helpers.h"
 
 class History;
 enum class SendMediaType;
@@ -44,6 +45,7 @@ struct Details;
 
 namespace Ui {
 struct PreparedList;
+struct PreparedBundle;
 class SendFilesWay;
 class RpWidget;
 } // namespace Ui
@@ -90,9 +92,13 @@ private:
 
 	bool send(
 		Api::MessageToSend message,
-		Api::SendOptions options,
 		bool skipToast = false);
 
+	[[nodiscard]] bool checkSendPayment(
+		int messagesCount,
+		int starsApproved,
+		Fn<void(int)> withPaymentApproved);
+
 	void uploadFile(const QByteArray &fileContent, SendMediaType type);
 	bool confirmSendingFiles(
 		QImage &&image,
@@ -116,6 +122,9 @@ private:
 		TextWithTags &&caption,
 		Api::SendOptions options,
 		bool ctrlShiftEnter);
+	void sendingFilesConfirmed(
+		std::shared_ptr<Ui::PreparedBundle> bundle,
+		Api::SendOptions options);
 	void finishSending(bool skipToast = false);
 
 	bool sendExistingDocument(
@@ -141,7 +150,7 @@ private:
 	[[nodiscard]] Api::SendAction prepareSendAction(
 		Api::SendOptions options) const;
 	void send(Api::SendOptions options);
-	void sendVoice(VoiceToSend &&data);
+	void sendVoice(const VoiceToSend &data);
 	void chooseAttach(std::optional<bool> overrideSendImagesAsPhotos);
 
 	[[nodiscard]] Fn<SendMenu::Details()> sendMenuDetails() const;
@@ -151,6 +160,7 @@ private:
 
 	const not_null<Controller*> _controller;
 	rpl::variable<bool> _isComment;
+	rpl::variable<int> _starsForMessage;
 
 	const std::unique_ptr<HistoryView::ComposeControls> _controls;
 	std::unique_ptr<Cant> _cant;
@@ -160,6 +170,8 @@ private:
 	bool _chooseAttachRequest = false;
 	rpl::variable<bool> _choosingAttach;
 
+	SendPaymentHelper _sendPayment;
+
 	rpl::lifetime _lifetime;
 
 };
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
index 3c5e0e707..fb968607b 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
@@ -266,6 +266,27 @@ bool PreparedList::hasSpoilerMenu(bool compress) const {
 	return allAreVideo || (allAreMedia && compress);
 }
 
+std::shared_ptr<PreparedBundle> PrepareFilesBundle(
+		std::vector<PreparedGroup> groups,
+		SendFilesWay way,
+		TextWithTags caption,
+		bool ctrlShiftEnter) {
+	auto totalCount = 0;
+	for (const auto &group : groups) {
+		totalCount += group.list.files.size();
+	}
+	const auto sendComment = !caption.text.isEmpty()
+		&& (groups.size() != 1 || !groups.front().sentWithCaption());
+	return std::make_shared<PreparedBundle>(PreparedBundle{
+		.groups = std::move(groups),
+		.way = way,
+		.caption = std::move(caption),
+		.totalCount = totalCount + (sendComment ? 1 : 0),
+		.sendComment = sendComment,
+		.ctrlShiftEnter = ctrlShiftEnter,
+	});
+}
+
 int MaxAlbumItems() {
 	return kMaxAlbumCount;
 }
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
index b244f321b..311903a00 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "editor/photo_editor_common.h"
+#include "ui/chat/attach/attach_send_files_way.h"
 #include "ui/rect_part.h"
 
 #include <QtCore/QSemaphore>
@@ -153,6 +154,20 @@ struct PreparedGroup {
 	SendFilesWay way,
 	bool slowmode);
 
+struct PreparedBundle {
+	std::vector<PreparedGroup> groups;
+	SendFilesWay way;
+	TextWithTags caption;
+	int totalCount = 0;
+	bool sendComment = false;
+	bool ctrlShiftEnter = false;
+};
+[[nodiscard]] std::shared_ptr<PreparedBundle> PrepareFilesBundle(
+	std::vector<PreparedGroup> groups,
+	SendFilesWay way,
+	TextWithTags caption,
+	bool ctrlShiftEnter);
+
 [[nodiscard]] int MaxAlbumItems();
 [[nodiscard]] bool ValidateThumbDimensions(int width, int height);