diff --git a/.devcontainer.json b/.devcontainer.json
index 30bff840d..366f6ed46 100644
--- a/.devcontainer.json
+++ b/.devcontainer.json
@@ -9,10 +9,7 @@
"--compile-commands-dir=${workspaceFolder}/out"
],
"cmake.generator": "Ninja Multi-Config",
- "cmake.buildDirectory": "${workspaceFolder}/out",
- "cmake.configureSettings": {
- "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
- }
+ "cmake.buildDirectory": "${workspaceFolder}/out"
},
"extensions": [
"ms-vscode.cpptools-extension-pack",
diff --git a/Telegram/Resources/icons/chat/mini_info_alert.png b/Telegram/Resources/icons/chat/mini_info_alert.png
new file mode 100644
index 000000000..8c79b97d3
Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_info_alert.png differ
diff --git a/Telegram/Resources/icons/chat/mini_info_alert@2x.png b/Telegram/Resources/icons/chat/mini_info_alert@2x.png
new file mode 100644
index 000000000..e36a5ed41
Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_info_alert@2x.png differ
diff --git a/Telegram/Resources/icons/chat/mini_info_alert@3x.png b/Telegram/Resources/icons/chat/mini_info_alert@3x.png
new file mode 100644
index 000000000..1c8cfe8ed
Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_info_alert@3x.png differ
diff --git a/Telegram/Resources/icons/payments/premium_emoji.png b/Telegram/Resources/icons/payments/premium_emoji.png
new file mode 100644
index 000000000..e8b8fcf29
Binary files /dev/null and b/Telegram/Resources/icons/payments/premium_emoji.png differ
diff --git a/Telegram/Resources/icons/payments/premium_emoji@2x.png b/Telegram/Resources/icons/payments/premium_emoji@2x.png
new file mode 100644
index 000000000..8824f11b9
Binary files /dev/null and b/Telegram/Resources/icons/payments/premium_emoji@2x.png differ
diff --git a/Telegram/Resources/icons/payments/premium_emoji@3x.png b/Telegram/Resources/icons/payments/premium_emoji@3x.png
new file mode 100644
index 000000000..5bd6ad013
Binary files /dev/null and b/Telegram/Resources/icons/payments/premium_emoji@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 1cf93bef0..6b8fbbe24 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1218,6 +1218,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_privacy_contacts" = "My contacts";
"lng_edit_privacy_close_friends" = "Close friends";
"lng_edit_privacy_contacts_and_premium" = "Contacts & Premium";
+"lng_edit_privacy_paid" = "Paid";
"lng_edit_privacy_contacts_and_miniapps" = "Contacts & Mini Apps";
"lng_edit_privacy_nobody" = "Nobody";
"lng_edit_privacy_premium" = "Premium users";
@@ -1356,6 +1357,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_messages_privacy_premium_about" = "Subscribe now to change this setting and get access to other exclusive features of Telegram Premium.";
"lng_messages_privacy_premium" = "Only subscribers of {link} can select this option.";
"lng_messages_privacy_premium_link" = "Telegram Premium";
+"lng_messages_privacy_charge" = "Charge for messages";
+"lng_messages_privacy_charge_about" = "Charge a fee for messages from people outside your contacts or those you haven't messaged first.";
+"lng_messages_privacy_price" = "Set your price per message";
+"lng_messages_privacy_price_about" = "You will receive {percent} of the selected fee ({amount}) for each incoming message.";
+"lng_messages_privacy_exceptions" = "Exceptions";
+"lng_messages_privacy_remove_fee" = "Remove Fee";
+"lng_messages_privacy_remove_about" = "Add users or entire groups who won't be charged for sending messages to you.";
"lng_self_destruct_title" = "Account self-destruction";
"lng_self_destruct_description" = "If you don't come online at least once within this period, your account will be deleted along with all groups, messages and contacts.";
@@ -2158,6 +2166,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_boost_apply#other" = "{from} boosted the group {count} times";
"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} 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";
@@ -2664,6 +2681,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_summary_history_entry_inner_in" = "In-App Purchase";
"lng_credits_summary_balance" = "Balance";
"lng_credits_commission" = "{amount} commission";
+"lng_credits_paid_messages_fee#one" = "Fee for {count} Message";
+"lng_credits_paid_messages_fee#other" = "Fee for {count} Messages";
+"lng_credits_paid_messages_fee_about" = "You receive {percent} of the price that you charge for each incoming message. {link}";
+"lng_credits_paid_messages_fee_about_link" = "Change Fee {emoji}";
+"lng_credits_paid_messages_full" = "Full Price";
+"lng_credits_premium_gift_duration" = "Duration";
"lng_credits_more_options" = "More Options";
"lng_credits_balance_me" = "your balance";
"lng_credits_buy_button" = "Buy More Stars";
@@ -2777,6 +2800,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts.";
"lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels.";
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
+"lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}.";
+"lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}";
@@ -3310,6 +3335,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}";
"lng_gift_premium_features" = "See Features >";
"lng_gift_premium_label" = "Premium";
+"lng_gift_premium_by_stars" = "or {amount}";
"lng_gift_stars_subtitle" = "Gift Stars";
"lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}";
"lng_gift_stars_link" = "What are Stars >";
@@ -3321,8 +3347,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_send_title" = "Send a Gift";
"lng_gift_send_message" = "Enter Message";
"lng_gift_send_anonymous" = "Hide My Name";
+"lng_gift_send_pay_with_stars" = "Pay with {amount}";
+"lng_gift_send_stars_balance" = "Your balance is {amount}. {link}";
+"lng_gift_send_stars_balance_link" = "Get More Stars >";
"lng_gift_send_anonymous_self" = "Hide my name and message from visitors to my profile.";
"lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.";
+"lng_gift_send_anonymous_about_paid" = "You can hide your name from visitors to {user}'s profile. {recipient} will still see your name.";
"lng_gift_send_anonymous_about_channel" = "You can hide your name and message from all visitors of this channel except its admins.";
"lng_gift_send_unique" = "Make Unique for {price}";
"lng_gift_send_unique_about" = "Enable this to let {user} turn your gift into a unique collectible. {link}";
@@ -3398,6 +3428,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_display_done_channel" = "The gift is now shown in channel's Gifts.";
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
"lng_gift_display_done_hide_channel" = "The gift is now hidden from channel's Gifts.";
+"lng_gift_pinned_done" = "The gift will always be shown on top.";
"lng_gift_got_stars#one" = "You got **{count} Star** for this gift.";
"lng_gift_got_stars#other" = "You got **{count} Stars** for this gift.";
"lng_gift_channel_got#one" = "Channel got **{count} Star** for this gift.";
@@ -3456,6 +3487,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_transfer_button_for" = "Transfer for {price}";
"lng_gift_transfer_wear" = "Wear";
"lng_gift_transfer_take_off" = "Take Off";
+"lng_gift_menu_show" = "Show";
+"lng_gift_menu_hide" = "Hide";
"lng_gift_wear_title" = "Wear {name}";
"lng_gift_wear_about" = "and get these benefits:";
"lng_gift_wear_badge_title" = "Radiant Badge";
@@ -3609,6 +3642,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_new_contact_from_request_group" = "{user} is an admin of {name}, a group you requested to join.";
"lng_new_contact_about_status" = "This account uses {emoji} as a custom status next to its\nname. Such emoji statuses are available to all\nsubscribers of {link}.";
"lng_new_contact_about_status_link" = "Telegram Premium";
+"lng_new_contact_not_contact" = "Not a contact";
+"lng_new_contact_phone_number" = "Phone number";
+"lng_new_contact_registration" = "Registration";
+"lng_new_contact_common_groups" = "Common groups";
+"lng_new_contact_groups#one" = "{count} group {emoji} {arrow}";
+"lng_new_contact_groups#other" = "{count} groups {emoji} {arrow}";
+"lng_new_contact_not_official" = "Not an official account";
+"lng_new_contact_updated_name" = "User updated name {when}";
+"lng_new_contact_updated_photo" = "User updated photo {when}";
+"lng_new_contact_updated_now" = "less than an hour ago";
+"lng_new_contact_updated_hours#one" = "{count} hour ago";
+"lng_new_contact_updated_hours#other" = "{count} hours ago";
+"lng_new_contact_updated_days#one" = "{count} day ago";
+"lng_new_contact_updated_days#other" = "{count} days ago";
+"lng_new_contact_updated_months#one" = "{count} month ago";
+"lng_new_contact_updated_months#other" = "{count} months ago";
"lng_from_request_title_channel" = "Response to your join request";
"lng_from_request_title_group" = "Response to your join request";
"lng_from_request_body" = "You received this message because you requested to join {name} on {date}.";
@@ -3637,6 +3686,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_anonymous_ph" = "Send anonymously...";
"lng_story_reply_ph" = "Reply privately...";
"lng_story_comment_ph" = "Comment story...";
+"lng_message_paid_ph" = "Message for {amount}";
"lng_send_text_no" = "Text not allowed.";
"lng_send_text_no_about" = "The admins of this group only allow sending {types}.";
"lng_send_text_type_and_last" = "{types} and {last}";
@@ -4798,6 +4848,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_boosts_no_restrict" = "Do not restrict boosters";
"lng_rights_boosts_about" = "Turn this on to always allow users who boosted your group to send messages and media.";
"lng_rights_boosts_about_on" = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages.";
+"lng_rights_charge_stars" = "Charge Stars for Messages";
+"lng_rights_charge_stars_about" = "If you turn this on, regular members of the group will have to pay Stars to send messages.";
+"lng_rights_charge_price" = "Set price per message";
+"lng_rights_charge_price_about" = "Your group will receive {percent} of the selected fee ({amount}) for each incoming message.";
"lng_slowmode_enabled" = "Slow Mode is active.\nYou can send your next message in {left}.";
"lng_slowmode_no_many" = "Slow mode is enabled. You can't send more than one message at a time.";
@@ -4805,6 +4859,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_slowmode_seconds#one" = "{count} second";
"lng_slowmode_seconds#other" = "{count} seconds";
+"lng_payment_confirm_title" = "Confirm payment";
+"lng_payment_confirm_text#one" = "{name} charges **{count}** Star per message.";
+"lng_payment_confirm_text#other" = "{name} charges **{count}** Stars per message.";
+"lng_payment_confirm_amount#one" = "**{count}** Star";
+"lng_payment_confirm_amount#other" = "**{count}** Stars";
+"lng_payment_confirm_users#one" = "You selected **{count}** user who charge Stars for messages.";
+"lng_payment_confirm_users#other" = "You selected **{count}** users who charge Stars for messages.";
+"lng_payment_confirm_chats#one" = "You selected **{count}** chat where you pay Stars for messages.";
+"lng_payment_confirm_chats#other" = "You selected **{count}** chats where you pay Stars for messages.";
+"lng_payment_confirm_sure#one" = "Would you like to pay {amount} to send **{count}** message?";
+"lng_payment_confirm_sure#other" = "Would you like to pay {amount} to send **{count}** messages?";
+"lng_payment_confirm_dont_ask" = "Don't ask me again";
+"lng_payment_confirm_button#one" = "Pay for {count} Message";
+"lng_payment_confirm_button#other" = "Pay for {count} Messages";
+"lng_payment_bar_text" = "{name} must pay {cost} for each message to you.";
+"lng_payment_bar_button" = "Remove Fee";
+"lng_payment_refund_title" = "Remove Fee";
+"lng_payment_refund_text" = "Are you sure you want to allow {name} to message you for free?";
+"lng_payment_refund_also#one" = "Refund already paid {count} Star";
+"lng_payment_refund_also#other" = "Refund already paid {count} Stars";
+"lng_payment_refund_confirm" = "Confirm";
+
"lng_rights_gigagroup_title" = "Broadcast group";
"lng_rights_gigagroup_convert" = "Convert to Broadcast Group";
"lng_rights_gigagroup_about" = "Broadcast groups can have over 200,000 members, but only admins can send messages in them.";
@@ -4936,6 +5012,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers.";
"lng_send_non_premium_message_toast_link" = "Telegram Premium";
+"lng_send_charges_stars_text" = "{user} charges {amount} for each message.";
+"lng_send_charges_stars_go" = "Buy Stars";
+
"lng_exceptions_list_title" = "Exceptions";
"lng_removed_list_title" = "Removed users";
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index f6451e78d..65aaca6cc 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="5.12.1.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index a5a4b7eaa..21f7f0286 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,11,1,0
- PRODUCTVERSION 5,11,1,0
+ FILEVERSION 5,12,1,0
+ PRODUCTVERSION 5,12,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
- VALUE "FileVersion", "5.11.1.0"
+ VALUE "FileVersion", "5.12.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "5.11.1.0"
+ VALUE "ProductVersion", "5.12.1.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 783f6f30f..6f72c4477 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,11,1,0
- PRODUCTVERSION 5,11,1,0
+ FILEVERSION 5,12,1,0
+ PRODUCTVERSION 5,12,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
- VALUE "FileVersion", "5.11.1.0"
+ VALUE "FileVersion", "5.12.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "5.11.1.0"
+ VALUE "ProductVersion", "5.12.1.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp
index d7a1636b3..55bbabbc5 100644
--- a/Telegram/SourceFiles/api/api_chat_filters.cpp
+++ b/Telegram/SourceFiles/api/api_chat_filters.cpp
@@ -149,18 +149,14 @@ void InitFilterLinkHeader(
iconEmoji
).value_or(Ui::FilterIcon::Custom)).active;
const auto isStatic = title.isStatic;
- const auto makeContext = [=](Fn repaint) {
- return Core::MarkedTextContext{
- .session = &box->peerListUiShow()->session(),
- .customEmojiRepaint = std::move(repaint),
- .customEmojiLoopLimit = isStatic ? -1 : 0,
- };
- };
auto header = Ui::MakeFilterLinkHeader(box, {
.type = type,
.title = TitleText(type)(tr::now),
.about = AboutText(type, title.text),
- .makeAboutContext = makeContext,
+ .aboutContext = Core::TextContext({
+ .session = &box->peerListUiShow()->session(),
+ .customEmojiLoopLimit = isStatic ? -1 : 0,
+ }),
.folderTitle = title.text,
.folderIcon = icon,
.badge = (type == Ui::FilterLinkHeaderType::AddingChats
@@ -560,16 +556,12 @@ void ShowImportToast(
text.append('\n').append(phrase(tr::now, lt_count, added));
}
const auto isStatic = title.isStatic;
- const auto makeContext = [=](not_null widget) {
- return Core::MarkedTextContext{
- .session = &strong->session(),
- .customEmojiRepaint = [=] { widget->update(); },
- .customEmojiLoopLimit = isStatic ? -1 : 0,
- };
- };
strong->showToast({
.text = std::move(text),
- .textContext = makeContext,
+ .textContext = Core::TextContext({
+ .session = &strong->session(),
+ .customEmojiLoopLimit = isStatic ? -1 : 0,
+ })
});
}
@@ -640,18 +632,14 @@ void ProcessFilterInvite(
raw->setRealContentHeight(box->heightValue());
const auto isStatic = title.isStatic;
- const auto makeContext = [=](Fn update) {
- return Core::MarkedTextContext{
- .session = &strong->session(),
- .customEmojiRepaint = update,
- .customEmojiLoopLimit = isStatic ? -1 : 0,
- };
- };
auto owned = Ui::FilterLinkProcessButton(
box,
type,
title.text,
- makeContext,
+ Core::TextContext({
+ .session = &strong->session(),
+ .customEmojiLoopLimit = isStatic ? -1 : 0,
+ }),
std::move(badge));
const auto button = owned.data();
@@ -873,18 +861,14 @@ void ProcessFilterRemove(
}, type, title, iconEmoji, rpl::single(0), horizontalFilters);
const auto isStatic = title.isStatic;
- const auto makeContext = [=](Fn update) {
- return Core::MarkedTextContext{
- .session = &strong->session(),
- .customEmojiRepaint = update,
- .customEmojiLoopLimit = isStatic ? -1 : 0,
- };
- };
auto owned = Ui::FilterLinkProcessButton(
box,
type,
title.text,
- makeContext,
+ Core::TextContext({
+ .session = &strong->session(),
+ .customEmojiLoopLimit = isStatic ? -1 : 0,
+ }),
std::move(badge));
const auto button = owned.data();
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index 77c30d095..c58f525c9 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -25,6 +25,7 @@ struct SendOptions {
TimeId scheduled = 0;
BusinessShortcutId shortcutId = 0;
EffectId effectId = 0;
+ int starsApproved = 0;
bool silent = false;
bool handleSupportSwitch = false;
bool invertCaption = false;
diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp
index bdb10d02d..0dc21c636 100644
--- a/Telegram/SourceFiles/api/api_credits.cpp
+++ b/Telegram/SourceFiles/api/api_credits.cpp
@@ -90,7 +90,13 @@ constexpr auto kTransactionsLimit = 100;
? peerFromMTP(*tl.data().vstarref_peer()).value
: 0;
const auto incoming = (amount >= StarsAmount());
- const auto saveActorId = (reaction || !extended.empty()) && incoming;
+ const auto paidMessagesCount
+ = tl.data().vpaid_messages().value_or_empty();
+ const auto premiumMonthsForStars
+ = tl.data().vpremium_gift_months().value_or_empty();
+ const auto saveActorId = (reaction
+ || !extended.empty()
+ || paidMessagesCount) && incoming;
const auto parsedGift = stargift
? FromTL(&peer->session(), *stargift)
: std::optional();
@@ -110,9 +116,9 @@ constexpr auto kTransactionsLimit = 100;
.bareGiftStickerId = giftStickerId,
.bareActorId = saveActorId ? barePeerId : uint64(0),
.uniqueGift = parsedGift ? parsedGift->unique : nullptr,
- .starrefAmount = starrefAmount,
- .starrefCommission = starrefCommission,
- .starrefRecipientId = starrefBarePeerId,
+ .starrefAmount = paidMessagesCount ? StarsAmount() : starrefAmount,
+ .starrefCommission = paidMessagesCount ? 0 : starrefCommission,
+ .starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
@@ -138,9 +144,15 @@ constexpr auto kTransactionsLimit = 100;
? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(),
.successLink = qs(tl.data().vtransaction_url().value_or_empty()),
+ .paidMessagesCount = paidMessagesCount,
+ .paidMessagesAmount = (paidMessagesCount
+ ? starrefAmount
+ : StarsAmount()),
+ .paidMessagesCommission = paidMessagesCount ? starrefCommission : 0,
.starsConverted = int(nonUniqueGift
? nonUniqueGift->vconvert_stars().v
: 0),
+ .premiumMonthsForStars = premiumMonthsForStars,
.floodSkip = int(tl.data().vfloodskip_number().value_or(0)),
.converted = stargift && incoming,
.stargift = stargift.has_value(),
diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp
index 9e2ab1a38..90c56ae4e 100644
--- a/Telegram/SourceFiles/api/api_global_privacy.cpp
+++ b/Telegram/SourceFiles/api/api_global_privacy.cpp
@@ -114,7 +114,8 @@ void GlobalPrivacy::updateHideReadTime(bool hide) {
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hide,
- newRequirePremiumCurrent());
+ newRequirePremiumCurrent(),
+ newChargeStarsCurrent());
}
bool GlobalPrivacy::hideReadTimeCurrent() const {
@@ -125,14 +126,6 @@ rpl::producer GlobalPrivacy::hideReadTime() const {
return _hideReadTime.value();
}
-void GlobalPrivacy::updateNewRequirePremium(bool value) {
- update(
- archiveAndMuteCurrent(),
- unarchiveOnNewMessageCurrent(),
- hideReadTimeCurrent(),
- value);
-}
-
bool GlobalPrivacy::newRequirePremiumCurrent() const {
return _newRequirePremium.current();
}
@@ -141,6 +134,25 @@ rpl::producer GlobalPrivacy::newRequirePremium() const {
return _newRequirePremium.value();
}
+int GlobalPrivacy::newChargeStarsCurrent() const {
+ return _newChargeStars.current();
+}
+
+rpl::producer GlobalPrivacy::newChargeStars() const {
+ return _newChargeStars.value();
+}
+
+void GlobalPrivacy::updateMessagesPrivacy(
+ bool requirePremium,
+ int chargeStars) {
+ update(
+ archiveAndMuteCurrent(),
+ unarchiveOnNewMessageCurrent(),
+ hideReadTimeCurrent(),
+ requirePremium,
+ chargeStars);
+}
+
void GlobalPrivacy::loadPaidReactionShownPeer() {
if (_paidReactionShownPeerLoaded) {
return;
@@ -169,7 +181,8 @@ void GlobalPrivacy::updateArchiveAndMute(bool value) {
value,
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
- newRequirePremiumCurrent());
+ newRequirePremiumCurrent(),
+ newChargeStarsCurrent());
}
void GlobalPrivacy::updateUnarchiveOnNewMessage(
@@ -178,14 +191,16 @@ void GlobalPrivacy::updateUnarchiveOnNewMessage(
archiveAndMuteCurrent(),
value,
hideReadTimeCurrent(),
- newRequirePremiumCurrent());
+ newRequirePremiumCurrent(),
+ newChargeStarsCurrent());
}
void GlobalPrivacy::update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
- bool newRequirePremium) {
+ bool newRequirePremium,
+ int newChargeStars) {
using Flag = MTPDglobalPrivacySettings::Flag;
_api.request(_requestId).cancel();
@@ -204,35 +219,44 @@ void GlobalPrivacy::update(
| (hideReadTime ? Flag::f_hide_read_marks : Flag())
| ((newRequirePremium && newRequirePremiumAllowed)
? Flag::f_new_noncontact_peers_require_premium
- : Flag());
+ : Flag())
+ | Flag::f_noncontact_peers_paid_stars;
_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
- MTP_globalPrivacySettings(MTP_flags(flags))
+ MTP_globalPrivacySettings(
+ MTP_flags(flags),
+ MTP_long(newChargeStars))
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
}).fail([=](const MTP::Error &error) {
_requestId = 0;
if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
- update(archiveAndMute, unarchiveOnNewMessage, hideReadTime, {});
+ update(
+ archiveAndMute,
+ unarchiveOnNewMessage,
+ hideReadTime,
+ false,
+ 0);
}
}).send();
_archiveAndMute = archiveAndMute;
_unarchiveOnNewMessage = unarchiveOnNewMessage;
_hideReadTime = hideReadTime;
_newRequirePremium = newRequirePremium;
+ _newChargeStars = newChargeStars;
}
-void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) {
- data.match([&](const MTPDglobalPrivacySettings &data) {
- _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
- _unarchiveOnNewMessage = data.is_keep_archived_unmuted()
- ? UnarchiveOnNewMessage::None
- : data.is_keep_archived_folders()
- ? UnarchiveOnNewMessage::NotInFoldersUnmuted
- : UnarchiveOnNewMessage::AnyUnmuted;
- _hideReadTime = data.is_hide_read_marks();
- _newRequirePremium = data.is_new_noncontact_peers_require_premium();
- });
+void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
+ const auto &data = settings.data();
+ _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
+ _unarchiveOnNewMessage = data.is_keep_archived_unmuted()
+ ? UnarchiveOnNewMessage::None
+ : data.is_keep_archived_folders()
+ ? UnarchiveOnNewMessage::NotInFoldersUnmuted
+ : UnarchiveOnNewMessage::AnyUnmuted;
+ _hideReadTime = data.is_hide_read_marks();
+ _newRequirePremium = data.is_new_noncontact_peers_require_premium();
+ _newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty();
}
} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h
index 6c5848961..6b0fc064b 100644
--- a/Telegram/SourceFiles/api/api_global_privacy.h
+++ b/Telegram/SourceFiles/api/api_global_privacy.h
@@ -49,23 +49,28 @@ public:
[[nodiscard]] bool hideReadTimeCurrent() const;
[[nodiscard]] rpl::producer hideReadTime() const;
- void updateNewRequirePremium(bool value);
[[nodiscard]] bool newRequirePremiumCurrent() const;
[[nodiscard]] rpl::producer newRequirePremium() const;
+ [[nodiscard]] int newChargeStarsCurrent() const;
+ [[nodiscard]] rpl::producer newChargeStars() const;
+
+ void updateMessagesPrivacy(bool requirePremium, int chargeStars);
+
void loadPaidReactionShownPeer();
void updatePaidReactionShownPeer(PeerId shownPeer);
[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
[[nodiscard]] rpl::producer paidReactionShownPeer() const;
private:
- void apply(const MTPGlobalPrivacySettings &data);
+ void apply(const MTPGlobalPrivacySettings &settings);
void update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
- bool newRequirePremium);
+ bool newRequirePremium,
+ int newChargeStars);
const not_null _session;
MTP::Sender _api;
@@ -76,6 +81,7 @@ private:
rpl::variable _showArchiveAndMute = false;
rpl::variable _hideReadTime = false;
rpl::variable _newRequirePremium = false;
+ rpl::variable _newChargeStars = 0;
rpl::variable _paidReactionShownPeer = false;
std::vector> _callbacks;
bool _paidReactionShownPeerLoaded = false;
diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp
index a56f25d0e..8e41f95cd 100644
--- a/Telegram/SourceFiles/api/api_polls.cpp
+++ b/Telegram/SourceFiles/api/api_polls.cpp
@@ -42,7 +42,7 @@ Polls::Polls(not_null api)
void Polls::create(
const PollData &data,
- const SendAction &action,
+ SendAction action,
Fn done,
Fn fail) {
_session->api().sendAction(action);
@@ -64,6 +64,9 @@ void Polls::create(
history->startSavingCloudDraft(topicRootId);
}
const auto silentPost = ShouldSendSilent(peer, action.options);
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
@@ -76,6 +79,10 @@ void Polls::create(
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
+ }
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
@@ -98,7 +105,8 @@ void Polls::create(
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
- MTP_long(action.options.effectId)
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h
index 2ff08a1ac..f77e34d67 100644
--- a/Telegram/SourceFiles/api/api_polls.h
+++ b/Telegram/SourceFiles/api/api_polls.h
@@ -27,7 +27,7 @@ public:
void create(
const PollData &data,
- const SendAction &action,
+ SendAction action,
Fn done,
Fn fail);
void sendVotes(
diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp
index 3bd36b40c..d7dbcd37f 100644
--- a/Telegram/SourceFiles/api/api_premium.cpp
+++ b/Telegram/SourceFiles/api/api_premium.cpp
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/random.h"
+#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_peer_values.h"
@@ -377,15 +378,15 @@ const Data::PremiumSubscriptionOptions &Premium::subscriptionOptions() const {
return _subscriptionOptions;
}
-rpl::producer<> Premium::somePremiumRequiredResolved() const {
- return _somePremiumRequiredResolved.events();
+rpl::producer<> Premium::someMessageMoneyRestrictionsResolved() const {
+ return _someMessageMoneyRestrictionsResolved.events();
}
-void Premium::resolvePremiumRequired(not_null user) {
- _resolvePremiumRequiredUsers.emplace(user);
- if (!_premiumRequiredRequestScheduled
- && _resolvePremiumRequestedUsers.empty()) {
- _premiumRequiredRequestScheduled = true;
+void Premium::resolveMessageMoneyRestrictions(not_null user) {
+ _resolveMessageMoneyRequiredUsers.emplace(user);
+ if (!_messageMoneyRequestScheduled
+ && _resolveMessageMoneyRequestedUsers.empty()) {
+ _messageMoneyRequestScheduled = true;
crl::on_main(_session, [=] {
requestPremiumRequiredSlice();
});
@@ -393,50 +394,65 @@ void Premium::resolvePremiumRequired(not_null user) {
}
void Premium::requestPremiumRequiredSlice() {
- _premiumRequiredRequestScheduled = false;
- if (!_resolvePremiumRequestedUsers.empty()
- || _resolvePremiumRequiredUsers.empty()) {
+ _messageMoneyRequestScheduled = false;
+ if (!_resolveMessageMoneyRequestedUsers.empty()
+ || _resolveMessageMoneyRequiredUsers.empty()) {
return;
}
constexpr auto kPerRequest = 100;
- auto users = MTP_vector_from_range(_resolvePremiumRequiredUsers
+ auto users = MTP_vector_from_range(_resolveMessageMoneyRequiredUsers
| ranges::views::transform(&UserData::inputUser));
if (users.v.size() > kPerRequest) {
auto shortened = users.v;
shortened.resize(kPerRequest);
users = MTP_vector(std::move(shortened));
- const auto from = begin(_resolvePremiumRequiredUsers);
- _resolvePremiumRequestedUsers = { from, from + kPerRequest };
- _resolvePremiumRequiredUsers.erase(from, from + kPerRequest);
+ const auto from = begin(_resolveMessageMoneyRequiredUsers);
+ _resolveMessageMoneyRequestedUsers = { from, from + kPerRequest };
+ _resolveMessageMoneyRequiredUsers.erase(from, from + kPerRequest);
} else {
- _resolvePremiumRequestedUsers
- = base::take(_resolvePremiumRequiredUsers);
+ _resolveMessageMoneyRequestedUsers
+ = base::take(_resolveMessageMoneyRequiredUsers);
}
- const auto finish = [=](const QVector &list) {
- constexpr auto me = UserDataFlag::MeRequiresPremiumToWrite;
- constexpr auto known = UserDataFlag::RequirePremiumToWriteKnown;
- constexpr auto mask = me | known;
+ const auto finish = [=](const QVector &list) {
auto index = 0;
- for (const auto &user : base::take(_resolvePremiumRequestedUsers)) {
- const auto require = (index < list.size())
- && mtpIsTrue(list[index++]);
- user->setFlags((user->flags() & ~mask)
- | known
- | (require ? me : UserDataFlag()));
+ for (const auto &user : base::take(_resolveMessageMoneyRequestedUsers)) {
+ const auto set = [&](bool requirePremium, int stars) {
+ using Flag = UserDataFlag;
+ constexpr auto me = Flag::RequiresPremiumToWrite;
+ constexpr auto known = Flag::MessageMoneyRestrictionsKnown;
+ constexpr auto hasPrem = Flag::HasRequirePremiumToWrite;
+ constexpr auto hasStars = Flag::HasStarsPerMessage;
+ user->setStarsPerMessage(stars);
+ user->setFlags((user->flags() & ~(me | hasPrem | hasStars))
+ | known
+ | (requirePremium ? (me | hasPrem) : Flag())
+ | (stars ? hasStars : Flag()));
+ };
+ if (index >= list.size()) {
+ set(false, 0);
+ continue;
+ }
+ list[index++].match([&](const MTPDrequirementToContactEmpty &) {
+ set(false, 0);
+ }, [&](const MTPDrequirementToContactPremium &) {
+ set(true, 0);
+ }, [&](const MTPDrequirementToContactPaidMessages &data) {
+ set(false, data.vstars_amount().v);
+ });
}
- if (!_premiumRequiredRequestScheduled
- && !_resolvePremiumRequiredUsers.empty()) {
- _premiumRequiredRequestScheduled = true;
+ if (!_messageMoneyRequestScheduled
+ && !_resolveMessageMoneyRequiredUsers.empty()) {
+ _messageMoneyRequestScheduled = true;
crl::on_main(_session, [=] {
requestPremiumRequiredSlice();
});
}
- _somePremiumRequiredResolved.fire({});
+ _someMessageMoneyRestrictionsResolved.fire({});
};
_session->api().request(
- MTPusers_GetIsPremiumRequiredToContact(std::move(users))
- ).done([=](const MTPVector &result) {
+ MTPusers_GetRequirementsToContact(std::move(users))
+ ).done([=](const MTPVector &result) {
finish(result.v);
}).fail([=] {
finish({});
@@ -463,10 +479,14 @@ rpl::producer PremiumGiftCodeOptions::request() {
for (const auto &tlOption : result.v) {
const auto &data = tlOption.data();
tlMapOptions[data.vusers().v].push_back(tlOption);
+ if (qs(data.vcurrency()) == Ui::kCreditsCurrency) {
+ continue;
+ }
const auto token = Token{ data.vusers().v, data.vmonths().v };
_stores[token] = Store{
.amount = data.vamount().v,
+ .currency = qs(data.vcurrency()),
.product = qs(data.vstore_product().value_or_empty()),
.quantity = data.vstore_quantity().value_or_empty(),
};
@@ -475,14 +495,14 @@ rpl::producer PremiumGiftCodeOptions::request() {
}
}
for (const auto &[amount, tlOptions] : tlMapOptions) {
- if (amount == 1 && _optionsForOnePerson.currency.isEmpty()) {
- _optionsForOnePerson.currency = qs(
- tlOptions.front().data().vcurrency());
+ if (amount == 1 && _optionsForOnePerson.currencies.empty()) {
for (const auto &option : tlOptions) {
_optionsForOnePerson.months.push_back(
option.data().vmonths().v);
_optionsForOnePerson.totalCosts.push_back(
option.data().vamount().v);
+ _optionsForOnePerson.currencies.push_back(
+ qs(option.data().vcurrency()));
}
}
_subscriptionOptions[amount] = GiftCodesFromTL(tlOptions);
@@ -509,7 +529,7 @@ rpl::producer PremiumGiftCodeOptions::applyPrepaid(
_api.request(MTPpayments_LaunchPrepaidGiveaway(
_peer->input,
MTP_long(prepaidId),
- invoice.creditsAmount
+ invoice.giveawayCredits
? Payments::InvoiceCreditsGiveawayToTL(invoice)
: Payments::InvoicePremiumGiftCodeGiveawayToTL(invoice)
)).done([=](const MTPUpdates &result) {
@@ -540,7 +560,7 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
const auto token = Token{ users, months };
const auto &store = _stores[token];
return Payments::InvoicePremiumGiftCode{
- .currency = _optionsForOnePerson.currency,
+ .currency = store.currency,
.storeProduct = store.product,
.randomId = randomId,
.amount = store.amount,
@@ -553,14 +573,15 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
std::vector PremiumGiftCodeOptions::optionsForPeer() const {
auto result = std::vector();
- if (!_optionsForOnePerson.currency.isEmpty()) {
+ if (!_optionsForOnePerson.currencies.empty()) {
const auto count = int(_optionsForOnePerson.months.size());
result.reserve(count);
for (auto i = 0; i != count; ++i) {
Assert(i < _optionsForOnePerson.totalCosts.size());
+ Assert(i < _optionsForOnePerson.currencies.size());
result.push_back({
.cost = _optionsForOnePerson.totalCosts[i],
- .currency = _optionsForOnePerson.currency,
+ .currency = _optionsForOnePerson.currencies[i],
.months = _optionsForOnePerson.months[i],
});
}
@@ -581,7 +602,7 @@ Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) {
MTP_int(_optionsForOnePerson.months[i]),
MTPstring(),
MTPint(),
- MTP_string(_optionsForOnePerson.currency),
+ MTP_string(_optionsForOnePerson.currencies[i]),
MTP_long(_optionsForOnePerson.totalCosts[i] * amount)));
}
_subscriptionOptions[amount] = GiftCodesFromTL(tlOptions);
@@ -694,28 +715,38 @@ rpl::producer SponsoredToggle::setToggled(bool v) {
};
}
-RequirePremiumState ResolveRequiresPremiumToWrite(
+MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null peer,
History *maybeHistory) {
- const auto user = peer->asUser();
- if (!user
- || !user->someRequirePremiumToWrite()
- || user->session().premium()) {
- return RequirePremiumState::No;
- } else if (user->requirePremiumToWriteKnown()) {
- return user->meRequiresPremiumToWrite()
- ? RequirePremiumState::Yes
- : RequirePremiumState::No;
- } else if (user->flags() & UserDataFlag::MutualContact) {
- return RequirePremiumState::No;
- } else if (!maybeHistory) {
- return RequirePremiumState::Unknown;
+ if (const auto channel = peer->asChannel()) {
+ return {
+ .starsPerMessage = channel->starsPerMessageChecked(),
+ .known = true,
+ };
+ }
+ const auto user = peer->asUser();
+ if (!user) {
+ return { .known = true };
+ } else if (user->messageMoneyRestrictionsKnown()) {
+ return {
+ .starsPerMessage = user->starsPerMessageChecked(),
+ .premiumRequired = (user->requiresPremiumToWrite()
+ && !user->session().premium()),
+ .known = true,
+ };
+ } else if (user->hasStarsPerMessage()) {
+ return {};
+ } else if (!user->hasRequirePremiumToWrite()) {
+ return { .known = true };
+ } else if (user->flags() & UserDataFlag::MutualContact) {
+ return { .known = true };
+ } else if (!maybeHistory) {
+ return {};
}
-
const auto update = [&](bool require) {
using Flag = UserDataFlag;
- constexpr auto known = Flag::RequirePremiumToWriteKnown;
- constexpr auto me = Flag::MeRequiresPremiumToWrite;
+ constexpr auto known = Flag::MessageMoneyRestrictionsKnown;
+ constexpr auto me = Flag::RequiresPremiumToWrite;
user->setFlags((user->flags() & ~me)
| known
| (require ? me : Flag()));
@@ -727,16 +758,19 @@ RequirePremiumState ResolveRequiresPremiumToWrite(
const auto item = view->data();
if (!item->out() && !item->isService()) {
update(false);
- return RequirePremiumState::No;
+ return { .known = true };
}
}
}
if (user->isContact() // Here we know, that we're not in his contacts.
&& maybeHistory->loadedAtTop() // And no incoming messages.
&& maybeHistory->loadedAtBottom()) {
- update(true);
+ return {
+ .premiumRequired = !user->session().premium(),
+ .known = true,
+ };
}
- return RequirePremiumState::Unknown;
+ return {};
}
rpl::producer RandomHelloStickerValue(
@@ -870,6 +904,7 @@ std::optional FromTL(
.date = data.vdate().v,
.upgradable = data.is_can_upgrade(),
.anonymous = data.is_name_hidden(),
+ .pinned = data.is_pinned_to_top(),
.hidden = data.is_unsaved(),
.mine = to->isSelf(),
};
diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h
index a7757a490..2b692a484 100644
--- a/Telegram/SourceFiles/api/api_premium.h
+++ b/Telegram/SourceFiles/api/api_premium.h
@@ -116,8 +116,9 @@ public:
[[nodiscard]] auto subscriptionOptions() const
-> const Data::PremiumSubscriptionOptions &;
- [[nodiscard]] rpl::producer<> somePremiumRequiredResolved() const;
- void resolvePremiumRequired(not_null user);
+ [[nodiscard]] auto someMessageMoneyRestrictionsResolved() const
+ -> rpl::producer<>;
+ void resolveMessageMoneyRestrictions(not_null user);
private:
void reloadPromo();
@@ -166,10 +167,10 @@ private:
Data::PremiumSubscriptionOptions _subscriptionOptions;
- rpl::event_stream<> _somePremiumRequiredResolved;
- base::flat_set> _resolvePremiumRequiredUsers;
- base::flat_set> _resolvePremiumRequestedUsers;
- bool _premiumRequiredRequestScheduled = false;
+ rpl::event_stream<> _someMessageMoneyRestrictionsResolved;
+ base::flat_set> _resolveMessageMoneyRequiredUsers;
+ base::flat_set> _resolveMessageMoneyRequestedUsers;
+ bool _messageMoneyRequestScheduled = false;
};
@@ -208,6 +209,7 @@ private:
};
struct Store final {
uint64 amount = 0;
+ QString currency;
QString product;
int quantity = 0;
};
@@ -218,7 +220,7 @@ private:
struct {
std::vector months;
std::vector totalCosts;
- QString currency;
+ std::vector currencies;
} _optionsForOnePerson;
std::vector _availablePresets;
@@ -244,12 +246,20 @@ private:
};
-enum class RequirePremiumState {
- Unknown,
- Yes,
- No,
+struct MessageMoneyRestriction {
+ int starsPerMessage = 0;
+ bool premiumRequired = false;
+ bool known = false;
+
+ explicit operator bool() const {
+ return starsPerMessage != 0 || premiumRequired;
+ }
+
+ friend inline bool operator==(
+ const MessageMoneyRestriction &,
+ const MessageMoneyRestriction &) = default;
};
-[[nodiscard]] RequirePremiumState ResolveRequiresPremiumToWrite(
+[[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null peer,
History *maybeHistory);
diff --git a/Telegram/SourceFiles/api/api_premium_option.cpp b/Telegram/SourceFiles/api/api_premium_option.cpp
index bd3056a75..d3c67e23b 100644
--- a/Telegram/SourceFiles/api/api_premium_option.cpp
+++ b/Telegram/SourceFiles/api/api_premium_option.cpp
@@ -26,7 +26,7 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption(
}();
return {
.duration = Ui::FormatTTL(months * 86400 * 31),
- .discount = discount
+ .discount = (discount > 0)
? QString::fromUtf8("\xe2\x88\x92%1%").arg(discount)
: QString(),
.costPerMonth = Ui::FillAmountAndCurrency(
diff --git a/Telegram/SourceFiles/api/api_premium_option.h b/Telegram/SourceFiles/api/api_premium_option.h
index afe66ac4a..2648a7f9c 100644
--- a/Telegram/SourceFiles/api/api_premium_option.h
+++ b/Telegram/SourceFiles/api/api_premium_option.h
@@ -24,15 +24,26 @@ template
if (tlOpts.isEmpty()) {
return {};
}
+ auto monthlyAmountPerCurrency = base::flat_map();
auto result = Data::PremiumSubscriptionOptions();
- const auto monthlyAmount = [&] {
+ const auto monthlyAmount = [&](const QString ¤cy) -> int {
+ const auto it = monthlyAmountPerCurrency.find(currency);
+ if (it != end(monthlyAmountPerCurrency)) {
+ return it->second;
+ }
const auto &min = ranges::min_element(
tlOpts,
ranges::less(),
- [](const Option &o) { return o.data().vamount().v; }
+ [&](const Option &o) {
+ return currency == qs(o.data().vcurrency())
+ ? o.data().vamount().v
+ : std::numeric_limits::max();
+ }
)->data();
- return min.vamount().v / float64(min.vmonths().v);
- }();
+ const auto monthly = min.vamount().v / float64(min.vmonths().v);
+ monthlyAmountPerCurrency.emplace(currency, monthly);
+ return monthly;
+ };
result.reserve(tlOpts.size());
for (const auto &tlOption : tlOpts) {
const auto &option = tlOption.data();
@@ -45,7 +56,7 @@ template
const auto currency = qs(option.vcurrency());
result.push_back(CreateSubscriptionOption(
months,
- monthlyAmount,
+ monthlyAmount(currency),
amount,
currency,
botUrl));
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 80b6f1007..814b0a983 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -95,7 +95,9 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
const auto messagePostAuthor = peer->isBroadcast()
? session->user()->name()
: QString();
-
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
@@ -111,6 +113,10 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
+ }
auto &histories = history->owner().histories();
histories.sendPreparedMessage(
@@ -129,7 +135,8 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
- MTP_long(action.options.effectId)
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId);
@@ -160,7 +167,7 @@ void SendExistingMedia(
? (*localMessageId)
: session->data().nextLocalMessageId());
const auto randomId = base::RandomValue();
- const auto &action = message.action;
+ auto &action = message.action;
auto flags = NewMessageFlags(peer);
auto sendFlags = MTPmessages_SendMedia::Flags(0);
@@ -190,7 +197,9 @@ void SendExistingMedia(
sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
}
const auto captionText = caption.text;
-
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
@@ -206,6 +215,10 @@ void SendExistingMedia(
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
+ }
session->data().registerMessageRandomId(randomId, newId);
@@ -216,6 +229,7 @@ void SendExistingMedia(
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
+ .starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, media, caption);
@@ -240,7 +254,8 @@ void SendExistingMedia(
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
- MTP_long(action.options.effectId)
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (error.code() == 400
@@ -341,7 +356,7 @@ bool SendDice(MessageToSend &message) {
message.action.generateLocal = true;
- const auto &action = message.action;
+ auto &action = message.action;
api->sendAction(action);
const auto newId = FullMsgId(
@@ -380,6 +395,13 @@ bool SendDice(MessageToSend &message) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
+ }
session->data().registerMessageRandomId(randomId, newId);
@@ -390,6 +412,7 @@ bool SendDice(MessageToSend &message) {
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
+ .starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaDice(
@@ -411,7 +434,8 @@ bool SendDice(MessageToSend &message) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
- MTP_long(action.options.effectId)
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId, newId);
@@ -610,6 +634,9 @@ void SendConfirmedFile(
.replyTo = file->to.replyTo,
.date = NewMessageDate(file->to.options),
.shortcutId = file->to.options.shortcutId,
+ .starsPaid = std::min(
+ history->peer->starsPerMessageChecked(),
+ file->to.options.starsApproved),
.postAuthor = NewMessagePostAuthor(action),
.groupedId = groupId,
.effectId = file->to.options.effectId,
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index e34ef373d..9c959bbf9 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -1227,7 +1227,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck(),
- MTPint()), // report_delivery_until_date
+ MTPint(), // report_delivery_until_date
+ MTPlong()), // paid_message_stars
MessageFlags(),
NewMessageType::Unread);
} break;
@@ -1265,7 +1266,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck(),
- MTPint()), // report_delivery_until_date
+ MTPint(), // report_delivery_until_date
+ MTPlong()), // paid_message_stars
MessageFlags(),
NewMessageType::Unread);
} break;
diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp
index 78f863bed..e07858dda 100644
--- a/Telegram/SourceFiles/api/api_user_privacy.cpp
+++ b/Telegram/SourceFiles/api/api_user_privacy.cpp
@@ -210,6 +210,7 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) {
case Key::About: return MTP_inputPrivacyKeyAbout();
case Key::Birthday: return MTP_inputPrivacyKeyBirthday();
case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave();
+ case Key::NoPaidMessages: return MTP_inputPrivacyKeyNoPaidMessages();
}
Unexpected("Key in Api::UserPrivacy::KetToTL.");
}
@@ -241,6 +242,8 @@ std::optional TLToKey(mtpTypeId type) {
case mtpc_inputPrivacyKeyBirthday: return Key::Birthday;
case mtpc_privacyKeyStarGiftsAutoSave:
case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave;
+ case mtpc_privacyKeyNoPaidMessages:
+ case mtpc_inputPrivacyKeyNoPaidMessages: return Key::NoPaidMessages;
}
return std::nullopt;
}
diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h
index a1f66189f..676e9be11 100644
--- a/Telegram/SourceFiles/api/api_user_privacy.h
+++ b/Telegram/SourceFiles/api/api_user_privacy.h
@@ -32,6 +32,7 @@ public:
About,
Birthday,
GiftsAutoSave,
+ NoPaidMessages,
};
enum class Option {
Everyone,
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 86a10680a..7d2c1c31f 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -546,6 +546,7 @@ void ApiWrap::sendMessageFail(
uint64 randomId,
FullMsgId itemId) {
const auto show = ShowForPeer(peer);
+ const auto paidStarsPrefix = u"ALLOW_PAYMENT_REQUIRED_"_q;
if (show && error == u"PEER_FLOOD"_q) {
show->showBox(
Ui::MakeInformBox(
@@ -600,6 +601,19 @@ void ApiWrap::sendMessageFail(
if (show) {
show->showToast(tr::lng_error_schedule_limit(tr::now));
}
+ } else if (error.startsWith(paidStarsPrefix)) {
+ if (show) {
+ show->showToast(
+ u"Payment requirements changed. Please, try again."_q);
+ }
+ if (const auto stars = error.mid(paidStarsPrefix.size()).toInt()) {
+ if (const auto user = peer->asUser()) {
+ user->setStarsPerMessage(stars);
+ } else if (const auto channel = peer->asChannel()) {
+ channel->setStarsPerMessage(stars);
+ }
+ }
+ peer->updateFull();
}
if (const auto item = _session->data().message(itemId)) {
Assert(randomId != 0);
@@ -3342,7 +3356,7 @@ void ApiWrap::finishForwarding(const SendAction &action) {
void ApiWrap::forwardMessages(
Data::ResolvedForwardDraft &&draft,
- const SendAction &action,
+ SendAction action,
FnMut &&successCallback) {
Expects(!draft.items.empty());
@@ -3417,9 +3431,17 @@ void ApiWrap::forwardMessages(
const auto requestType = Data::Histories::RequestType::Send;
const auto idsCopy = localIds;
const auto scheduled = action.options.scheduled;
+ const auto starsPaid = std::min(
+ action.options.starsApproved,
+ int(ids.size() * peer->starsPerMessageChecked()));
+ auto oneFlags = sendFlags;
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ oneFlags |= SendFlag::f_allow_paid_stars;
+ }
histories.sendRequest(history, requestType, [=](Fn finish) {
history->sendRequestId = request(MTPmessages_ForwardMessages(
- MTP_flags(sendFlags),
+ MTP_flags(oneFlags),
forwardFrom->input,
MTP_vector(ids),
MTP_vector(randomIds),
@@ -3428,7 +3450,8 @@ void ApiWrap::forwardMessages(
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
- MTPint() // video_timestamp
+ MTPint(), // video_timestamp
+ MTP_long(starsPaid)
)).done([=](const MTPUpdates &result) {
if (!scheduled) {
this->updates().checkForSentToScheduled(result);
@@ -3480,6 +3503,7 @@ void ApiWrap::forwardMessages(
.replyTo = { .topicRootId = topMsgId },
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
+ .starsPaid = action.options.starsApproved,
.postAuthor = NewMessagePostAuthor(action),
// forwarded messages don't have effects
@@ -3573,6 +3597,7 @@ void ApiWrap::sendSharedContact(
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
+ .starsPaid = action.options.starsApproved,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaContact(
@@ -3944,6 +3969,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendFlags |= MTPmessages_SendMessage::Flag::f_effect;
mediaFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= MTPmessages_SendMessage::Flag::f_allow_paid_stars;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
+ }
lastMessage = history->addNewLocalMessage({
.id = newId.msg,
.flags = flags,
@@ -3951,6 +3984,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
+ .starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, sending, media);
@@ -4001,7 +4035,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
- MTP_long(action.options.effectId)
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid)
), done, fail);
} else {
histories.sendPreparedMessage(
@@ -4019,7 +4054,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
- MTP_long(action.options.effectId)
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid)
), done, fail);
}
isFirst = false;
@@ -4084,7 +4120,7 @@ void ApiWrap::sendBotStart(
void ApiWrap::sendInlineResult(
not_null bot,
not_null data,
- const SendAction &action,
+ SendAction action,
std::optional localMessageId,
Fn done) {
sendAction(action);
@@ -4124,6 +4160,13 @@ void ApiWrap::sendInlineResult(
if (action.options.hideViaBot) {
sendFlags |= SendFlag::f_hide_via;
}
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= SendFlag::f_allow_paid_stars;
+ }
const auto sendAs = action.options.sendAs;
if (sendAs) {
@@ -4138,6 +4181,7 @@ void ApiWrap::sendInlineResult(
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
+ .starsPaid = starsPaid,
.viaBotId = ((bot && !action.options.hideViaBot)
? peerToUser(bot->id)
: UserId()),
@@ -4161,7 +4205,8 @@ void ApiWrap::sendInlineResult(
MTP_string(data->getId()),
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
- Data::ShortcutIdToMTP(_session, action.options.shortcutId)
+ Data::ShortcutIdToMTP(_session, action.options.shortcutId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
history->finishSavingCloudDraft(
topicRootId,
@@ -4298,6 +4343,7 @@ void ApiWrap::sendMediaWithRandomId(
const auto history = item->history();
const auto replyTo = item->replyTo();
+ const auto peer = history->peer;
auto caption = item->originalText();
TextUtilities::Trim(caption);
@@ -4307,6 +4353,12 @@ void ApiWrap::sendMediaWithRandomId(
Api::ConvertOption::SkipLocal);
const auto updateRecentStickers = Api::HasAttachedStickers(media);
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ options.starsApproved);
+ if (starsPaid) {
+ options.starsApproved -= starsPaid;
+ }
using Flag = MTPmessages_SendMedia::Flag;
const auto flags = Flag(0)
@@ -4319,10 +4371,10 @@ void ApiWrap::sendMediaWithRandomId(
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
- | (options.invertCaption ? Flag::f_invert_media : Flag(0));
+ | (options.invertCaption ? Flag::f_invert_media : Flag(0))
+ | (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
auto &histories = history->owner().histories();
- const auto peer = history->peer;
const auto itemId = item->fullId();
histories.sendPreparedMessage(
history,
@@ -4346,7 +4398,8 @@ void ApiWrap::sendMediaWithRandomId(
MTP_int(options.scheduled),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
- MTP_long(options.effectId)
+ MTP_long(options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (done) done(true);
if (updateRecentStickers) {
@@ -4367,7 +4420,7 @@ void ApiWrap::sendMultiPaidMedia(
Expects(album->options.price > 0);
const auto groupId = album->groupId;
- const auto &options = album->options;
+ auto &options = album->options;
const auto randomId = album->items.front().randomId;
auto medias = album->items | ranges::view::transform([](
const SendingAlbum::Item &part) {
@@ -4377,6 +4430,7 @@ void ApiWrap::sendMultiPaidMedia(
const auto history = item->history();
const auto replyTo = item->replyTo();
+ const auto peer = history->peer;
auto caption = item->originalText();
TextUtilities::Trim(caption);
@@ -4384,6 +4438,12 @@ void ApiWrap::sendMultiPaidMedia(
_session,
caption.entities,
Api::ConvertOption::SkipLocal);
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ options.starsApproved);
+ if (starsPaid) {
+ options.starsApproved -= starsPaid;
+ }
using Flag = MTPmessages_SendMedia::Flag;
const auto flags = Flag(0)
@@ -4396,10 +4456,10 @@ void ApiWrap::sendMultiPaidMedia(
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
- | (options.invertCaption ? Flag::f_invert_media : Flag(0));
+ | (options.invertCaption ? Flag::f_invert_media : Flag(0))
+ | (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
auto &histories = history->owner().histories();
- const auto peer = history->peer;
const auto itemId = item->fullId();
album->sent = true;
histories.sendPreparedMessage(
@@ -4422,7 +4482,8 @@ void ApiWrap::sendMultiPaidMedia(
MTP_int(options.scheduled),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
- MTP_long(options.effectId)
+ MTP_long(options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (const auto album = _sendingAlbums.take(groupId)) {
const auto copy = (*album)->items;
@@ -4518,6 +4579,12 @@ void ApiWrap::sendAlbumIfReady(not_null album) {
const auto history = sample->history();
const auto replyTo = sample->replyTo();
const auto sendAs = album->options.sendAs;
+ const auto starsPaid = std::min(
+ history->peer->starsPerMessageChecked() * int(medias.size()),
+ album->options.starsApproved);
+ if (starsPaid) {
+ album->options.starsApproved -= starsPaid;
+ }
using Flag = MTPmessages_SendMultiMedia::Flag;
const auto flags = Flag(0)
| (replyTo ? Flag::f_reply_to : Flag(0))
@@ -4530,7 +4597,8 @@ void ApiWrap::sendAlbumIfReady(not_null album) {
? Flag::f_quick_reply_shortcut
: Flag(0))
| (album->options.effectId ? Flag::f_effect : Flag(0))
- | (album->options.invertCaption ? Flag::f_invert_media : Flag(0));
+ | (album->options.invertCaption ? Flag::f_invert_media : Flag(0))
+ | (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
auto &histories = history->owner().histories();
const auto peer = history->peer;
album->sent = true;
@@ -4546,7 +4614,8 @@ void ApiWrap::sendAlbumIfReady(not_null album) {
MTP_int(album->options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, album->options.shortcutId),
- MTP_long(album->options.effectId)
+ MTP_long(album->options.effectId),
+ MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
_sendingAlbums.remove(groupId);
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index d6e2bbc67..0817560b7 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -306,7 +306,7 @@ public:
void finishForwarding(const SendAction &action);
void forwardMessages(
Data::ResolvedForwardDraft &&draft,
- const SendAction &action,
+ SendAction action,
FnMut &&successCallback = nullptr);
void shareContact(
const QString &phone,
@@ -368,7 +368,7 @@ public:
void sendInlineResult(
not_null bot,
not_null data,
- const SendAction &action,
+ SendAction action,
std::optional localMessageId,
Fn done = nullptr);
void sendMessageFail(
diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp
index 828b2d095..0faa67772 100644
--- a/Telegram/SourceFiles/boxes/background_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp
@@ -1078,7 +1078,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector &bg) {
? tr::lng_background_other_group(tr::now)
: forChannel()
? tr::lng_background_other_channel(tr::now)
- : (_forPeer && !_fromMessageId)
+ : (_forPeer
+ && !_fromMessageId
+ && !_forPeer->starsPerMessageChecked())
? tr::lng_background_other_info(
tr::now,
lt_user,
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index be1a42ae2..43b3cab7d 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -1122,3 +1122,10 @@ profileQrBackgroundRadius: 12px;
profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px);
profileQrBackgroundPadding: margins(0px, 24px, 0px, 24px);
+
+foldersMenu: PopupMenu(popupMenuWithIcons) {
+ maxHeight: 320px;
+ menu: Menu(menuWithIcons) {
+ itemPadding: margins(54px, 8px, 44px, 8px);
+ }
+}
diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp
index 685ef547f..93164126b 100644
--- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp
@@ -172,13 +172,6 @@ void ChangeFilterById(
const auto account = not_null(&history->session().account());
if (const auto controller = Core::App().windowFor(account)) {
const auto isStatic = name.isStatic;
- const auto textContext = [=](not_null widget) {
- return Core::MarkedTextContext{
- .session = &history->session(),
- .customEmojiRepaint = [=] { widget->update(); },
- .customEmojiLoopLimit = isStatic ? -1 : 0,
- };
- };
controller->showToast({
.text = (add
? tr::lng_filters_toast_add
@@ -189,7 +182,10 @@ void ChangeFilterById(
lt_folder,
Ui::Text::Wrapped(name.text, EntityType::Bold),
Ui::Text::WithEntities),
- .textContext = textContext,
+ .textContext = Core::TextContext({
+ .session = &history->session(),
+ .customEmojiLoopLimit = isStatic ? -1 : 0,
+ }),
});
}
}).fail([=](const MTP::Error &error) {
@@ -290,19 +286,17 @@ void FillChooseFilterMenu(
const auto title = filter.title();
auto item = base::make_unique_q(
menu.get(),
- st::foldersMenu,
+ menu->st().menu,
Ui::Menu::CreateAction(
menu.get(),
Ui::Text::FixAmpersandInAction(title.text.text),
std::move(callback)),
contains ? &st::mediaPlayerMenuCheck : nullptr,
contains ? &st::mediaPlayerMenuCheck : nullptr);
- const auto context = Core::MarkedTextContext{
+ item->setMarkedText(title.text, QString(), Core::TextContext({
.session = &history->session(),
- .customEmojiRepaint = [raw = item.get()] { raw->update(); },
.customEmojiLoopLimit = title.isStatic ? -1 : 0,
- };
- item->setMarkedText(title.text, QString(), context);
+ }));
item->setIcon(Icon(showColors ? filter : filter.withColorIndex({})));
const auto action = menu->addAction(std::move(item));
diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp
index 223c5cab4..fab9731cc 100644
--- a/Telegram/SourceFiles/boxes/create_poll_box.cpp
+++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp
@@ -817,13 +817,15 @@ CreatePollBox::CreatePollBox(
not_null controller,
PollData::Flags chosen,
PollData::Flags disabled,
+ rpl::producer starsRequired,
Api::SendType sendType,
SendMenu::Details sendMenuDetails)
: _controller(controller)
, _chosen(chosen)
, _disabled(disabled)
, _sendType(sendType)
-, _sendMenuDetails([result = sendMenuDetails] { return result; }) {
+, _sendMenuDetails([result = sendMenuDetails] { return result; })
+, _starsRequired(std::move(starsRequired)) {
}
rpl::producer CreatePollBox::submitRequests() const {
@@ -1226,10 +1228,11 @@ object_ptr CreatePollBox::setupContent() {
_sendMenuDetails());
};
const auto submit = addButton(
- (isNormal
- ? tr::lng_polls_create_button()
- : tr::lng_schedule_button()),
+ tr::lng_polls_create_button(),
[=] { isNormal ? send({}) : schedule(); });
+ submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal
+ ? tr::lng_polls_create_button()
+ : tr::lng_schedule_button()));
const auto sendMenuDetails = [=] {
collectError();
return (*error) ? SendMenu::Details() : _sendMenuDetails();
diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h
index 91fc290ca..33fbdd3f1 100644
--- a/Telegram/SourceFiles/boxes/create_poll_box.h
+++ b/Telegram/SourceFiles/boxes/create_poll_box.h
@@ -42,6 +42,7 @@ public:
not_null controller,
PollData::Flags chosen,
PollData::Flags disabled,
+ rpl::producer starsRequired,
Api::SendType sendType,
SendMenu::Details sendMenuDetails);
@@ -76,6 +77,7 @@ private:
const PollData::Flags _disabled = PollData::Flags();
const Api::SendType _sendType = Api::SendType();
const Fn _sendMenuDetails;
+ rpl::variable _starsRequired;
base::unique_qptr _emojiPanel;
Fn _setInnerFocus;
Fn()> _dataIsValidValue;
diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp
index 94c4af054..5c7463261 100644
--- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp
@@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/checkbox.h"
+#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/shadow.h"
+#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/wrap/slide_wrap.h"
@@ -21,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "boxes/peer_list_controllers.h"
#include "settings/settings_premium.h"
+#include "settings/settings_privacy_controllers.h"
#include "settings/settings_privacy_security.h"
#include "calls/calls_instance.h"
#include "lang/lang_keys.h"
@@ -42,6 +45,8 @@ namespace {
constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;
constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;
+constexpr auto kStarsMin = 1;
+constexpr auto kDefaultChargeStars = 10;
using Exceptions = Api::UserPrivacy::Exceptions;
@@ -452,6 +457,143 @@ auto PrivacyExceptionsBoxController::createRow(not_null history)
return result;
}
+[[nodiscard]] object_ptr MakeChargeStarsSlider(
+ QWidget *parent,
+ not_null sliderStyle,
+ not_null labelStyle,
+ int valuesCount,
+ Fn valueByIndex,
+ int value,
+ int maxValue,
+ Fn valueProgress,
+ Fn valueFinished) {
+ auto result = object_ptr(parent);
+ const auto raw = result.data();
+
+ const auto labels = raw->add(object_ptr(raw));
+ const auto min = Ui::CreateChild(
+ raw,
+ QString::number(kStarsMin),
+ *labelStyle);
+ const auto max = Ui::CreateChild(
+ raw,
+ QString::number(maxValue),
+ *labelStyle);
+ const auto current = Ui::CreateChild(
+ raw,
+ QString::number(value),
+ *labelStyle);
+ min->setTextColorOverride(st::windowSubTextFg->c);
+ max->setTextColorOverride(st::windowSubTextFg->c);
+ const auto slider = raw->add(object_ptr(
+ raw,
+ *sliderStyle));
+ labels->resize(
+ labels->width(),
+ current->height() + st::defaultVerticalListSkip);
+ struct State {
+ int indexMin = 0;
+ int index = 0;
+ };
+ const auto state = raw->lifetime().make_state();
+ const auto updateByIndex = [=] {
+ const auto outer = labels->width();
+ const auto minWidth = min->width();
+ const auto maxWidth = max->width();
+ const auto currentWidth = current->width();
+ if (minWidth + maxWidth + currentWidth > outer) {
+ return;
+ }
+
+ min->moveToLeft(0, 0, outer);
+ max->moveToRight(0, 0, outer);
+ current->moveToLeft((outer - current->width()) / 2, 0, outer);
+ };
+ const auto updateByValue = [=](int value) {
+ current->setText(
+ tr::lng_action_gift_for_stars(tr::now, lt_count, value));
+
+ state->index = 0;
+ auto maxIndex = valuesCount - 1;
+ while (state->index < maxIndex) {
+ const auto mid = (state->index + maxIndex) / 2;
+ const auto midValue = valueByIndex(mid);
+ if (midValue == value) {
+ state->index = mid;
+ break;
+ } else if (midValue < value) {
+ state->index = mid + 1;
+ } else {
+ maxIndex = mid - 1;
+ }
+ }
+ updateByIndex();
+ };
+ const auto progress = [=](int value) {
+ updateByValue(value);
+ valueProgress(value);
+ };
+ const auto finished = [=](int value) {
+ updateByValue(value);
+ valueFinished(value);
+ };
+ style::PaletteChanged() | rpl::start_with_next([=] {
+ min->setTextColorOverride(st::windowSubTextFg->c);
+ max->setTextColorOverride(st::windowSubTextFg->c);
+ }, raw->lifetime());
+ updateByValue(value);
+ state->indexMin = 0;
+
+ slider->setPseudoDiscrete(
+ valuesCount,
+ valueByIndex,
+ value,
+ progress,
+ finished,
+ state->indexMin);
+ slider->resize(slider->width(), sliderStyle->seekSize.height());
+
+ raw->widthValue() | rpl::start_with_next([=](int width) {
+ labels->resizeToWidth(width);
+ updateByIndex();
+ }, slider->lifetime());
+
+ return result;
+}
+
+void EditNoPaidMessagesExceptions(
+ not_null window,
+ const Api::UserPrivacy::Rule &value) {
+ auto controller = std::make_unique(
+ &window->session(),
+ tr::lng_messages_privacy_remove_fee(),
+ value.always,
+ std::optional());
+ auto initBox = [=, controller = controller.get()](
+ not_null box) {
+ box->addButton(tr::lng_settings_save(), [=] {
+ auto copy = value;
+ auto &setTo = copy.always;
+ setTo.peers = box->collectSelectedRows();
+ setTo.premiums = false;
+ setTo.miniapps = false;
+ auto &removeFrom = copy.never;
+ for (const auto peer : setTo.peers) {
+ removeFrom.peers.erase(
+ ranges::remove(removeFrom.peers, peer),
+ end(removeFrom.peers));
+ }
+ window->session().api().userPrivacy().save(
+ Api::UserPrivacy::Key::NoPaidMessages,
+ copy);
+ box->closeBox();
+ });
+ box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+ };
+ window->show(
+ Box(std::move(controller), std::move(initBox)));
+}
+
} // namespace
bool EditPrivacyController::hasOption(Option option) const {
@@ -812,19 +954,27 @@ void EditMessagesPrivacyBox(
constexpr auto kOptionAll = 0;
constexpr auto kOptionPremium = 1;
+ constexpr auto kOptionCharge = 2;
+ const auto session = &controller->session();
const auto allowed = [=] {
- return controller->session().premium()
- || controller->session().appConfig().newRequirePremiumFree();
+ return session->premium()
+ || session->appConfig().newRequirePremiumFree();
};
- const auto privacy = &controller->session().api().globalPrivacy();
+ const auto privacy = &session->api().globalPrivacy();
const auto inner = box->verticalLayout();
inner->add(object_ptr(box));
Ui::AddSkip(inner, st::messagePrivacyTopSkip);
Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle());
const auto group = std::make_shared(
- privacy->newRequirePremiumCurrent() ? kOptionPremium : kOptionAll);
+ (!allowed()
+ ? kOptionAll
+ : privacy->newRequirePremiumCurrent()
+ ? kOptionPremium
+ : privacy->newChargeStarsCurrent()
+ ? kOptionCharge
+ : kOptionAll));
inner->add(
object_ptr(
inner,
@@ -846,6 +996,92 @@ void EditMessagesPrivacyBox(
0,
st::messagePrivacyBottomSkip));
+ Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
+
+ const auto available = session->appConfig().paidMessagesAvailable();
+
+ const auto charged = available
+ ? inner->add(
+ object_ptr(
+ inner,
+ group,
+ kOptionCharge,
+ tr::lng_messages_privacy_charge(tr::now),
+ st::messagePrivacyCheck),
+ st::settingsSendTypePadding + style::margins(
+ 0,
+ st::messagePrivacyBottomSkip,
+ 0,
+ st::messagePrivacyBottomSkip))
+ : nullptr;
+
+ struct State {
+ rpl::variable stars;
+ };
+ const auto state = std::make_shared();
+ const auto savedValue = privacy->newChargeStarsCurrent();
+
+ if (available) {
+ Ui::AddDividerText(inner, tr::lng_messages_privacy_charge_about());
+
+ const auto chargeWrap = inner->add(
+ object_ptr>(
+ inner,
+ object_ptr(inner)));
+ const auto chargeInner = chargeWrap->entity();
+
+ Ui::AddSkip(chargeInner);
+
+ state->stars = SetupChargeSlider(
+ chargeInner,
+ session->user(),
+ savedValue);
+
+ Ui::AddSkip(chargeInner);
+ Ui::AddSubsectionTitle(
+ chargeInner,
+ tr::lng_messages_privacy_exceptions());
+
+ const auto key = Api::UserPrivacy::Key::NoPaidMessages;
+ session->api().userPrivacy().reload(key);
+ auto label = session->api().userPrivacy().value(
+ key
+ ) | rpl::map([=](const Api::UserPrivacy::Rule &value) {
+ using namespace Settings;
+ const auto always = ExceptionUsersCount(value.always.peers);
+ return always
+ ? tr::lng_edit_privacy_exceptions_count(
+ tr::now,
+ lt_count,
+ always)
+ : QString();
+ });
+
+ const auto exceptions = Settings::AddButtonWithLabel(
+ chargeInner,
+ tr::lng_messages_privacy_remove_fee(),
+ std::move(label),
+ st::settingsButtonNoIcon);
+
+ const auto shower = exceptions->lifetime().make_state();
+ exceptions->setClickedCallback([=] {
+ *shower = session->api().userPrivacy().value(
+ key
+ ) | rpl::take(
+ 1
+ ) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) {
+ EditNoPaidMessagesExceptions(controller, value);
+ });
+ });
+ Ui::AddSkip(chargeInner);
+ Ui::AddDividerText(
+ chargeInner,
+ tr::lng_messages_privacy_remove_about());
+
+ using namespace rpl::mappers;
+ chargeWrap->toggleOn(group->value() | rpl::map(_1 == kOptionCharge));
+ chargeWrap->finishAnimating();
+ }
using WeakToast = base::weak_ptr;
const auto toast = std::make_shared();
const auto showToast = [=] {
@@ -875,19 +1111,20 @@ void EditMessagesPrivacyBox(
}),
});
};
+
if (!allowed()) {
CreateRadiobuttonLock(restricted, st::messagePrivacyCheck);
+ if (charged) {
+ CreateRadiobuttonLock(charged, st::messagePrivacyCheck);
+ }
group->setChangedCallback([=](int value) {
- if (value == kOptionPremium) {
+ if (value == kOptionPremium || value == kOptionCharge) {
group->setValue(kOptionAll);
showToast();
}
});
- }
- Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
- if (!allowed()) {
Ui::AddSkip(inner);
Settings::AddButtonWithIcon(
inner,
@@ -907,8 +1144,12 @@ void EditMessagesPrivacyBox(
} else {
box->addButton(tr::lng_settings_save(), [=] {
if (allowed()) {
- privacy->updateNewRequirePremium(
- group->current() == kOptionPremium);
+ const auto value = group->current();
+ const auto premiumRequired = (value == kOptionPremium);
+ const auto chargeStars = (value == kOptionCharge)
+ ? state->stars.current()
+ : 0;
+ privacy->updateMessagesPrivacy(premiumRequired, chargeStars);
box->closeBox();
} else {
showToast();
@@ -919,3 +1160,78 @@ void EditMessagesPrivacyBox(
});
}
}
+
+rpl::producer SetupChargeSlider(
+ not_null container,
+ not_null peer,
+ int savedValue) {
+ struct State {
+ rpl::variable stars;
+ };
+ const auto group = !peer->isUser();
+ const auto state = container->lifetime().make_state();
+ const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars;
+ state->stars = chargeStars;
+
+ Ui::AddSubsectionTitle(container, group
+ ? tr::lng_rights_charge_price()
+ : tr::lng_messages_privacy_price());
+
+ auto values = std::vector();
+ const auto maxStars = peer->session().appConfig().paidMessageStarsMax();
+ if (chargeStars < kStarsMin) {
+ values.push_back(chargeStars);
+ }
+ for (auto i = kStarsMin; i < std::min(100, maxStars); ++i) {
+ values.push_back(i);
+ }
+ for (auto i = 100; i < std::min(1000, maxStars); i += 10) {
+ if (i < chargeStars + 10 && chargeStars < i) {
+ values.push_back(chargeStars);
+ }
+ values.push_back(i);
+ }
+ for (auto i = 1000; i < maxStars + 1; i += 100) {
+ if (i < chargeStars + 100 && chargeStars < i) {
+ values.push_back(chargeStars);
+ }
+ values.push_back(i);
+ }
+ const auto valuesCount = int(values.size());
+ const auto setStars = [=](int value) {
+ state->stars = value;
+ };
+ container->add(
+ MakeChargeStarsSlider(
+ container,
+ &st::settingsScale,
+ &st::settingsScaleLabel,
+ valuesCount,
+ [=](int index) { return values[index]; },
+ chargeStars,
+ maxStars,
+ setStars,
+ setStars),
+ st::boxRowPadding);
+
+ const auto skip = 2 * st::defaultVerticalListSkip;
+ Ui::AddSkip(container, skip);
+
+ auto dollars = state->stars.value() | rpl::map([=](int stars) {
+ const auto ratio = peer->session().appConfig().starsWithdrawRate();
+ const auto dollars = int(base::SafeRound(stars * ratio));
+ return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q);
+ });
+ const auto percent = peer->session().appConfig().paidMessageCommission();
+ Ui::AddDividerText(
+ container,
+ (group
+ ? tr::lng_rights_charge_price_about
+ : tr::lng_messages_privacy_price_about)(
+ lt_percent,
+ rpl::single(QString::number(percent / 10.) + '%'),
+ lt_amount,
+ std::move(dollars)));
+
+ return state->stars.value();
+}
diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h
index bd87e90f2..256ebe5b5 100644
--- a/Telegram/SourceFiles/boxes/edit_privacy_box.h
+++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h
@@ -169,3 +169,8 @@ private:
void EditMessagesPrivacyBox(
not_null box,
not_null controller);
+
+[[nodiscard]] rpl::producer SetupChargeSlider(
+ not_null container,
+ not_null peer,
+ int savedValue);
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
index f9a05a557..be4531aec 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
@@ -441,13 +441,10 @@ void EditFilterBox(
using namespace Window;
return window->isGifPausedAtLeastFor(GifPauseReason::Layer);
};
- name->setCustomTextContext([=](Fn repaint) {
- return std::any(Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = std::move(repaint),
- .customEmojiLoopLimit = value ? -1 : 0,
- });
- }, [paused] {
+ name->setCustomTextContext(Core::TextContext({
+ .session = session,
+ .customEmojiLoopLimit = value ? -1 : 0,
+ }), [paused] {
return On(PowerSaving::kEmojiChat) || paused();
}, [paused] {
return On(PowerSaving::kChatSpoiler) || paused();
@@ -609,10 +606,7 @@ void EditFilterBox(
float64 alpha = 1.;
};
const auto tag = preview->lifetime().make_state();
- tag->context.textContext = Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = [] {},
- };
+ tag->context.textContext = Core::TextContext({ .session = session });
preview->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(preview);
p.setOpacity(tag->alpha);
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
index 2ed72aa25..a200bcecb 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
@@ -163,10 +163,10 @@ ExceptionRow::ExceptionRow(
st::defaultTextStyle,
filters,
kMarkupTextOptions,
- Core::MarkedTextContext{
+ Core::TextContext({
.session = &history->session(),
- .customEmojiRepaint = repaint,
- });
+ .repaint = repaint,
+ }));
} else if (peer()->isSelf()) {
setCustomStatus(tr::lng_saved_forward_here(tr::now));
}
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
index 61bbd85a0..90dd083bd 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
@@ -537,13 +537,6 @@ void LinkController::addHeader(not_null container) {
verticalLayout->add(std::move(icon.widget));
const auto isStatic = _filterTitle.isStatic;
- const auto makeContext = [=](Fn update) {
- return Core::MarkedTextContext{
- .session = &_window->session(),
- .customEmojiRepaint = update,
- .customEmojiLoopLimit = isStatic ? -1 : 0,
- };
- };
verticalLayout->add(
object_ptr>(
verticalLayout,
@@ -559,7 +552,10 @@ void LinkController::addHeader(not_null container) {
Ui::Text::WithEntities)),
st::settingsFilterDividerLabel,
st::defaultPopupMenu,
- makeContext)),
+ Core::TextContext({
+ .session = &_window->session(),
+ .customEmojiLoopLimit = isStatic ? -1 : 0,
+ }))),
st::filterLinkDividerLabelPadding);
verticalLayout->geometryValue(
diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp
index 80e859eb1..761b387e5 100644
--- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_credits.h"
#include "boxes/peer_list_controllers.h"
+#include "core/ui_integration.h" // TextContext.
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
@@ -67,14 +68,9 @@ void GiftCreditsBox(
2.);
{
Ui::AddSkip(content);
- const auto arrow = Ui::Text::SingleCustomEmoji(
- peer->owner().customEmojiManager().registerInternalEmoji(
- st::topicButtonArrow,
- st::channelEarnLearnArrowMargins,
- true));
auto link = tr::lng_credits_box_history_entry_gift_about_link(
lt_emoji,
- rpl::single(arrow),
+ rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
Ui::Text::RichLangValue
) | rpl::map([](TextWithEntities text) {
return Ui::Text::Link(
@@ -92,7 +88,7 @@ void GiftCreditsBox(
lt_link,
std::move(link),
Ui::Text::RichLangValue),
- { .session = &peer->session() },
+ Core::TextContext({ .session = &peer->session() }),
st::creditsBoxAbout)),
st::boxRowPadding);
}
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index db9eaf3a9..691821c16 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox.
#include "boxes/star_gift_box.h" // ShowStarGiftBox.
#include "boxes/transfer_gift_box.h" // ShowTransferGiftBox.
+#include "core/ui_integration.h"
#include "data/data_boosts.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@@ -58,7 +59,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/toast/toast.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/gradient_round_button.h"
-#include "ui/widgets/label_with_custom_emoji.h"
#include "ui/widgets/tooltip.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
@@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_peer_menu.h" // ShowChooseRecipientBox.
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
+#include "styles/style_credits.h"
#include "styles/style_giveaway.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
@@ -516,13 +517,13 @@ not_null AddTableRow(
not_null table,
rpl::producer label,
rpl::producer value,
- const Fn)> &makeContext = nullptr) {
+ const Ui::Text::MarkedContext &context = {}) {
auto widget = object_ptr(
table,
std::move(value),
table->st().defaultValue,
st::defaultPopupMenu,
- std::move(makeContext));
+ context);
const auto result = widget.data();
AddTableRow(
table,
@@ -1272,8 +1273,8 @@ void AddStarGiftTable(
const auto selfBareId = session->userPeerId().value;
const auto giftToSelf = (peerId == session->userPeerId())
&& (entry.in || entry.bareGiftOwnerId == selfBareId);
- const auto giftToChannel = entry.giftSavedId
- && peerIsChannel(PeerId(entry.bareGiftListPeerId));
+ const auto giftToChannel = entry.giftChannelSavedId
+ && peerIsChannel(PeerId(entry.bareEntryOwnerId));
const auto raw = std::make_shared(nullptr);
const auto showTooltip = [=](
@@ -1394,14 +1395,14 @@ void AddStarGiftTable(
? MakePeerTableValue(table, show, PeerId(entry.bareActorId))
: MakeHiddenPeerTableValue(table)),
st::giveawayGiftCodePeerMargin);
- if (entry.bareGiftListPeerId) {
+ if (entry.bareEntryOwnerId) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer(),
MakePeerTableValue(
table,
show,
- PeerId(entry.bareGiftListPeerId)),
+ PeerId(entry.bareEntryOwnerId)),
st::giveawayGiftCodePeerMargin);
}
} else if (peerId && !giftToSelf) {
@@ -1526,12 +1527,6 @@ void AddStarGiftTable(
: nullptr;
const auto date = base::unixtime::parse(original.date).date();
const auto dateText = TextWithEntities{ langDayOfMonth(date) };
- const auto makeContext = [=](Fn update) {
- return Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = std::move(update),
- };
- };
auto label = object_ptr(
table,
(from
@@ -1573,7 +1568,7 @@ void AddStarGiftTable(
? *st.tableValueMessage
: st::giveawayGiftMessage),
st::defaultPopupMenu,
- makeContext);
+ Core::TextContext({ .session = session }));
const auto showBoxLink = [=](not_null peer) {
return std::make_shared([=] {
show->showBox(PrepareShortInfoBox(peer, show));
@@ -1591,12 +1586,6 @@ void AddStarGiftTable(
st::giveawayGiftCodeValueMargin);
}
} else if (!entry.description.empty()) {
- const auto makeContext = [=](Fn update) {
- return Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = std::move(update),
- };
- };
auto label = object_ptr(
table,
rpl::single(entry.description),
@@ -1604,7 +1593,7 @@ void AddStarGiftTable(
? *st.tableValueMessage
: st::giveawayGiftMessage),
st::defaultPopupMenu,
- makeContext);
+ Core::TextContext({ .session = session }));
label->setSelectable(true);
table->addRow(
nullptr,
@@ -1775,6 +1764,25 @@ void AddCreditsHistoryEntryTable(
tr::lng_credits_box_history_entry_subscription(
Ui::Text::WithEntities));
}
+ if (entry.paidMessagesAmount) {
+ auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored);
+ const auto full = (entry.in ? 1 : -1)
+ * (entry.credits + entry.paidMessagesAmount);
+ const auto starsText = Lang::FormatStarsAmountDecimal(full);
+ AddTableRow(
+ table,
+ tr::lng_credits_paid_messages_full(),
+ rpl::single(value.append(' ' + starsText)));
+ }
+ if (const auto months = entry.premiumMonthsForStars) {
+ AddTableRow(
+ table,
+ tr::lng_credits_premium_gift_duration(),
+ tr::lng_months(
+ lt_count,
+ rpl::single(1. * months),
+ Ui::Text::WithEntities));
+ }
if (!entry.id.isEmpty()) {
auto label = MakeMaybeMultilineTokenValue(table, entry.id, st);
label->setClickHandlerFilter([=](const auto &...) {
diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp
index 8007b9383..d278d2681 100644
--- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp
+++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp
@@ -453,10 +453,7 @@ void CreateModerateMessagesBox(
) | rpl::start_with_next([=](const TextWithEntities &text) {
raw->setMarkedText(
Ui::Text::Link(text, u"internal:"_q),
- Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = [=] { raw->update(); },
- });
+ Core::TextContext({ .session = session }));
}, label->lifetime());
Ui::AddSkip(inner);
diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp
index 308384e16..96d5f2d33 100644
--- a/Telegram/SourceFiles/boxes/passcode_box.cpp
+++ b/Telegram/SourceFiles/boxes/passcode_box.cpp
@@ -1154,8 +1154,7 @@ RecoverBox::RecoverBox(
rpl::single(Ui::Text::WrapEmailPattern(pattern)),
Ui::Text::WithEntities),
st::termsContent,
- st::defaultPopupMenu,
- [=](Fn update) { return CommonTextContext{ std::move(update) }; })
+ st::defaultPopupMenu)
, _closeParent(std::move(closeParent)) {
_patternLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
if (_cloudFields.pendingResetDate != 0 || !session) {
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index a9e33955b..d9df69287 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -883,6 +883,7 @@ void PeerListRow::paintUserpic(
} else if (const auto callback = generatePaintUserpicCallback(false)) {
callback(p, x, y, outerWidth, st.photoSize);
}
+ paintUserpicOverlay(p, st, x, y, outerWidth);
}
// Emulates Ui::RoundImageCheckbox::paint() in a checked state.
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h
index c4a79c456..b8dbecb8c 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.h
+++ b/Telegram/SourceFiles/boxes/peer_list_box.h
@@ -95,6 +95,13 @@ public:
[[nodiscard]] virtual QString generateShortName();
[[nodiscard]] virtual auto generatePaintUserpicCallback(
bool forceRound) -> PaintRoundImageCallback;
+ virtual void paintUserpicOverlay(
+ Painter &p,
+ const style::PeerListItem &st,
+ int x,
+ int y,
+ int outerWidth) {
+ }
[[nodiscard]] virtual auto generateNameFirstLetters() const
-> const base::flat_set &;
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index ff3e2722a..3b2214753 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "api/api_chat_participants.h"
-#include "api/api_premium.h"
+#include "api/api_premium.h" // MessageMoneyRestriction.
#include "base/random.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "settings/settings_premium.h"
@@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "dialogs/dialogs_main_list.h"
+#include "payments/ui/payments_reaction_box.h"
#include "ui/effects/outline_segments.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_separate_id.h"
@@ -275,40 +276,71 @@ bool PeerListGlobalSearchController::isLoading() {
return _timer.isActive() || _requestId;
}
+struct RecipientRow::Restriction {
+ Api::MessageMoneyRestriction value;
+ RestrictionBadgeCache cache;
+};
+
RecipientRow::RecipientRow(
not_null peer,
const style::PeerListItem *maybeLockedSt,
History *maybeHistory)
: PeerListRow(peer)
, _maybeHistory(maybeHistory)
-, _resolvePremiumRequired(maybeLockedSt != nullptr) {
- if (maybeLockedSt
- && (Api::ResolveRequiresPremiumToWrite(peer, maybeHistory)
- == Api::RequirePremiumState::Yes)) {
- _lockedSt = maybeLockedSt;
+, _maybeLockedSt(maybeLockedSt) {
+ if (_maybeLockedSt) {
+ setRestriction(Api::ResolveMessageMoneyRestrictions(
+ peer,
+ maybeHistory));
}
}
-PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback(
- bool forceRound) {
- auto result = PeerListRow::generatePaintUserpicCallback(forceRound);
- if (const auto st = _lockedSt) {
- return [=](Painter &p, int x, int y, int outerWidth, int size) {
- result(p, x, y, outerWidth, size);
- PaintPremiumRequiredLock(p, st, x, y, outerWidth, size);
- };
+Api::MessageMoneyRestriction RecipientRow::restriction() const {
+ return _restriction
+ ? _restriction->value
+ : Api::MessageMoneyRestriction();
+}
+
+void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) {
+ if (!restriction) {
+ _restriction = nullptr;
+ return;
+ } else if (!_restriction) {
+ _restriction = std::make_unique();
+ }
+ _restriction->value = restriction;
+}
+
+void RecipientRow::paintUserpicOverlay(
+ Painter &p,
+ const style::PeerListItem &st,
+ int x,
+ int y,
+ int outerWidth) {
+ if (const auto &r = _restriction) {
+ PaintRestrictionBadge(
+ p,
+ _maybeLockedSt,
+ r->value.starsPerMessage,
+ r->cache,
+ x,
+ y,
+ outerWidth,
+ st.photoSize);
}
- return result;
}
bool RecipientRow::refreshLock(
not_null maybeLockedSt) {
if (const auto user = peer()->asUser()) {
- const auto locked = _resolvePremiumRequired
- && (Api::ResolveRequiresPremiumToWrite(user, _maybeHistory)
- == Api::RequirePremiumState::Yes);
- if (this->locked() != locked) {
- setLocked(locked ? maybeLockedSt.get() : nullptr);
+ using Restriction = Api::MessageMoneyRestriction;
+ const auto r = _maybeLockedSt
+ ? Api::ResolveMessageMoneyRestrictions(
+ user,
+ _maybeHistory)
+ : Restriction();
+ if ((_restriction ? _restriction->value : Restriction()) != r) {
+ setRestriction(r);
return true;
}
}
@@ -318,22 +350,30 @@ bool RecipientRow::refreshLock(
void RecipientRow::preloadUserpic() {
PeerListRow::preloadUserpic();
- if (!_resolvePremiumRequired) {
+ if (!_maybeLockedSt) {
return;
- } else if (Api::ResolveRequiresPremiumToWrite(peer(), _maybeHistory)
- == Api::RequirePremiumState::Unknown) {
- const auto user = peer()->asUser();
- user->session().api().premium().resolvePremiumRequired(user);
+ }
+ const auto peer = this->peer();
+ const auto known = Api::ResolveMessageMoneyRestrictions(
+ peer,
+ _maybeHistory).known;
+ if (known) {
+ return;
+ } else if (const auto user = peer->asUser()) {
+ const auto api = &user->session().api();
+ api->premium().resolveMessageMoneyRestrictions(user);
+ } else if (const auto group = peer->asChannel()) {
+ group->updateFull();
}
}
-void TrackPremiumRequiredChanges(
+void TrackMessageMoneyRestrictionsChanges(
not_null controller,
rpl::lifetime &lifetime) {
const auto session = &controller->session();
rpl::merge(
Data::AmPremiumValue(session) | rpl::to_empty,
- session->api().premium().somePremiumRequiredResolved()
+ session->api().premium().someMessageMoneyRestrictionsResolved()
) | rpl::start_with_next([=] {
const auto st = &controller->computeListSt().item;
const auto delegate = controller->delegate();
@@ -726,7 +766,7 @@ std::unique_ptr ContactsBoxController::createRow(
return std::make_unique(user);
}
-RecipientPremiumRequiredError WritePremiumRequiredError(
+RecipientMoneyRestrictionError WriteMoneyRestrictionError(
not_null user) {
return {
.text = tr::lng_send_non_premium_message_toast(
@@ -759,7 +799,7 @@ ChooseRecipientBoxController::ChooseRecipientBoxController(
, _session(args.session)
, _callback(std::move(args.callback))
, _filter(std::move(args.filter))
-, _premiumRequiredError(std::move(args.premiumRequiredError)) {
+, _moneyRestrictionError(std::move(args.moneyRestrictionError)) {
}
Main::Session &ChooseRecipientBoxController::session() const {
@@ -769,14 +809,17 @@ Main::Session &ChooseRecipientBoxController::session() const {
void ChooseRecipientBoxController::prepareViewHook() {
delegate()->peerListSetTitle(tr::lng_forward_choose());
- if (_premiumRequiredError) {
- TrackPremiumRequiredChanges(this, lifetime());
+ if (_moneyRestrictionError) {
+ TrackMessageMoneyRestrictionsChanges(this, lifetime());
}
}
bool ChooseRecipientBoxController::showLockedError(
not_null row) {
- return RecipientRow::ShowLockedError(this, row, _premiumRequiredError);
+ return RecipientRow::ShowLockedError(
+ this,
+ row,
+ _moneyRestrictionError);
}
void ChooseRecipientBoxController::rowClicked(not_null row) {
@@ -836,8 +879,9 @@ void ChooseRecipientBoxController::rowClicked(not_null row) {
bool RecipientRow::ShowLockedError(
not_null controller,
not_null row,
- Fn)> error) {
- if (!static_cast(row.get())->locked()) {
+ Fn)> error) {
+ const auto recipient = static_cast(row.get());
+ if (!recipient->restriction().premiumRequired) {
return false;
}
::Settings::ShowPremiumPromoToast(
@@ -860,15 +904,15 @@ auto ChooseRecipientBoxController::createRow(
: ((peer->isBroadcast() && !Data::CanSendAnything(peer))
|| peer->isRepliesChat()
|| peer->isVerifyCodes()
- || (peer->isUser() && (_premiumRequiredError
- ? !peer->asUser()->canSendIgnoreRequirePremium()
+ || (peer->isUser() && (_moneyRestrictionError
+ ? !peer->asUser()->canSendIgnoreMoneyRestrictions()
: !Data::CanSendAnything(peer))));
if (skip) {
return nullptr;
}
auto result = std::make_unique(
history,
- _premiumRequiredError ? &computeListSt().item : nullptr);
+ _moneyRestrictionError ? &computeListSt().item : nullptr);
return result;
}
@@ -1093,25 +1137,61 @@ auto ChooseTopicBoxController::createRow(not_null topic)
return skip ? nullptr : std::make_unique(topic);
};
-void PaintPremiumRequiredLock(
+void PaintRestrictionBadge(
Painter &p,
not_null st,
+ int stars,
+ RestrictionBadgeCache &cache,
int x,
int y,
int outerWidth,
int size) {
- auto hq = PainterHighQualityEnabler(p);
+ const auto paletteVersion = style::PaletteVersion();
+ const auto good = !cache.badge.isNull()
+ && (cache.stars == stars)
+ && (cache.paletteVersion == paletteVersion);
const auto &check = st->checkbox.check;
- auto pen = check.border->p;
- pen.setWidthF(check.width);
- p.setPen(pen);
- p.setBrush(st::premiumButtonBg2);
- const auto &icon = st::stickersPremiumLock;
- const auto width = icon.width();
- const auto height = icon.height();
- const auto rect = QRect(
- QPoint(x + size - width, y + size - height),
- icon.size());
- p.drawEllipse(rect);
- icon.paintInCenter(p, rect);
+ const auto add = check.width;
+ if (!good) {
+ cache.stars = stars;
+ cache.paletteVersion = paletteVersion;
+ if (stars) {
+ const auto text = (stars >= 1000)
+ ? (QString::number(stars / 1000) + 'K')
+ : QString::number(stars);
+ cache.badge = Ui::GenerateSmallBadgeImage(
+ text,
+ st::paidReactTopStarIcon,
+ check.bgActive->c,
+ st::premiumButtonFg->c,
+ &check);
+ } else {
+ auto hq = PainterHighQualityEnabler(p);
+ const auto &icon = st::stickersPremiumLock;
+ const auto width = icon.width();
+ const auto height = icon.height();
+ const auto rect = QRect(
+ QPoint(x + size - width, y + size - height),
+ icon.size());
+ const auto added = QMargins(add, add, add, add);
+ const auto ratio = style::DevicePixelRatio();
+ cache.badge = QImage(
+ (rect + added).size() * ratio,
+ QImage::Format_ARGB32_Premultiplied);
+ cache.badge.setDevicePixelRatio(ratio);
+ cache.badge.fill(Qt::transparent);
+ const auto inner = QRect(add, add, rect.width(), rect.height());
+ auto q = QPainter(&cache.badge);
+ auto pen = check.border->p;
+ pen.setWidthF(check.width);
+ q.setPen(pen);
+ q.setBrush(st::premiumButtonBg2);
+ q.drawEllipse(inner);
+ icon.paintInCenter(q, inner);
+ }
+ }
+ const auto cached = cache.badge.size() / cache.badge.devicePixelRatio();
+ const auto left = x + size + add - cached.width();
+ const auto top = stars ? (y - add) : (y + size + add - cached.height());
+ p.drawImage(left, top, cache.badge);
}
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h
index 07f71534a..de9c67dbf 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.h
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h
@@ -19,6 +19,10 @@ namespace style {
struct PeerListItem;
} // namespace style
+namespace Api {
+struct MessageMoneyRestriction;
+} // namespace Api
+
namespace Data {
class Thread;
class Forum;
@@ -93,13 +97,28 @@ private:
};
-struct RecipientPremiumRequiredError {
+struct RecipientMoneyRestrictionError {
TextWithEntities text;
};
-[[nodiscard]] RecipientPremiumRequiredError WritePremiumRequiredError(
+[[nodiscard]] RecipientMoneyRestrictionError WriteMoneyRestrictionError(
not_null user);
+struct RestrictionBadgeCache {
+ int paletteVersion = 0;
+ int stars = 0;
+ QImage badge;
+};
+void PaintRestrictionBadge(
+ Painter &p,
+ not_null st,
+ int stars,
+ RestrictionBadgeCache &cache,
+ int x,
+ int y,
+ int outerWidth,
+ int size);
+
class RecipientRow : public PeerListRow {
public:
explicit RecipientRow(
@@ -112,30 +131,33 @@ public:
[[nodiscard]] static bool ShowLockedError(
not_null controller,
not_null row,
- Fn)> error);
+ Fn)> error);
[[nodiscard]] History *maybeHistory() const {
return _maybeHistory;
}
- [[nodiscard]] bool locked() const {
- return _lockedSt != nullptr;
- }
- void setLocked(const style::PeerListItem *lockedSt) {
- _lockedSt = lockedSt;
- }
- PaintRoundImageCallback generatePaintUserpicCallback(
- bool forceRound) override;
+ void paintUserpicOverlay(
+ Painter &p,
+ const style::PeerListItem &st,
+ int x,
+ int y,
+ int outerWidth) override;
void preloadUserpic() override;
+ [[nodiscard]] Api::MessageMoneyRestriction restriction() const;
+ void setRestriction(Api::MessageMoneyRestriction restriction);
+
private:
+ struct Restriction;
+
History *_maybeHistory = nullptr;
- const style::PeerListItem *_lockedSt = nullptr;
- bool _resolvePremiumRequired = false;
+ const style::PeerListItem *_maybeLockedSt = nullptr;
+ std::shared_ptr _restriction;
};
-void TrackPremiumRequiredChanges(
+void TrackMessageMoneyRestrictionsChanges(
not_null controller,
rpl::lifetime &lifetime);
@@ -261,8 +283,8 @@ struct ChooseRecipientArgs {
FnMut)> callback;
Fn)> filter;
- using PremiumRequiredError = RecipientPremiumRequiredError;
- Fn)> premiumRequiredError;
+ using MoneyRestrictionError = RecipientMoneyRestrictionError;
+ Fn)> moneyRestrictionError;
};
class ChooseRecipientBoxController
@@ -290,8 +312,8 @@ private:
const not_null _session;
FnMut)> _callback;
Fn)> _filter;
- Fn)> _premiumRequiredError;
+ Fn)> _moneyRestrictionError;
};
@@ -371,11 +393,3 @@ private:
Fn)> _filter;
};
-
-void PaintPremiumRequiredLock(
- Painter &p,
- not_null st,
- int x,
- int y,
- int outerWidth,
- int size);
diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp
index ab48b6b8d..4701cac1e 100644
--- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp
@@ -9,10 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_participants.h"
#include "api/api_invite_links.h"
+#include "api/api_premium.h"
#include "boxes/peers/edit_participant_box.h"
#include "boxes/peers/edit_peer_type_box.h"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/max_invite_box.h"
+#include "chat_helpers/message_field.h"
#include "lang/lang_keys.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
@@ -22,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h"
#include "data/data_peer_values.h"
#include "history/history.h"
+#include "history/history_item_helpers.h"
#include "dialogs/dialogs_indexed_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/show_or_premium_box.h"
@@ -52,16 +55,39 @@ constexpr auto kUserpicsLimit = 3;
class ForbiddenRow final : public PeerListRow {
public:
- ForbiddenRow(not_null peer, bool locked);
+ ForbiddenRow(
+ not_null peer,
+ not_null lockSt,
+ bool locked);
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
+ Api::MessageMoneyRestriction restriction() const;
+ void setRestriction(Api::MessageMoneyRestriction restriction);
+
+ void preloadUserpic() override;
+ void paintUserpicOverlay(
+ Painter &p,
+ const style::PeerListItem &st,
+ int x,
+ int y,
+ int outerWidth) override;
+
+ bool refreshLock();
+
private:
+ struct Restriction {
+ Api::MessageMoneyRestriction value;
+ RestrictionBadgeCache cache;
+ };
+
const bool _locked = false;
+ const not_null _lockSt;
QImage _disabledFrame;
InMemoryKey _userpicKey;
int _paletteVersion = 0;
+ std::shared_ptr _restriction;
};
@@ -81,6 +107,9 @@ public:
[[nodiscard]] rpl::producer selectedValue() const {
return _selected.value();
}
+ [[nodiscard]] rpl::producer starsToSend() const {
+ return _starsToSend.value();
+ }
void send(
std::vector> list,
@@ -89,10 +118,16 @@ public:
private:
void appendRow(not_null user);
- [[nodiscard]] std::unique_ptr createRow(
+ [[nodiscard]] std::unique_ptr createRow(
not_null user) const;
[[nodiscard]] bool canInvite(not_null peer) const;
+ void send(
+ std::vector> list,
+ Ui::ShowPtr show,
+ Fn close,
+ Api::SendOptions options);
+
void setSimpleCover();
void setComplexCover();
@@ -101,8 +136,11 @@ private:
const std::vector> &_users;
const bool _can = false;
rpl::variable _selected;
+ rpl::variable _starsToSend;
bool _sending = false;
+ rpl::lifetime _paymentCheckLifetime;
+
};
base::flat_set> GetAlreadyInFromPeer(PeerData *peer) {
@@ -256,11 +294,17 @@ Main::Session &InviteForbiddenController::session() const {
return _peer->session();
}
-ForbiddenRow::ForbiddenRow(not_null peer, bool locked)
+ForbiddenRow::ForbiddenRow(
+ not_null peer,
+ not_null lockSt,
+ bool locked)
: PeerListRow(peer)
-, _locked(locked) {
+, _locked(locked)
+, _lockSt(lockSt) {
if (_locked) {
setCustomStatus(tr::lng_invite_status_disabled(tr::now));
+ } else {
+ setRestriction(Api::ResolveMessageMoneyRestrictions(peer, nullptr));
}
}
@@ -339,6 +383,76 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
};
}
+
+Api::MessageMoneyRestriction ForbiddenRow::restriction() const {
+ return _restriction
+ ? _restriction->value
+ : Api::MessageMoneyRestriction();
+}
+
+void ForbiddenRow::setRestriction(Api::MessageMoneyRestriction restriction) {
+ if (!restriction || !restriction.starsPerMessage) {
+ _restriction = nullptr;
+ return;
+ } else if (!_restriction) {
+ _restriction = std::make_unique();
+ }
+ _restriction->value = restriction;
+}
+
+void ForbiddenRow::paintUserpicOverlay(
+ Painter &p,
+ const style::PeerListItem &st,
+ int x,
+ int y,
+ int outerWidth) {
+ if (const auto &r = _restriction) {
+ PaintRestrictionBadge(
+ p,
+ _lockSt,
+ r->value.starsPerMessage,
+ r->cache,
+ x,
+ y,
+ outerWidth,
+ st.photoSize);
+ }
+}
+
+bool ForbiddenRow::refreshLock() {
+ if (_locked) {
+ return false;
+ } else if (const auto user = peer()->asUser()) {
+ using Restriction = Api::MessageMoneyRestriction;
+ auto r = Api::ResolveMessageMoneyRestrictions(user, nullptr);
+ if (!r || !r.starsPerMessage) {
+ r = Restriction();
+ }
+ if ((_restriction ? _restriction->value : Restriction()) != r) {
+ setRestriction(r);
+ return true;
+ }
+ }
+ return false;
+}
+
+void ForbiddenRow::preloadUserpic() {
+ PeerListRow::preloadUserpic();
+
+ const auto peer = this->peer();
+ const auto known = Api::ResolveMessageMoneyRestrictions(
+ peer,
+ nullptr).known;
+ if (known) {
+ return;
+ } else if (const auto user = peer->asUser()) {
+ const auto api = &user->session().api();
+ api->premium().resolveMessageMoneyRestrictions(user);
+ } else if (const auto group = peer->asChannel()) {
+ group->updateFull();
+ }
+}
+
void InviteForbiddenController::setSimpleCover() {
delegate()->peerListSetTitle(
_can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant());
@@ -435,6 +549,30 @@ void InviteForbiddenController::setComplexCover() {
}
void InviteForbiddenController::prepare() {
+ session().api().premium().someMessageMoneyRestrictionsResolved(
+ ) | rpl::start_with_next([=] {
+ auto stars = 0;
+ const auto process = [&](not_null raw) {
+ const auto row = static_cast(raw.get());
+ if (row->refreshLock()) {
+ delegate()->peerListUpdateRow(raw);
+ }
+ if (const auto r = row->restriction()) {
+ stars += r.starsPerMessage;
+ }
+ };
+ auto count = delegate()->peerListFullRowsCount();
+ for (auto i = 0; i != count; ++i) {
+ process(delegate()->peerListRowAt(i));
+ }
+ _starsToSend = stars;
+
+ count = delegate()->peerListSearchRowsCount();
+ for (auto i = 0; i != count; ++i) {
+ process(delegate()->peerListSearchRowAt(i));
+ }
+ }, lifetime());
+
if (session().premium()
|| (_forbidden.premiumAllowsInvite.empty()
&& _forbidden.premiumAllowsWrite.empty())) {
@@ -464,6 +602,11 @@ void InviteForbiddenController::rowClicked(not_null row) {
const auto checked = row->checked();
delegate()->peerListSetRowChecked(row, !checked);
_selected = _selected.current() + (checked ? -1 : 1);
+ const auto r = static_cast(row.get())->restriction();
+ if (r.starsPerMessage) {
+ _starsToSend = _starsToSend.current()
+ + (checked ? -r.starsPerMessage : r.starsPerMessage);
+ }
}
void InviteForbiddenController::appendRow(not_null user) {
@@ -473,6 +616,9 @@ void InviteForbiddenController::appendRow(not_null user) {
delegate()->peerListAppendRow(std::move(row));
if (canInvite(user)) {
delegate()->peerListSetRowChecked(raw, true);
+ if (const auto r = raw->restriction()) {
+ _starsToSend = _starsToSend.current() + r.starsPerMessage;
+ }
}
}
}
@@ -481,7 +627,64 @@ void InviteForbiddenController::send(
std::vector> list,
Ui::ShowPtr show,
Fn close) {
- if (_sending || list.empty()) {
+ send(list, show, close, {});
+}
+
+void InviteForbiddenController::send(
+ std::vector> list,
+ Ui::ShowPtr show,
+ Fn close,
+ Api::SendOptions options) {
+ if (list.empty()) {
+ return;
+ }
+ _paymentCheckLifetime.destroy();
+
+ const auto withPaymentApproved = [=](int approved) {
+ auto copy = options;
+ copy.starsApproved = approved;
+ send(list, show, close, copy);
+ };
+ const auto messagesCount = 1;
+ const auto alreadyApproved = options.starsApproved;
+ auto paid = std::vector>();
+ auto waiting = base::flat_set>();
+ auto totalStars = 0;
+ for (const auto &peer : list) {
+ const auto details = ComputePaymentDetails(peer, messagesCount);
+ if (!details) {
+ waiting.emplace(peer);
+ } else if (details->stars > 0) {
+ totalStars += details->stars;
+ paid.push_back(peer);
+ }
+ }
+ if (!waiting.empty()) {
+ session().changes().peerUpdates(
+ Data::PeerUpdate::Flag::FullInfo
+ ) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
+ if (waiting.contains(update.peer)) {
+ withPaymentApproved(alreadyApproved);
+ }
+ }, _paymentCheckLifetime);
+
+ if (!session().credits().loaded()) {
+ session().credits().loadedValue(
+ ) | rpl::filter(
+ rpl::mappers::_1
+ ) | rpl::take(1) | rpl::start_with_next([=] {
+ withPaymentApproved(alreadyApproved);
+ }, _paymentCheckLifetime);
+ }
+ return;
+ } else if (totalStars > alreadyApproved) {
+ const auto sessionShow = Main::MakeSessionShow(show, &session());
+ ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{
+ .messages = messagesCount,
+ .stars = totalStars,
+ }, [=] { withPaymentApproved(totalStars); });
+ return;
+ } else if (_sending) {
return;
}
_sending = true;
@@ -492,12 +695,18 @@ void InviteForbiddenController::send(
if (link.isEmpty()) {
return false;
}
+ auto full = options;
auto &api = _peer->session().api();
- auto options = Api::SendOptions();
for (const auto &to : list) {
+ auto copy = full;
+ copy.starsApproved = std::min(
+ to->starsPerMessageChecked(),
+ full.starsApproved);
+ full.starsApproved -= copy.starsApproved;
+
const auto history = to->owner().history(to);
auto message = Api::MessageToSend(
- Api::SendAction(history, options));
+ Api::SendAction(history, copy));
message.textWithTags = { link };
message.action.clearDraft = false;
api.sendMessage(std::move(message));
@@ -542,10 +751,11 @@ void InviteForbiddenController::send(
}
}
-std::unique_ptr InviteForbiddenController::createRow(
+std::unique_ptr InviteForbiddenController::createRow(
not_null user) const {
const auto locked = _can && !canInvite(user);
- return std::make_unique(user, locked);
+ const auto lockSt = &computeListSt().item;
+ return std::make_unique(user, lockSt, locked);
}
} // namespace
@@ -584,8 +794,8 @@ void AddParticipantsBoxController::subscribeToMigration() {
}
void AddParticipantsBoxController::rowClicked(not_null row) {
- const auto premiumRequiredError = WritePremiumRequiredError;
- if (RecipientRow::ShowLockedError(this, row, premiumRequiredError)) {
+ const auto moneyRestrictionError = WriteMoneyRestrictionError;
+ if (RecipientRow::ShowLockedError(this, row, moneyRestrictionError)) {
return;
}
const auto &serverConfig = session().serverConfig();
@@ -614,7 +824,7 @@ void AddParticipantsBoxController::itemDeselectedHook(
void AddParticipantsBoxController::prepareViewHook() {
updateTitle();
- TrackPremiumRequiredChanges(this, lifetime());
+ TrackMessageMoneyRestrictionsChanges(this, lifetime());
}
int AddParticipantsBoxController::alreadyInCount() const {
@@ -929,12 +1139,15 @@ bool ChatInviteForbidden(
) | rpl::start_with_next([=](bool has) {
box->clearButtons();
if (has) {
- box->addButton(tr::lng_via_link_send(), [=] {
+ const auto send = box->addButton(tr::lng_via_link_send(), [=] {
weak->send(
box->collectSelectedRows(),
box->uiShow(),
crl::guard(box, [=] { box->closeBox(); }));
});
+ send->setText(PaidSendButtonText(
+ weak->starsToSend(),
+ tr::lng_via_link_send()));
}
box->addButton(tr::lng_create_group_skip(), [=] {
box->closeBox();
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
index 73b3004f0..c457ce579 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
@@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/background_box.h"
#include "boxes/stickers_box.h"
#include "chat_helpers/compose/compose_show.h"
-#include "core/ui_integration.h" // Core::MarkedTextContext.
+#include "core/ui_integration.h" // TextContext
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_changes.h"
@@ -165,7 +165,7 @@ private:
const uint32 _level;
const TextWithEntities _icon;
- const Core::MarkedTextContext _context;
+ const Ui::Text::MarkedContext _context;
Ui::Text::String _text;
bool _minimal = false;
@@ -466,7 +466,10 @@ LevelBadge::LevelBadge(
st::settingsLevelBadgeLock,
QMargins(0, st::settingsLevelBadgeLockSkip, 0, 0),
false)))
-, _context({ .session = session }) {
+, _context(Core::TextContext({
+ .session = session,
+ .repaint = [this] { update(); },
+})) {
updateText();
}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
index b5cad4e65..75ca484b8 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
@@ -219,6 +219,33 @@ void SaveSlowmodeSeconds(
api->registerModifyRequest(key, requestId);
}
+void SaveStarsPerMessage(
+ not_null channel,
+ int starsPerMessage,
+ Fn done) {
+ const auto api = &channel->session().api();
+ const auto key = Api::RequestKey("stars_per_message", channel->id);
+
+ const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice(
+ channel->inputChannel,
+ MTP_long(starsPerMessage)
+ )).done([=](const MTPUpdates &result) {
+ api->clearModifyRequest(key);
+ api->applyUpdates(result);
+ channel->setStarsPerMessage(starsPerMessage);
+ done();
+ }).fail([=](const MTP::Error &error) {
+ api->clearModifyRequest(key);
+ if (error.type() != u"CHAT_NOT_MODIFIED"_q) {
+ return;
+ }
+ channel->setStarsPerMessage(starsPerMessage);
+ done();
+ }).send();
+
+ api->registerModifyRequest(key, requestId);
+}
+
void SaveBoostsUnrestrict(
not_null channel,
int boostsUnrestrict,
@@ -271,6 +298,7 @@ void ShowEditPermissions(
channel,
result.boostsUnrestrict,
close);
+ SaveStarsPerMessage(channel, result.starsPerMessage, close);
}
};
auto done = [=](EditPeerPermissionsBoxResult result) {
@@ -282,7 +310,9 @@ void ShowEditPermissions(
const auto saveFor = peer->migrateToOrMe();
const auto chat = saveFor->asChat();
if (!chat
- || (!result.slowmodeSeconds && !result.boostsUnrestrict)) {
+ || (!result.slowmodeSeconds
+ && !result.boostsUnrestrict
+ && !result.starsPerMessage)) {
save(saveFor, result);
return;
}
@@ -2689,3 +2719,9 @@ bool EditPeerInfoBox::Available(not_null peer) {
return false;
}
}
+
+void ShowEditChatPermissions(
+ not_null navigation,
+ not_null peer) {
+ ShowEditPermissions(navigation, peer);
+}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h
index 9844320cf..b8787afac 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h
@@ -56,3 +56,7 @@ private:
not_null _peer;
};
+
+void ShowEditChatPermissions(
+ not_null navigation,
+ not_null peer);
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
index 2797b5394..70ad41acb 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
@@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "boxes/share_box.h"
#include "core/application.h"
-#include "core/ui_integration.h" // Core::MarkedTextContext.
+#include "core/ui_integration.h" // TextContext
#include "data/components/credits.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@@ -740,10 +740,10 @@ void Controller::setupAboveJoinedWidget() {
{ QString::number(current.subscription.credits) },
Ui::Text::WithEntities),
kMarkupTextOptions,
- Core::MarkedTextContext{
+ Core::TextContext({
.session = &session(),
- .customEmojiRepaint = [=] { widget->update(); },
- });
+ .repaint = [=] { widget->update(); },
+ }));
auto &lifetime = widget->lifetime();
const auto rateValue = lifetime.make_state>(
session().credits().rateValue(_peer));
@@ -994,10 +994,7 @@ void Controller::rowClicked(not_null row) {
lt_cost,
{ QString::number(data.subscription.credits) },
Ui::Text::WithEntities),
- Core::MarkedTextContext{
- .session = session,
- .customEmojiRepaint = [=] { subtitle1->update(); },
- });
+ Core::TextContext({ .session = session }));
const auto subtitle2 = box->addRow(
object_ptr>(
box,
@@ -1484,8 +1481,12 @@ object_ptr ShareInviteLinkBox(
? tr::lng_group_invite_copied(tr::now)
: copied);
};
+ auto countMessagesCallback = [=](const TextWithTags &comment) {
+ return 1;
+ };
auto submitCallback = [=](
std::vector> &&result,
+ Fn checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@@ -1503,6 +1504,8 @@ object_ptr ShareInviteLinkBox(
result.size() > 1));
}
return;
+ } else if (!checkPaid()) {
+ return;
}
*sending = true;
@@ -1530,7 +1533,7 @@ object_ptr ShareInviteLinkBox(
};
auto filterCallback = [](not_null thread) {
if (const auto user = thread->peer()->asUser()) {
- if (user->canSendIgnoreRequirePremium()) {
+ if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@@ -1539,9 +1542,10 @@ object_ptr ShareInviteLinkBox(
auto object = Box(ShareBox::Descriptor{
.session = session,
.copyCallback = std::move(copyCallback),
+ .countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
- .premiumRequiredError = SharePremiumRequiredError(),
+ .moneyRestrictionError = ShareMessageMoneyRestrictionError(),
});
*box = Ui::MakeWeak(object.data());
return object;
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp
index 1b6996fad..c630f603b 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp
@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_values.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/edit_peer_info_box.h"
+#include "boxes/edit_privacy_box.h"
#include "settings/settings_power_saving.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
@@ -891,11 +892,10 @@ void AddBoostsUnrestrictLabels(
manager->registerInternalEmoji(
st::boostsMessageIcon,
st::boostsMessageIconPadding));
- const auto context = Core::MarkedTextContext{
+ const auto context = Core::TextContext({
.session = session,
- .customEmojiRepaint = [] {},
.customEmojiLoopLimit = 1,
- };
+ });
for (auto i = 0; i != kBoostsUnrestrictValues; ++i) {
const auto label = Ui::CreateChild(
labels,
@@ -942,9 +942,7 @@ rpl::producer AddBoostsUnrestrictSlider(
const auto boostsUnrestrict = lifetime.make_state>(
channel ? channel->boostsUnrestrict() : 0);
- container->add(
- object_ptr