From 572c074c4245d656edbdde8b441d0c0691a29e7c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 15 Nov 2024 15:34:23 +0400
Subject: [PATCH] Add gifts-to-profile privacy.

---
 Telegram/Resources/langs/lang.strings         |  14 +
 Telegram/SourceFiles/api/api_user_privacy.cpp |  13 +
 Telegram/SourceFiles/api/api_user_privacy.h   |   2 +
 .../SourceFiles/boxes/edit_privacy_box.cpp    | 244 ++++++++++++------
 Telegram/SourceFiles/boxes/edit_privacy_box.h |   4 +
 Telegram/SourceFiles/mtproto/scheme/api.tl    |   6 +
 .../settings/settings_privacy_controllers.cpp | 131 +++++++---
 .../settings/settings_privacy_controllers.h   |  20 +-
 .../settings/settings_privacy_security.cpp    |  19 +-
 9 files changed, 346 insertions(+), 107 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 2c124769b..14cc25536 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -682,6 +682,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_settings_messages_privacy" = "Messages";
 "lng_settings_voices_privacy" = "Voice messages";
 "lng_settings_bio_privacy" = "Bio";
+"lng_settings_gifts_privacy" = "Gifts";
 "lng_settings_birthday_privacy" = "Date of Birth";
 "lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages.";
 "lng_settings_privacy_premium_link" = "Telegram Premium";
@@ -1162,19 +1163,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_blocked_list_subtitle#other" = "{count} blocked users";
 
 "lng_edit_privacy_everyone" = "Everybody";
+"lng_edit_privacy_no_miniapps" = "Not Mini Apps";
 "lng_edit_privacy_contacts" = "My contacts";
 "lng_edit_privacy_close_friends" = "Close friends";
 "lng_edit_privacy_contacts_and_premium" = "Contacts & Premium";
+"lng_edit_privacy_contacts_and_miniapps" = "Contacts & Mini Apps";
 "lng_edit_privacy_nobody" = "Nobody";
 "lng_edit_privacy_premium" = "Premium users";
+"lng_edit_privacy_miniapps" = "Mini Apps";
 "lng_edit_privacy_exceptions" = "Add exceptions";
 "lng_edit_privacy_user_types" = "User types";
 "lng_edit_privacy_users_and_groups" = "Users and groups";
 "lng_edit_privacy_premium_status" = "all Telegram Premium subscribers";
+"lng_edit_privacy_miniapps_status" = "web mini apps that you use";
 
 "lng_edit_privacy_exceptions_count#one" = "{count} user";
 "lng_edit_privacy_exceptions_count#other" = "{count} users";
 "lng_edit_privacy_exceptions_premium_and" = "Premium & {users}";
+"lng_edit_privacy_exceptions_miniapps_and" = "Mini Apps & {users}";
 "lng_edit_privacy_exceptions_add" = "Add users";
 
 "lng_edit_privacy_phone_number_title" = "Phone number privacy";
@@ -1228,6 +1234,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_edit_privacy_birthday_yet" = "You haven't entered your date of birth yet.\n{link}";
 "lng_edit_privacy_birthday_yet_link" = "Add my birthday >";
 
+"lng_edit_privacy_gifts_title" = "Gifts";
+"lng_edit_privacy_gifts_header" = "Who can display gifts on my profile";
+"lng_edit_privacy_gifts_always_empty" = "Always allow";
+"lng_edit_privacy_gifts_never_empty" = "Never allow";
+"lng_edit_privacy_gifts_exceptions" = "Choose whether gifts from specific senders need your approval before they're visible to others on your profile.";
+"lng_edit_privacy_gifts_always_title" = "Always allow";
+"lng_edit_privacy_gifts_never_title" = "Never allow";
+
 "lng_edit_privacy_calls_title" = "Calls";
 "lng_edit_privacy_calls_header" = "Who can call me";
 "lng_edit_privacy_calls_always_empty" = "Always allow";
diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp
index d0c17fa6b..78f863bed 100644
--- a/Telegram/SourceFiles/api/api_user_privacy.cpp
+++ b/Telegram/SourceFiles/api/api_user_privacy.cpp
@@ -69,6 +69,9 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) {
 		if (rule.always.premiums && (rule.option != Option::Everyone)) {
 			result.push_back(MTP_inputPrivacyValueAllowPremium());
 		}
+		if (rule.always.miniapps && (rule.option != Option::Everyone)) {
+			result.push_back(MTP_inputPrivacyValueAllowBots());
+		}
 	}
 	if (!rule.ignoreNever) {
 		const auto users = collectInputUsers(rule.never);
@@ -83,6 +86,9 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) {
 				MTP_inputPrivacyValueDisallowChatParticipants(
 					MTP_vector<MTPlong>(chats)));
 		}
+		if (rule.never.miniapps && (rule.option != Option::Nobody)) {
+			result.push_back(MTP_inputPrivacyValueDisallowBots());
+		}
 	}
 	result.push_back([&] {
 		switch (rule.option) {
@@ -124,6 +130,10 @@ UserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) {
 			setOption(Option::CloseFriends);
 		}, [&](const MTPDprivacyValueAllowPremium &) {
 			result.always.premiums = true;
+		}, [&](const MTPDprivacyValueAllowBots &) {
+			result.always.miniapps = true;
+		}, [&](const MTPDprivacyValueDisallowBots &) {
+			result.never.miniapps = true;
 		}, [&](const MTPDprivacyValueAllowUsers &data) {
 			const auto &users = data.vusers().v;
 			always.reserve(always.size() + users.size());
@@ -199,6 +209,7 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) {
 	case Key::Voices: return MTP_inputPrivacyKeyVoiceMessages();
 	case Key::About: return MTP_inputPrivacyKeyAbout();
 	case Key::Birthday: return MTP_inputPrivacyKeyBirthday();
+	case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave();
 	}
 	Unexpected("Key in Api::UserPrivacy::KetToTL.");
 }
@@ -228,6 +239,8 @@ std::optional<UserPrivacy::Key> TLToKey(mtpTypeId type) {
 	case mtpc_inputPrivacyKeyAbout: return Key::About;
 	case mtpc_privacyKeyBirthday:
 	case mtpc_inputPrivacyKeyBirthday: return Key::Birthday;
+	case mtpc_privacyKeyStarGiftsAutoSave:
+	case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave;
 	}
 	return std::nullopt;
 }
diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h
index 471f41f48..a1f66189f 100644
--- a/Telegram/SourceFiles/api/api_user_privacy.h
+++ b/Telegram/SourceFiles/api/api_user_privacy.h
@@ -31,6 +31,7 @@ public:
 		Voices,
 		About,
 		Birthday,
+		GiftsAutoSave,
 	};
 	enum class Option {
 		Everyone,
@@ -41,6 +42,7 @@ public:
 	struct Exceptions {
 		std::vector<not_null<PeerData*>> peers;
 		bool premiums = false;
+		bool miniapps = false;
 	};
 	struct Rule {
 		Option option = Option::Everyone;
diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp
index c9a9055e1..d90059cb9 100644
--- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp
@@ -36,13 +36,64 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_settings.h"
 #include "styles/style_layers.h"
 #include "styles/style_menu_icons.h"
+#include "styles/style_window.h"
 
 namespace {
 
 constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;
+constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;
 
 using Exceptions = Api::UserPrivacy::Exceptions;
 
+enum class SpecialRowType {
+	Premiums,
+	MiniApps,
+};
+
+[[nodiscard]] PaintRoundImageCallback GeneratePremiumsUserpicCallback(
+		bool forceRound) {
+	return [=](QPainter &p, int x, int y, int outerWidth, int size) {
+		auto gradient = QLinearGradient(
+			QPointF(x, y),
+			QPointF(x + size, y + size));
+		gradient.setStops(Ui::Premium::ButtonGradientStops());
+
+		auto hq = PainterHighQualityEnabler(p);
+		p.setPen(Qt::NoPen);
+		p.setBrush(gradient);
+		if (forceRound) {
+			p.drawEllipse(x, y, size, size);
+		} else {
+			const auto radius = size * Ui::ForumUserpicRadiusMultiplier();
+			p.drawRoundedRect(x, y, size, size, radius, radius);
+		}
+		st::settingsPrivacyPremium.paintInCenter(p, QRect(x, y, size, size));
+	};
+}
+
+[[nodiscard]] PaintRoundImageCallback GenerateMiniAppsUserpicCallback(
+		bool forceRound) {
+	return [=](QPainter &p, int x, int y, int outerWidth, int size) {
+		const auto &color1 = st::historyPeer6UserpicBg;
+		const auto &color2 = st::historyPeer6UserpicBg2;
+
+		const auto rect = style::rtlrect(x, y, size, size, outerWidth);
+		auto hq = PainterHighQualityEnabler(p);
+		auto gradient = QLinearGradient(x, y, x, y + size);
+		gradient.setStops({ { 0., color1->c }, { 1., color2->c } });
+
+		p.setPen(Qt::NoPen);
+		p.setBrush(gradient);
+		if (forceRound) {
+			p.drawEllipse(x, y, size, size);
+		} else {
+			const auto radius = size * Ui::ForumUserpicRadiusMultiplier();
+			p.drawRoundedRect(x, y, size, size, radius, radius);
+		}
+		st::windowFilterTypeBots.paintInCenter(p, QRect(x, y, size, size));
+	};
+}
+
 void CreateRadiobuttonLock(
 		not_null<Ui::RpWidget*> widget,
 		const style::Checkbox &st) {
@@ -102,7 +153,7 @@ public:
 		not_null<Main::Session*> session,
 		rpl::producer<QString> title,
 		const Exceptions &selected,
-		bool allowChoosePremiums);
+		std::optional<SpecialRowType> allowChooseSpecial);
 
 	Main::Session &session() const override;
 	void rowClicked(not_null<PeerListRow*> row) override;
@@ -110,18 +161,20 @@ public:
 	bool handleDeselectForeignRow(PeerListRowId itemId) override;
 
 	[[nodiscard]] bool premiumsSelected() const;
+	[[nodiscard]] bool miniAppsSelected() const;
 
 protected:
 	void prepareViewHook() override;
 	std::unique_ptr<Row> createRow(not_null<History*> history) override;
 
 private:
-	[[nodiscard]] object_ptr<Ui::RpWidget> preparePremiumsRowList();
+	[[nodiscard]] object_ptr<Ui::RpWidget> prepareSpecialRowList(
+		SpecialRowType type);
 
 	const not_null<Main::Session*> _session;
 	rpl::producer<QString> _title;
 	Exceptions _selected;
-	bool _allowChoosePremiums = false;
+	std::optional<SpecialRowType> _allowChooseSpecial;
 
 	PeerListContentDelegate *_typesDelegate = nullptr;
 	Fn<void(PeerListRowId)> _deselectOption;
@@ -133,9 +186,9 @@ struct RowSelectionChange {
 	bool checked = false;
 };
 
-class PremiumsRow final : public PeerListRow {
+class SpecialRow final : public PeerListRow {
 public:
-	PremiumsRow();
+	explicit SpecialRow(SpecialRowType type);
 
 	QString generateName() override;
 	QString generateShortName() override;
@@ -143,70 +196,68 @@ public:
 		bool forceRound) override;
 	bool useForumLikeUserpic() const override;
 
+private:
+	const SpecialRowType _type = SpecialRowType::Premiums;
+
 };
 
 class TypesController final : public PeerListController {
 public:
-	TypesController(not_null<Main::Session*> session, bool premiums);
+	TypesController(not_null<Main::Session*> session, SpecialRowType type);
 
 	Main::Session &session() const override;
 	void prepare() override;
 	void rowClicked(not_null<PeerListRow*> row) override;
 
-	[[nodiscard]] bool premiumsSelected() const;
-	[[nodiscard]] rpl::producer<bool> premiumsChanges() const;
+	[[nodiscard]] bool specialSelected() const;
+	[[nodiscard]] rpl::producer<bool> specialChanges() const;
 	[[nodiscard]] auto rowSelectionChanges() const
 		-> rpl::producer<RowSelectionChange>;
 
 private:
 	const not_null<Main::Session*> _session;
+	const SpecialRowType _type;
 
 	rpl::event_stream<> _selectionChanged;
 	rpl::event_stream<RowSelectionChange> _rowSelectionChanges;
 
 };
 
-PremiumsRow::PremiumsRow() : PeerListRow(kPremiumsRowId) {
-	setCustomStatus(tr::lng_edit_privacy_premium_status(tr::now));
+SpecialRow::SpecialRow(SpecialRowType type)
+: PeerListRow((type == SpecialRowType::Premiums)
+	? kPremiumsRowId
+	: kMiniAppsRowId) {
+	setCustomStatus((id() == kPremiumsRowId)
+		? tr::lng_edit_privacy_premium_status(tr::now)
+		: tr::lng_edit_privacy_miniapps_status(tr::now));
 }
 
-QString PremiumsRow::generateName() {
-	return tr::lng_edit_privacy_premium(tr::now);
+QString SpecialRow::generateName() {
+	return (id() == kPremiumsRowId)
+		? tr::lng_edit_privacy_premium(tr::now)
+		: tr::lng_edit_privacy_miniapps(tr::now);
 }
 
-QString PremiumsRow::generateShortName() {
+QString SpecialRow::generateShortName() {
 	return generateName();
 }
 
-PaintRoundImageCallback PremiumsRow::generatePaintUserpicCallback(
+PaintRoundImageCallback SpecialRow::generatePaintUserpicCallback(
 		bool forceRound) {
-	return [=](QPainter &p, int x, int y, int outerWidth, int size) {
-		auto gradient = QLinearGradient(
-			QPointF(x, y),
-			QPointF(x + size, y + size));
-		gradient.setStops(Ui::Premium::ButtonGradientStops());
-
-		auto hq = PainterHighQualityEnabler(p);
-		p.setPen(Qt::NoPen);
-		p.setBrush(gradient);
-		if (forceRound) {
-			p.drawEllipse(x, y, size, size);
-		} else {
-			const auto radius = size * Ui::ForumUserpicRadiusMultiplier();
-			p.drawRoundedRect(x, y, size, size, radius, radius);
-		}
-		st::settingsPrivacyPremium.paintInCenter(p, QRect(x, y, size, size));
-	};
+	return (id() == kPremiumsRowId)
+		? GeneratePremiumsUserpicCallback(forceRound)
+		: GenerateMiniAppsUserpicCallback(forceRound);
 }
 
-bool PremiumsRow::useForumLikeUserpic() const {
+bool SpecialRow::useForumLikeUserpic() const {
 	return true;
 }
 
 TypesController::TypesController(
 	not_null<Main::Session*> session,
-	bool premiums)
-: _session(session) {
+	SpecialRowType type)
+: _session(session)
+, _type(type) {
 }
 
 Main::Session &TypesController::session() const {
@@ -214,12 +265,15 @@ Main::Session &TypesController::session() const {
 }
 
 void TypesController::prepare() {
-	delegate()->peerListAppendRow(std::make_unique<PremiumsRow>());
+	delegate()->peerListAppendRow(std::make_unique<SpecialRow>(_type));
 	delegate()->peerListRefreshRows();
 }
 
-bool TypesController::premiumsSelected() const {
-	const auto row = delegate()->peerListFindRow(kPremiumsRowId);
+bool TypesController::specialSelected() const {
+	const auto premiums = (_type == SpecialRowType::Premiums);
+	const auto row = delegate()->peerListFindRow(premiums
+		? kPremiumsRowId
+		: kMiniAppsRowId);
 	Assert(row != nullptr);
 
 	return row->checked();
@@ -231,10 +285,10 @@ void TypesController::rowClicked(not_null<PeerListRow*> row) {
 	_rowSelectionChanges.fire({ row, checked });
 }
 
-rpl::producer<bool> TypesController::premiumsChanges() const {
+rpl::producer<bool> TypesController::specialChanges() const {
 	return _rowSelectionChanges.events(
 	) | rpl::map([=] {
-		return premiumsSelected();
+		return specialSelected();
 	});
 }
 
@@ -247,12 +301,12 @@ PrivacyExceptionsBoxController::PrivacyExceptionsBoxController(
 	not_null<Main::Session*> session,
 	rpl::producer<QString> title,
 	const Exceptions &selected,
-	bool allowChoosePremiums)
+	std::optional<SpecialRowType> allowChooseSpecial)
 : ChatsListBoxController(session)
 , _session(session)
 , _title(std::move(title))
 , _selected(selected)
-, _allowChoosePremiums(allowChoosePremiums) {
+, _allowChooseSpecial(allowChooseSpecial) {
 }
 
 Main::Session &PrivacyExceptionsBoxController::session() const {
@@ -261,14 +315,18 @@ Main::Session &PrivacyExceptionsBoxController::session() const {
 
 void PrivacyExceptionsBoxController::prepareViewHook() {
 	delegate()->peerListSetTitle(std::move(_title));
-	if (_allowChoosePremiums || _selected.premiums) {
-		delegate()->peerListSetAboveWidget(preparePremiumsRowList());
+	if (_allowChooseSpecial || _selected.premiums || _selected.miniapps) {
+		delegate()->peerListSetAboveWidget(prepareSpecialRowList(
+			_allowChooseSpecial.value_or(_selected.premiums
+				? SpecialRowType::Premiums
+				: SpecialRowType::MiniApps)));
 	}
 	delegate()->peerListAddSelectedPeers(_selected.peers);
 }
 
 bool PrivacyExceptionsBoxController::isForeignRow(PeerListRowId itemId) {
-	return (itemId == kPremiumsRowId);
+	return (itemId == kPremiumsRowId)
+		|| (itemId == kMiniAppsRowId);
 }
 
 bool PrivacyExceptionsBoxController::handleDeselectForeignRow(
@@ -280,7 +338,8 @@ bool PrivacyExceptionsBoxController::handleDeselectForeignRow(
 	return false;
 }
 
-auto PrivacyExceptionsBoxController::preparePremiumsRowList()
+auto PrivacyExceptionsBoxController::prepareSpecialRowList(
+	SpecialRowType type)
 -> object_ptr<Ui::RpWidget> {
 	auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
 	const auto container = result.data();
@@ -291,30 +350,39 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList()
 	_typesDelegate = lifetime.make_state<PeerListContentDelegateSimple>();
 	const auto controller = lifetime.make_state<TypesController>(
 		&session(),
-		_selected.premiums);
+		type);
 	const auto content = result->add(object_ptr<PeerListContent>(
 		container,
 		controller));
 	_typesDelegate->setContent(content);
 	controller->setDelegate(_typesDelegate);
 
+	const auto selectType = [&](PeerListRowId id) {
+		const auto row = _typesDelegate->peerListFindRow(id);
+		if (row) {
+			content->changeCheckState(row, true, anim::type::instant);
+			this->delegate()->peerListSetForeignRowChecked(
+				row,
+				true,
+				anim::type::instant);
+		}
+	};
 	if (_selected.premiums) {
-		const auto row = _typesDelegate->peerListFindRow(kPremiumsRowId);
-		Assert(row != nullptr);
-
-		content->changeCheckState(row, true, anim::type::instant);
-		this->delegate()->peerListSetForeignRowChecked(
-			row,
-			true,
-			anim::type::instant);
+		selectType(kPremiumsRowId);
+	} else if (_selected.miniapps) {
+		selectType(kMiniAppsRowId);
 	}
 	container->add(CreatePeerListSectionSubtitle(
 		container,
 		tr::lng_edit_privacy_users_and_groups()));
 
-	controller->premiumsChanges(
-	) | rpl::start_with_next([=](bool premiums) {
-		_selected.premiums = premiums;
+	controller->specialChanges(
+	) | rpl::start_with_next([=](bool chosen) {
+		if (type == SpecialRowType::Premiums) {
+			_selected.premiums = chosen;
+		} else {
+			_selected.miniapps = chosen;
+		}
 	}, lifetime);
 
 	controller->rowSelectionChanges(
@@ -329,6 +397,8 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList()
 		if (const auto row = _typesDelegate->peerListFindRow(itemId)) {
 			if (itemId == kPremiumsRowId) {
 				_selected.premiums = false;
+			} else if (itemId == kMiniAppsRowId) {
+				_selected.miniapps = false;
 			}
 			_typesDelegate->peerListSetRowChecked(row, false);
 		}
@@ -337,10 +407,14 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList()
 	return result;
 }
 
-[[nodiscard]] bool PrivacyExceptionsBoxController::premiumsSelected() const {
+bool PrivacyExceptionsBoxController::premiumsSelected() const {
 	return _selected.premiums;
 }
 
+bool PrivacyExceptionsBoxController::miniAppsSelected() const {
+	return _selected.miniapps;
+}
+
 void PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {
 	const auto peer = row->peer();
 
@@ -412,6 +486,11 @@ EditPrivacyBox::EditPrivacyBox(
 		// If we switch from Everyone to Contacts or Nobody suggest Premiums.
 		_value.always.premiums = true;
 	}
+	if (_controller->allowMiniAppsToggle(Exception::Always)
+		&& _value.option == Option::Everyone) {
+		// If we switch from Everyone to Contacts or Nobody suggest MiniApps.
+		_value.always.miniapps = true;
+	}
 }
 
 void EditPrivacyBox::prepare() {
@@ -427,12 +506,18 @@ void EditPrivacyBox::editExceptions(
 		&_window->session(),
 		_controller->exceptionBoxTitle(exception),
 		exceptions(exception),
-		_controller->allowPremiumsToggle(exception));
+		(_controller->allowPremiumsToggle(exception)
+			? SpecialRowType::Premiums
+			: _controller->allowMiniAppsToggle(exception)
+			? SpecialRowType::MiniApps
+			: std::optional<SpecialRowType>()));
 	auto initBox = [=, controller = controller.get()](
 			not_null<PeerListBox*> box) {
 		box->addButton(tr::lng_settings_save(), crl::guard(this, [=] {
-			exceptions(exception).peers = box->collectSelectedRows();
-			exceptions(exception).premiums = controller->premiumsSelected();
+			auto &setTo = exceptions(exception);
+			setTo.peers = box->collectSelectedRows();
+			setTo.premiums = controller->premiumsSelected();
+			setTo.miniapps = controller->miniAppsSelected();
 			const auto type = [&] {
 				switch (exception) {
 				case Exception::Always: return Exception::Never;
@@ -440,11 +525,17 @@ void EditPrivacyBox::editExceptions(
 				}
 				Unexpected("Invalid exception value.");
 			}();
-			auto &removeFrom = exceptions(type).peers;
+			auto &removeFrom = exceptions(type);
 			for (const auto peer : exceptions(exception).peers) {
-				removeFrom.erase(
-					ranges::remove(removeFrom, peer),
-					end(removeFrom));
+				removeFrom.peers.erase(
+					ranges::remove(removeFrom.peers, peer),
+					end(removeFrom.peers));
+			}
+			if (setTo.premiums) {
+				removeFrom.premiums = false;
+			}
+			if (setTo.miniapps) {
+				removeFrom.miniapps = false;
 			}
 			done();
 			box->closeBox();
@@ -566,14 +657,21 @@ void EditPrivacyBox::setupContent() {
 					lt_count,
 					count)
 				: tr::lng_edit_privacy_exceptions_add(tr::now);
-			return !value.premiums
-				? users
-				: !count
-				? tr::lng_edit_privacy_premium(tr::now)
-				: tr::lng_edit_privacy_exceptions_premium_and(
-					tr::now,
-					lt_users,
-					users);
+			return value.premiums
+				? (!count
+					? tr::lng_edit_privacy_premium(tr::now)
+					: tr::lng_edit_privacy_exceptions_premium_and(
+						tr::now,
+						lt_users,
+						users))
+				: value.miniapps
+				? (!count
+					? tr::lng_edit_privacy_miniapps(tr::now)
+					: tr::lng_edit_privacy_exceptions_miniapps_and(
+						tr::now,
+						lt_users,
+						users))
+				: users;
 		});
 		_controller->handleExceptionsChange(
 			exception,
diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h
index 3aae28403..bd87e90f2 100644
--- a/Telegram/SourceFiles/boxes/edit_privacy_box.h
+++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h
@@ -61,6 +61,10 @@ public:
 			Exception exception) const {
 		return false;
 	}
+	[[nodiscard]] virtual bool allowMiniAppsToggle(
+			Exception exception) const {
+		return false;
+	}
 	virtual void handleExceptionsChange(
 		Exception exception,
 		rpl::producer<int> value) {
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 6b2a753ce..db18d216e 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -533,6 +533,7 @@ inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey;
 inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey;
 inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey;
 inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey;
+inputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey;
 
 privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey;
 privacyKeyChatInvite#500e6dfa = PrivacyKey;
@@ -545,6 +546,7 @@ privacyKeyAddedByPhone#42ffd42b = PrivacyKey;
 privacyKeyVoiceMessages#697f414 = PrivacyKey;
 privacyKeyAbout#a486b761 = PrivacyKey;
 privacyKeyBirthday#2000a518 = PrivacyKey;
+privacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey;
 
 inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule;
 inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule;
@@ -556,6 +558,8 @@ inputPrivacyValueAllowChatParticipants#840649cf chats:Vector<long> = InputPrivac
 inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector<long> = InputPrivacyRule;
 inputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule;
 inputPrivacyValueAllowPremium#77cdc9f1 = InputPrivacyRule;
+inputPrivacyValueAllowBots#5a4fcce5 = InputPrivacyRule;
+inputPrivacyValueDisallowBots#c4e57915 = InputPrivacyRule;
 
 privacyValueAllowContacts#fffe1bac = PrivacyRule;
 privacyValueAllowAll#65427b82 = PrivacyRule;
@@ -567,6 +571,8 @@ privacyValueAllowChatParticipants#6b134e8e chats:Vector<long> = PrivacyRule;
 privacyValueDisallowChatParticipants#41c87565 chats:Vector<long> = PrivacyRule;
 privacyValueAllowCloseFriends#f7e8d89b = PrivacyRule;
 privacyValueAllowPremium#ece9814b = PrivacyRule;
+privacyValueAllowBots#21461b5d = PrivacyRule;
+privacyValueDisallowBots#f6a5f82f = PrivacyRule;
 
 account.privacyRules#50a04e45 rules:Vector<PrivacyRule> chats:Vector<Chat> users:Vector<User> = account.PrivacyRules;
 
diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
index dc9a7bcc3..c93c50aae 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
@@ -380,11 +380,13 @@ void BlockedBoxController::rowClicked(not_null<PeerListRow*> row) {
 	});
 }
 
-void BlockedBoxController::rowRightActionClicked(not_null<PeerListRow*> row) {
+void BlockedBoxController::rowRightActionClicked(
+		not_null<PeerListRow*> row) {
 	session().api().blockedPeers().unblock(row->peer());
 }
 
-void BlockedBoxController::applySlice(const Api::BlockedPeers::Slice &slice) {
+void BlockedBoxController::applySlice(
+		const Api::BlockedPeers::Slice &slice) {
 	if (slice.list.empty()) {
 		_allLoaded = true;
 	}
@@ -487,7 +489,8 @@ rpl::producer<QString> PhoneNumberPrivacyController::title() const {
 	return tr::lng_edit_privacy_phone_number_title();
 }
 
-rpl::producer<QString> PhoneNumberPrivacyController::optionsTitleKey() const {
+auto PhoneNumberPrivacyController::optionsTitleKey() const
+-> rpl::producer<QString> {
 	return tr::lng_edit_privacy_phone_number_header();
 }
 
@@ -731,7 +734,8 @@ void LastSeenPrivacyController::handleExceptionsChange(
 void LastSeenPrivacyController::confirmSave(
 		bool someAreDisallowed,
 		Fn<void()> saveCallback) {
-	if (someAreDisallowed && !Core::App().settings().lastSeenWarningSeen()) {
+	if (someAreDisallowed
+		&& !Core::App().settings().lastSeenWarningSeen()) {
 		auto callback = [
 			=,
 			saveCallback = std::move(saveCallback)
@@ -776,11 +780,14 @@ rpl::producer<QString> GroupsInvitePrivacyController::optionsTitleKey(
 	return tr::lng_edit_privacy_groups_header();
 }
 
-rpl::producer<QString> GroupsInvitePrivacyController::exceptionButtonTextKey(
-		Exception exception) const {
+auto GroupsInvitePrivacyController::exceptionButtonTextKey(
+	Exception exception) const
+-> rpl::producer<QString> {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_groups_always_empty();
-	case Exception::Never: return tr::lng_edit_privacy_groups_never_empty();
+	case Exception::Always:
+		return tr::lng_edit_privacy_groups_always_empty();
+	case Exception::Never:
+		return tr::lng_edit_privacy_groups_never_empty();
 	}
 	Unexpected("Invalid exception value.");
 }
@@ -788,8 +795,10 @@ rpl::producer<QString> GroupsInvitePrivacyController::exceptionButtonTextKey(
 rpl::producer<QString> GroupsInvitePrivacyController::exceptionBoxTitle(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_groups_always_title();
-	case Exception::Never: return tr::lng_edit_privacy_groups_never_title();
+	case Exception::Always:
+		return tr::lng_edit_privacy_groups_always_title();
+	case Exception::Never:
+		return tr::lng_edit_privacy_groups_never_title();
 	}
 	Unexpected("Invalid exception value.");
 }
@@ -819,8 +828,10 @@ rpl::producer<QString> CallsPrivacyController::optionsTitleKey() const {
 rpl::producer<QString> CallsPrivacyController::exceptionButtonTextKey(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_calls_always_empty();
-	case Exception::Never: return tr::lng_edit_privacy_calls_never_empty();
+	case Exception::Always:
+		return tr::lng_edit_privacy_calls_always_empty();
+	case Exception::Never:
+		return tr::lng_edit_privacy_calls_never_empty();
 	}
 	Unexpected("Invalid exception value.");
 }
@@ -828,7 +839,8 @@ rpl::producer<QString> CallsPrivacyController::exceptionButtonTextKey(
 rpl::producer<QString> CallsPrivacyController::exceptionBoxTitle(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_calls_always_title();
+	case Exception::Always:
+		return tr::lng_edit_privacy_calls_always_title();
 	case Exception::Never: return tr::lng_edit_privacy_calls_never_title();
 	}
 	Unexpected("Invalid exception value.");
@@ -871,7 +883,8 @@ rpl::producer<QString> CallsPeer2PeerPrivacyController::title() const {
 	return tr::lng_edit_privacy_calls_p2p_title();
 }
 
-rpl::producer<QString> CallsPeer2PeerPrivacyController::optionsTitleKey() const {
+auto CallsPeer2PeerPrivacyController::optionsTitleKey() const
+-> rpl::producer<QString> {
 	return tr::lng_edit_privacy_calls_p2p_header();
 }
 
@@ -895,8 +908,9 @@ auto CallsPeer2PeerPrivacyController::warning() const
 	return tr::lng_settings_peer_to_peer_about(Ui::Text::WithEntities);
 }
 
-rpl::producer<QString> CallsPeer2PeerPrivacyController::exceptionButtonTextKey(
-		Exception exception) const {
+auto CallsPeer2PeerPrivacyController::exceptionButtonTextKey(
+	Exception exception) const
+-> rpl::producer<QString> {
 	switch (exception) {
 	case Exception::Always: {
 		return tr::lng_edit_privacy_calls_p2p_always_empty();
@@ -1121,7 +1135,8 @@ rpl::producer<QString> ProfilePhotoPrivacyController::title() const {
 	return tr::lng_edit_privacy_profile_photo_title();
 }
 
-rpl::producer<QString> ProfilePhotoPrivacyController::optionsTitleKey() const {
+auto ProfilePhotoPrivacyController::optionsTitleKey() const
+-> rpl::producer<QString> {
 	return tr::lng_edit_privacy_profile_photo_header();
 }
 
@@ -1234,7 +1249,8 @@ object_ptr<Ui::RpWidget> ProfilePhotoPrivacyController::setupMiddleWidget(
 					container,
 					&controller->window(),
 					{
-						.confirm = tr::lng_profile_set_photo_button(tr::now),
+						.confirm = tr::lng_profile_set_photo_button(
+							tr::now),
 						.cropType = EditorData::CropType::Ellipse,
 						.keepAspectRatio = true,
 					},
@@ -1283,8 +1299,9 @@ void ProfilePhotoPrivacyController::saveAdditional() {
 	}
 }
 
-rpl::producer<QString> ProfilePhotoPrivacyController::exceptionButtonTextKey(
-		Exception exception) const {
+auto ProfilePhotoPrivacyController::exceptionButtonTextKey(
+	Exception exception) const
+-> rpl::producer<QString> {
 	switch (exception) {
 	case Exception::Always: {
 		return tr::lng_edit_privacy_profile_photo_always_empty();
@@ -1363,8 +1380,10 @@ rpl::producer<QString> VoicesPrivacyController::optionsTitleKey() const {
 rpl::producer<QString> VoicesPrivacyController::exceptionButtonTextKey(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_voices_always_empty();
-	case Exception::Never: return tr::lng_edit_privacy_voices_never_empty();
+	case Exception::Always:
+		return tr::lng_edit_privacy_voices_always_empty();
+	case Exception::Never:
+		return tr::lng_edit_privacy_voices_never_empty();
 	}
 	Unexpected("Invalid exception value.");
 }
@@ -1372,7 +1391,8 @@ rpl::producer<QString> VoicesPrivacyController::exceptionButtonTextKey(
 rpl::producer<QString> VoicesPrivacyController::exceptionBoxTitle(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_voices_always_title();
+	case Exception::Always:
+		return tr::lng_edit_privacy_voices_always_title();
 	case Exception::Never: return tr::lng_edit_privacy_voices_never_title();
 	}
 	Unexpected("Invalid exception value.");
@@ -1468,7 +1488,8 @@ rpl::producer<QString> AboutPrivacyController::optionsTitleKey() const {
 rpl::producer<QString> AboutPrivacyController::exceptionButtonTextKey(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_about_always_empty();
+	case Exception::Always:
+		return tr::lng_edit_privacy_about_always_empty();
 	case Exception::Never: return tr::lng_edit_privacy_about_never_empty();
 	}
 	Unexpected("Invalid exception value.");
@@ -1477,7 +1498,8 @@ rpl::producer<QString> AboutPrivacyController::exceptionButtonTextKey(
 rpl::producer<QString> AboutPrivacyController::exceptionBoxTitle(
 		Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_about_always_title();
+	case Exception::Always:
+		return tr::lng_edit_privacy_about_always_title();
 	case Exception::Never: return tr::lng_edit_privacy_about_never_title();
 	}
 	Unexpected("Invalid exception value.");
@@ -1485,7 +1507,7 @@ rpl::producer<QString> AboutPrivacyController::exceptionBoxTitle(
 
 auto AboutPrivacyController::exceptionsDescription() const
 -> rpl::producer<QString> {
-	return tr::lng_edit_privacy_birthday_exceptions();
+	return tr::lng_edit_privacy_about_exceptions();
 }
 
 UserPrivacy::Key BirthdayPrivacyController::key() const {
@@ -1503,8 +1525,10 @@ rpl::producer<QString> BirthdayPrivacyController::optionsTitleKey() const {
 rpl::producer<QString> BirthdayPrivacyController::exceptionButtonTextKey(
 	Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_birthday_always_empty();
-	case Exception::Never: return tr::lng_edit_privacy_birthday_never_empty();
+	case Exception::Always:
+		return tr::lng_edit_privacy_birthday_always_empty();
+	case Exception::Never:
+		return tr::lng_edit_privacy_birthday_never_empty();
 	}
 	Unexpected("Invalid exception value.");
 }
@@ -1512,8 +1536,10 @@ rpl::producer<QString> BirthdayPrivacyController::exceptionButtonTextKey(
 rpl::producer<QString> BirthdayPrivacyController::exceptionBoxTitle(
 	Exception exception) const {
 	switch (exception) {
-	case Exception::Always: return tr::lng_edit_privacy_birthday_always_title();
-	case Exception::Never: return tr::lng_edit_privacy_birthday_never_title();
+	case Exception::Always:
+		return tr::lng_edit_privacy_birthday_always_title();
+	case Exception::Never:
+		return tr::lng_edit_privacy_birthday_never_title();
 	}
 	Unexpected("Invalid exception value.");
 }
@@ -1553,4 +1579,49 @@ object_ptr<Ui::RpWidget> BirthdayPrivacyController::setupAboveWidget(
 	return result;
 }
 
+UserPrivacy::Key GiftsAutoSavePrivacyController::key() const {
+	return Key::GiftsAutoSave;
+}
+
+rpl::producer<QString> GiftsAutoSavePrivacyController::title() const {
+	return tr::lng_edit_privacy_gifts_title();
+}
+
+auto GiftsAutoSavePrivacyController::optionsTitleKey() const
+-> rpl::producer<QString> {
+	return tr::lng_edit_privacy_gifts_header();
+}
+
+auto GiftsAutoSavePrivacyController::exceptionButtonTextKey(
+	Exception exception) const
+-> rpl::producer<QString> {
+	switch (exception) {
+	case Exception::Always:
+		return tr::lng_edit_privacy_gifts_always_empty();
+	case Exception::Never:
+		return tr::lng_edit_privacy_gifts_never_empty();
+	}
+	Unexpected("Invalid exception value.");
+}
+
+rpl::producer<QString> GiftsAutoSavePrivacyController::exceptionBoxTitle(
+		Exception exception) const {
+	switch (exception) {
+	case Exception::Always:
+		return tr::lng_edit_privacy_gifts_always_title();
+	case Exception::Never: return tr::lng_edit_privacy_gifts_never_title();
+	}
+	Unexpected("Invalid exception value.");
+}
+
+auto GiftsAutoSavePrivacyController::exceptionsDescription() const
+-> rpl::producer<QString> {
+	return tr::lng_edit_privacy_lastseen_exceptions();
+}
+
+bool GiftsAutoSavePrivacyController::allowMiniAppsToggle(
+		Exception exception) const {
+	return true;
+}
+
 } // namespace Settings
diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h
index d058649d2..88a61d9cd 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h
+++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h
@@ -194,7 +194,7 @@ public:
 
 };
 
-class ForwardsPrivacyController
+class ForwardsPrivacyController final
 	: public EditPrivacyController
 	, private HistoryView::SimpleElementDelegate {
 public:
@@ -340,4 +340,22 @@ public:
 
 };
 
+class GiftsAutoSavePrivacyController final : public EditPrivacyController {
+public:
+	using Option = EditPrivacyBox::Option;
+	using Exception = EditPrivacyBox::Exception;
+
+	Key key() const override;
+
+	rpl::producer<QString> title() const override;
+	rpl::producer<QString> optionsTitleKey() const override;
+	rpl::producer<QString> exceptionButtonTextKey(
+		Exception exception) const override;
+	rpl::producer<QString> exceptionBoxTitle(
+		Exception exception) const override;
+	rpl::producer<QString> exceptionsDescription() const override;
+	bool allowMiniAppsToggle(Exception exception) const override;
+
+};
+
 } // namespace Settings
diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
index f71134c3c..3222bc248 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
@@ -185,16 +185,23 @@ QString PrivacyBase(Privacy::Key key, const Privacy::Rule &rule) {
 		[[fallthrough]];
 	default:
 		switch (rule.option) {
-		case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now);
+		case Option::Everyone:
+			return rule.never.miniapps
+				? tr::lng_edit_privacy_no_miniapps(tr::now)
+				: tr::lng_edit_privacy_everyone(tr::now);
 		case Option::Contacts:
 			return rule.always.premiums
 				? tr::lng_edit_privacy_contacts_and_premium(tr::now)
+				: rule.always.miniapps
+				? tr::lng_edit_privacy_contacts_and_miniapps(tr::now)
 				: tr::lng_edit_privacy_contacts(tr::now);
 		case Option::CloseFriends:
 			return tr::lng_edit_privacy_close_friends(tr::now);
 		case Option::Nobody:
 			return rule.always.premiums
 				? tr::lng_edit_privacy_premium(tr::now)
+				: rule.always.miniapps
+				? tr::lng_edit_privacy_miniapps(tr::now)
 				: tr::lng_edit_privacy_nobody(tr::now);
 		}
 		Unexpected("Value in Privacy::Option.");
@@ -405,7 +412,8 @@ void SetupPrivacy(
 	add(
 		tr::lng_settings_last_seen(),
 		Key::LastSeen,
-		[=] { return std::make_unique<LastSeenPrivacyController>(session); });
+		[=] { return std::make_unique<LastSeenPrivacyController>(
+			session); });
 	add(
 		tr::lng_settings_profile_photo_privacy(),
 		Key::ProfilePhoto,
@@ -414,6 +422,10 @@ void SetupPrivacy(
 		tr::lng_settings_bio_privacy(),
 		Key::About,
 		[] { return std::make_unique<AboutPrivacyController>(); });
+	add(
+		tr::lng_settings_gifts_privacy(),
+		Key::GiftsAutoSave,
+		[=] { return std::make_unique<GiftsAutoSavePrivacyController>(); });
 	add(
 		tr::lng_settings_birthday_privacy(),
 		Key::Birthday,
@@ -442,7 +454,8 @@ void SetupPrivacy(
 	}
 	AddMessagesPrivacyButton(controller, container);
 
-	session->api().userPrivacy().reload(Api::UserPrivacy::Key::AddedByPhone);
+	session->api().userPrivacy().reload(
+		Api::UserPrivacy::Key::AddedByPhone);
 
 	Ui::AddSkip(container, st::settingsPrivacySecurityPadding);
 	Ui::AddDivider(container);