Merge tag 'v5.12.1' into dev

This commit is contained in:
AlexeyZavar 2025-03-08 23:02:24 +03:00
commit adc691f516
271 changed files with 7375 additions and 2789 deletions

View file

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

View file

@ -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";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.11.1.0" />
Version="5.12.1.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -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"

View file

@ -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"

View file

@ -149,18 +149,14 @@ void InitFilterLinkHeader(
iconEmoji
).value_or(Ui::FilterIcon::Custom)).active;
const auto isStatic = title.isStatic;
const auto makeContext = [=](Fn<void()> 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<QWidget*> 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<void()> 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<void()> 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();

View file

@ -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;

View file

@ -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<Data::StarGift>();
@ -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(),

View file

@ -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<bool> 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<bool> GlobalPrivacy::newRequirePremium() const {
return _newRequirePremium.value();
}
int GlobalPrivacy::newChargeStarsCurrent() const {
return _newChargeStars.current();
}
rpl::producer<int> 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

View file

@ -49,23 +49,28 @@ public:
[[nodiscard]] bool hideReadTimeCurrent() const;
[[nodiscard]] rpl::producer<bool> hideReadTime() const;
void updateNewRequirePremium(bool value);
[[nodiscard]] bool newRequirePremiumCurrent() const;
[[nodiscard]] rpl::producer<bool> newRequirePremium() const;
[[nodiscard]] int newChargeStarsCurrent() const;
[[nodiscard]] rpl::producer<int> newChargeStars() const;
void updateMessagesPrivacy(bool requirePremium, int chargeStars);
void loadPaidReactionShownPeer();
void updatePaidReactionShownPeer(PeerId shownPeer);
[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
[[nodiscard]] rpl::producer<PeerId> 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<Main::Session*> _session;
MTP::Sender _api;
@ -76,6 +81,7 @@ private:
rpl::variable<bool> _showArchiveAndMute = false;
rpl::variable<bool> _hideReadTime = false;
rpl::variable<bool> _newRequirePremium = false;
rpl::variable<int> _newChargeStars = 0;
rpl::variable<PeerId> _paidReactionShownPeer = false;
std::vector<Fn<void()>> _callbacks;
bool _paidReactionShownPeerLoaded = false;

View file

@ -42,7 +42,7 @@ Polls::Polls(not_null<ApiWrap*> api)
void Polls::create(
const PollData &data,
const SendAction &action,
SendAction action,
Fn<void()> done,
Fn<void()> 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(

View file

@ -27,7 +27,7 @@ public:
void create(
const PollData &data,
const SendAction &action,
SendAction action,
Fn<void()> done,
Fn<void()> fail);
void sendVotes(

View file

@ -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<UserData*> user) {
_resolvePremiumRequiredUsers.emplace(user);
if (!_premiumRequiredRequestScheduled
&& _resolvePremiumRequestedUsers.empty()) {
_premiumRequiredRequestScheduled = true;
void Premium::resolveMessageMoneyRestrictions(not_null<UserData*> user) {
_resolveMessageMoneyRequiredUsers.emplace(user);
if (!_messageMoneyRequestScheduled
&& _resolveMessageMoneyRequestedUsers.empty()) {
_messageMoneyRequestScheduled = true;
crl::on_main(_session, [=] {
requestPremiumRequiredSlice();
});
@ -393,50 +394,65 @@ void Premium::resolvePremiumRequired(not_null<UserData*> 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<MTPInputUser>(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<MTPBool> &list) {
constexpr auto me = UserDataFlag::MeRequiresPremiumToWrite;
constexpr auto known = UserDataFlag::RequirePremiumToWriteKnown;
constexpr auto mask = me | known;
const auto finish = [=](const QVector<MTPRequirementToContact> &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<MTPBool> &result) {
MTPusers_GetRequirementsToContact(std::move(users))
).done([=](const MTPVector<MTPRequirementToContact> &result) {
finish(result.v);
}).fail([=] {
finish({});
@ -463,10 +479,14 @@ rpl::producer<rpl::no_value, QString> 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<rpl::no_value, QString> 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<rpl::no_value, QString> 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<GiftOptionData> PremiumGiftCodeOptions::optionsForPeer() const {
auto result = std::vector<GiftOptionData>();
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<rpl::no_value, QString> SponsoredToggle::setToggled(bool v) {
};
}
RequirePremiumState ResolveRequiresPremiumToWrite(
MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null<PeerData*> 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<DocumentData*> RandomHelloStickerValue(
@ -870,6 +904,7 @@ std::optional<Data::SavedStarGift> 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(),
};

View file

@ -116,8 +116,9 @@ public:
[[nodiscard]] auto subscriptionOptions() const
-> const Data::PremiumSubscriptionOptions &;
[[nodiscard]] rpl::producer<> somePremiumRequiredResolved() const;
void resolvePremiumRequired(not_null<UserData*> user);
[[nodiscard]] auto someMessageMoneyRestrictionsResolved() const
-> rpl::producer<>;
void resolveMessageMoneyRestrictions(not_null<UserData*> user);
private:
void reloadPromo();
@ -166,10 +167,10 @@ private:
Data::PremiumSubscriptionOptions _subscriptionOptions;
rpl::event_stream<> _somePremiumRequiredResolved;
base::flat_set<not_null<UserData*>> _resolvePremiumRequiredUsers;
base::flat_set<not_null<UserData*>> _resolvePremiumRequestedUsers;
bool _premiumRequiredRequestScheduled = false;
rpl::event_stream<> _someMessageMoneyRestrictionsResolved;
base::flat_set<not_null<UserData*>> _resolveMessageMoneyRequiredUsers;
base::flat_set<not_null<UserData*>> _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<int> months;
std::vector<int64> totalCosts;
QString currency;
std::vector<QString> currencies;
} _optionsForOnePerson;
std::vector<int> _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<PeerData*> peer,
History *maybeHistory);

View file

@ -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(

View file

@ -24,15 +24,26 @@ template<typename Option>
if (tlOpts.isEmpty()) {
return {};
}
auto monthlyAmountPerCurrency = base::flat_map<QString, int>();
auto result = Data::PremiumSubscriptionOptions();
const auto monthlyAmount = [&] {
const auto monthlyAmount = [&](const QString &currency) -> 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<int64_t>::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<typename Option>
const auto currency = qs(option.vcurrency());
result.push_back(CreateSubscriptionOption(
months,
monthlyAmount,
monthlyAmount(currency),
amount,
currency,
botUrl));

View file

@ -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<uint64>();
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,

View file

@ -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;

View file

@ -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<UserPrivacy::Key> 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;
}

View file

@ -32,6 +32,7 @@ public:
About,
Birthday,
GiftsAutoSave,
NoPaidMessages,
};
enum class Option {
Everyone,

View file

@ -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<void()> &&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<void()> finish) {
history->sendRequestId = request(MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
MTP_flags(oneFlags),
forwardFrom->input,
MTP_vector<MTPint>(ids),
MTP_vector<MTPlong>(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<UserData*> bot,
not_null<InlineBots::Result*> data,
const SendAction &action,
SendAction action,
std::optional<MsgId> localMessageId,
Fn<void(bool)> 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<SendingAlbum*> 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<SendingAlbum*> 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<SendingAlbum*> 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);

View file

@ -306,7 +306,7 @@ public:
void finishForwarding(const SendAction &action);
void forwardMessages(
Data::ResolvedForwardDraft &&draft,
const SendAction &action,
SendAction action,
FnMut<void()> &&successCallback = nullptr);
void shareContact(
const QString &phone,
@ -368,7 +368,7 @@ public:
void sendInlineResult(
not_null<UserData*> bot,
not_null<InlineBots::Result*> data,
const SendAction &action,
SendAction action,
std::optional<MsgId> localMessageId,
Fn<void(bool)> done = nullptr);
void sendMessageFail(

View file

@ -1078,7 +1078,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector<QColor> &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,

View file

@ -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);
}
}

View file

@ -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<QWidget*> 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<FilterAction>(
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));

View file

@ -817,13 +817,15 @@ CreatePollBox::CreatePollBox(
not_null<Window::SessionController*> controller,
PollData::Flags chosen,
PollData::Flags disabled,
rpl::producer<int> 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::Result> CreatePollBox::submitRequests() const {
@ -1226,10 +1228,11 @@ object_ptr<Ui::RpWidget> 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();

View file

@ -42,6 +42,7 @@ public:
not_null<Window::SessionController*> controller,
PollData::Flags chosen,
PollData::Flags disabled,
rpl::producer<int> 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<SendMenu::Details()> _sendMenuDetails;
rpl::variable<int> _starsRequired;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus;
Fn<rpl::producer<bool>()> _dataIsValidValue;

View file

@ -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*> history)
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeChargeStarsSlider(
QWidget *parent,
not_null<const style::MediaSlider*> sliderStyle,
not_null<const style::FlatLabel*> labelStyle,
int valuesCount,
Fn<int(int)> valueByIndex,
int value,
int maxValue,
Fn<void(int)> valueProgress,
Fn<void(int)> valueFinished) {
auto result = object_ptr<Ui::VerticalLayout>(parent);
const auto raw = result.data();
const auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));
const auto min = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(kStarsMin),
*labelStyle);
const auto max = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(maxValue),
*labelStyle);
const auto current = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(value),
*labelStyle);
min->setTextColorOverride(st::windowSubTextFg->c);
max->setTextColorOverride(st::windowSubTextFg->c);
const auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(
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<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::SessionController*> window,
const Api::UserPrivacy::Rule &value) {
auto controller = std::make_unique<PrivacyExceptionsBoxController>(
&window->session(),
tr::lng_messages_privacy_remove_fee(),
value.always,
std::optional<SpecialRowType>());
auto initBox = [=, controller = controller.get()](
not_null<PeerListBox*> 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<PeerListBox>(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<Ui::PlainShadow>(box));
Ui::AddSkip(inner, st::messagePrivacyTopSkip);
Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle());
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
privacy->newRequirePremiumCurrent() ? kOptionPremium : kOptionAll);
(!allowed()
? kOptionAll
: privacy->newRequirePremiumCurrent()
? kOptionPremium
: privacy->newChargeStarsCurrent()
? kOptionCharge
: kOptionAll));
inner->add(
object_ptr<Ui::Radiobutton>(
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<Ui::Radiobutton>(
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<int> stars;
};
const auto state = std::make_shared<State>();
const auto savedValue = privacy->newChargeStarsCurrent();
if (available) {
Ui::AddDividerText(inner, tr::lng_messages_privacy_charge_about());
const auto chargeWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(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<rpl::lifetime>();
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<Ui::Toast::Instance>;
const auto toast = std::make_shared<WeakToast>();
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<int> SetupChargeSlider(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
int savedValue) {
struct State {
rpl::variable<int> stars;
};
const auto group = !peer->isUser();
const auto state = container->lifetime().make_state<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<int>();
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();
}

View file

@ -169,3 +169,8 @@ private:
void EditMessagesPrivacyBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<int> SetupChargeSlider(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
int savedValue);

View file

@ -441,13 +441,10 @@ void EditFilterBox(
using namespace Window;
return window->isGifPausedAtLeastFor(GifPauseReason::Layer);
};
name->setCustomTextContext([=](Fn<void()> 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<TagState>();
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);

View file

@ -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));
}

View file

@ -537,13 +537,6 @@ void LinkController::addHeader(not_null<Ui::VerticalLayout*> container) {
verticalLayout->add(std::move(icon.widget));
const auto isStatic = _filterTitle.isStatic;
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = &_window->session(),
.customEmojiRepaint = update,
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
verticalLayout->add(
object_ptr<Ui::CenterWrap<>>(
verticalLayout,
@ -559,7 +552,10 @@ void LinkController::addHeader(not_null<Ui::VerticalLayout*> container) {
Ui::Text::WithEntities)),
st::settingsFilterDividerLabel,
st::defaultPopupMenu,
makeContext)),
Core::TextContext({
.session = &_window->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}))),
st::filterLinkDividerLabelPadding);
verticalLayout->geometryValue(

View file

@ -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);
}

View file

@ -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<Ui::FlatLabel*> AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
rpl::producer<TextWithEntities> value,
const Fn<std::any(Fn<void()>)> &makeContext = nullptr) {
const Ui::Text::MarkedContext &context = {}) {
auto widget = object_ptr<Ui::FlatLabel>(
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<Ui::ImportantTooltip*>(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<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto label = object_ptr<Ui::FlatLabel>(
table,
(from
@ -1573,7 +1568,7 @@ void AddStarGiftTable(
? *st.tableValueMessage
: st::giveawayGiftMessage),
st::defaultPopupMenu,
makeContext);
Core::TextContext({ .session = session }));
const auto showBoxLink = [=](not_null<PeerData*> peer) {
return std::make_shared<LambdaClickHandler>([=] {
show->showBox(PrepareShortInfoBox(peer, show));
@ -1591,12 +1586,6 @@ void AddStarGiftTable(
st::giveawayGiftCodeValueMargin);
}
} else if (!entry.description.empty()) {
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto label = object_ptr<Ui::FlatLabel>(
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 &...) {

View file

@ -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);

View file

@ -1154,8 +1154,7 @@ RecoverBox::RecoverBox(
rpl::single(Ui::Text::WrapEmailPattern(pattern)),
Ui::Text::WithEntities),
st::termsContent,
st::defaultPopupMenu,
[=](Fn<void()> update) { return CommonTextContext{ std::move(update) }; })
st::defaultPopupMenu)
, _closeParent(std::move(closeParent)) {
_patternLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
if (_cloudFields.pendingResetDate != 0 || !session) {

View file

@ -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.

View file

@ -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<QChar> &;

View file

@ -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<PeerData*> 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>();
}
_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<const style::PeerListItem*> 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<PeerListController*> 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<PeerListRow> ContactsBoxController::createRow(
return std::make_unique<PeerListRow>(user);
}
RecipientPremiumRequiredError WritePremiumRequiredError(
RecipientMoneyRestrictionError WriteMoneyRestrictionError(
not_null<UserData*> 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<PeerListRow*> row) {
return RecipientRow::ShowLockedError(this, row, _premiumRequiredError);
return RecipientRow::ShowLockedError(
this,
row,
_moneyRestrictionError);
}
void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
@ -836,8 +879,9 @@ void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
bool RecipientRow::ShowLockedError(
not_null<PeerListController*> controller,
not_null<PeerListRow*> row,
Fn<RecipientPremiumRequiredError(not_null<UserData*>)> error) {
if (!static_cast<RecipientRow*>(row.get())->locked()) {
Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> error) {
const auto recipient = static_cast<RecipientRow*>(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<Row>(
history,
_premiumRequiredError ? &computeListSt().item : nullptr);
_moneyRestrictionError ? &computeListSt().item : nullptr);
return result;
}
@ -1093,25 +1137,61 @@ auto ChooseTopicBoxController::createRow(not_null<Data::ForumTopic*> topic)
return skip ? nullptr : std::make_unique<Row>(topic);
};
void PaintPremiumRequiredLock(
void PaintRestrictionBadge(
Painter &p,
not_null<const style::PeerListItem*> 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);
}

View file

@ -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<UserData*> user);
struct RestrictionBadgeCache {
int paletteVersion = 0;
int stars = 0;
QImage badge;
};
void PaintRestrictionBadge(
Painter &p,
not_null<const style::PeerListItem*> 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<PeerListController*> controller,
not_null<PeerListRow*> row,
Fn<RecipientPremiumRequiredError(not_null<UserData*>)> error);
Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> 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> _restriction;
};
void TrackPremiumRequiredChanges(
void TrackMessageMoneyRestrictionsChanges(
not_null<PeerListController*> controller,
rpl::lifetime &lifetime);
@ -261,8 +283,8 @@ struct ChooseRecipientArgs {
FnMut<void(not_null<Data::Thread*>)> callback;
Fn<bool(not_null<Data::Thread*>)> filter;
using PremiumRequiredError = RecipientPremiumRequiredError;
Fn<PremiumRequiredError(not_null<UserData*>)> premiumRequiredError;
using MoneyRestrictionError = RecipientMoneyRestrictionError;
Fn<MoneyRestrictionError(not_null<UserData*>)> moneyRestrictionError;
};
class ChooseRecipientBoxController
@ -290,8 +312,8 @@ private:
const not_null<Main::Session*> _session;
FnMut<void(not_null<Data::Thread*>)> _callback;
Fn<bool(not_null<Data::Thread*>)> _filter;
Fn<RecipientPremiumRequiredError(
not_null<UserData*>)> _premiumRequiredError;
Fn<RecipientMoneyRestrictionError(
not_null<UserData*>)> _moneyRestrictionError;
};
@ -371,11 +393,3 @@ private:
Fn<bool(not_null<Data::ForumTopic*>)> _filter;
};
void PaintPremiumRequiredLock(
Painter &p,
not_null<const style::PeerListItem*> st,
int x,
int y,
int outerWidth,
int size);

View file

@ -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<PeerData*> peer, bool locked);
ForbiddenRow(
not_null<PeerData*> peer,
not_null<const style::PeerListItem*> 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<const style::PeerListItem*> _lockSt;
QImage _disabledFrame;
InMemoryKey _userpicKey;
int _paletteVersion = 0;
std::shared_ptr<Restriction> _restriction;
};
@ -81,6 +107,9 @@ public:
[[nodiscard]] rpl::producer<int> selectedValue() const {
return _selected.value();
}
[[nodiscard]] rpl::producer<int> starsToSend() const {
return _starsToSend.value();
}
void send(
std::vector<not_null<PeerData*>> list,
@ -89,10 +118,16 @@ public:
private:
void appendRow(not_null<UserData*> user);
[[nodiscard]] std::unique_ptr<PeerListRow> createRow(
[[nodiscard]] std::unique_ptr<ForbiddenRow> createRow(
not_null<UserData*> user) const;
[[nodiscard]] bool canInvite(not_null<PeerData*> peer) const;
void send(
std::vector<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> close,
Api::SendOptions options);
void setSimpleCover();
void setComplexCover();
@ -101,8 +136,11 @@ private:
const std::vector<not_null<UserData*>> &_users;
const bool _can = false;
rpl::variable<int> _selected;
rpl::variable<int> _starsToSend;
bool _sending = false;
rpl::lifetime _paymentCheckLifetime;
};
base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
@ -256,11 +294,17 @@ Main::Session &InviteForbiddenController::session() const {
return _peer->session();
}
ForbiddenRow::ForbiddenRow(not_null<PeerData*> peer, bool locked)
ForbiddenRow::ForbiddenRow(
not_null<PeerData*> peer,
not_null<const style::PeerListItem*> 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>();
}
_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<PeerListRow*> raw) {
const auto row = static_cast<ForbiddenRow*>(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<PeerListRow*> row) {
const auto checked = row->checked();
delegate()->peerListSetRowChecked(row, !checked);
_selected = _selected.current() + (checked ? -1 : 1);
const auto r = static_cast<ForbiddenRow*>(row.get())->restriction();
if (r.starsPerMessage) {
_starsToSend = _starsToSend.current()
+ (checked ? -r.starsPerMessage : r.starsPerMessage);
}
}
void InviteForbiddenController::appendRow(not_null<UserData*> user) {
@ -473,6 +616,9 @@ void InviteForbiddenController::appendRow(not_null<UserData*> 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<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> close) {
if (_sending || list.empty()) {
send(list, show, close, {});
}
void InviteForbiddenController::send(
std::vector<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> 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<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
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<PeerListRow> InviteForbiddenController::createRow(
std::unique_ptr<ForbiddenRow> InviteForbiddenController::createRow(
not_null<UserData*> user) const {
const auto locked = _can && !canInvite(user);
return std::make_unique<ForbiddenRow>(user, locked);
const auto lockSt = &computeListSt().item;
return std::make_unique<ForbiddenRow>(user, lockSt, locked);
}
} // namespace
@ -584,8 +794,8 @@ void AddParticipantsBoxController::subscribeToMigration() {
}
void AddParticipantsBoxController::rowClicked(not_null<PeerListRow*> 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();

View file

@ -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();
}

View file

@ -219,6 +219,33 @@ void SaveSlowmodeSeconds(
api->registerModifyRequest(key, requestId);
}
void SaveStarsPerMessage(
not_null<ChannelData*> channel,
int starsPerMessage,
Fn<void()> 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<ChannelData*> 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<PeerData*> peer) {
return false;
}
}
void ShowEditChatPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer) {
ShowEditPermissions(navigation, peer);
}

View file

@ -56,3 +56,7 @@ private:
not_null<PeerData*> _peer;
};
void ShowEditChatPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);

View file

@ -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<rpl::variable<float64>>(
session().credits().rateValue(_peer));
@ -994,10 +994,7 @@ void Controller::rowClicked(not_null<PeerListRow*> 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<Ui::CenterWrap<Ui::FlatLabel>>(
box,
@ -1484,8 +1481,12 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
? tr::lng_group_invite_copied(tr::now)
: copied);
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@ -1503,6 +1504,8 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
@ -1530,7 +1533,7 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
};
auto filterCallback = [](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -1539,9 +1542,10 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
auto object = Box<ShareBox>(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;

View file

@ -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<Ui::FlatLabel>(
labels,
@ -942,9 +942,7 @@ rpl::producer<int> AddBoostsUnrestrictSlider(
const auto boostsUnrestrict = lifetime.make_state<rpl::variable<int>>(
channel ? channel->boostsUnrestrict() : 0);
container->add(
object_ptr<Ui::BoxContentDivider>(container),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
Ui::AddSkip(container);
auto enabled = boostsUnrestrict->value(
) | rpl::map(_1 > 0);
@ -1008,19 +1006,20 @@ rpl::producer<int> AddBoostsUnrestrictWrapped(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
wrap->toggleOn(rpl::duplicate(shown), anim::type::normal);
wrap->toggleOn(std::move(shown), anim::type::normal);
wrap->finishAnimating();
auto result = AddBoostsUnrestrictSlider(wrap->entity(), peer);
const auto divider = container->add(
const auto inner = wrap->entity();
auto result = AddBoostsUnrestrictSlider(inner, peer);
const auto skip = st::defaultVerticalListSkip;
const auto divider = inner->add(
object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
container,
object_ptr<Ui::BoxContentDivider>(container),
QMargins{ 0, st::infoProfileSkip, 0, st::infoProfileSkip }));
divider->toggleOn(rpl::combine(
std::move(shown),
rpl::duplicate(result),
!rpl::mappers::_1 || !rpl::mappers::_2));
inner,
object_ptr<Ui::BoxContentDivider>(inner),
QMargins{ 0, skip, 0, skip }));
divider->toggleOn(rpl::duplicate(result) | rpl::map(!rpl::mappers::_1));
divider->finishAnimating();
return result;
@ -1159,7 +1158,43 @@ void ShowEditPeerPermissionsBox(
rpl::variable<int> slowmodeSeconds;
rpl::variable<int> boostsUnrestrict;
rpl::variable<bool> hasSendRestrictions;
rpl::variable<int> starsPerMessage;
};
const auto state = inner->lifetime().make_state<State>();
const auto channel = peer->asChannel();
const auto available = channel && channel->paidMessagesAvailable();
Ui::AddSkip(inner);
Ui::AddDivider(inner);
auto charging = (Ui::SettingsButton*)nullptr;
if (available) {
Ui::AddSkip(inner);
const auto starsPerMessage = peer->isChannel()
? peer->asChannel()->starsPerMessage()
: 0;
charging = inner->add(object_ptr<Ui::SettingsButton>(
inner,
tr::lng_rights_charge_stars(),
st::settingsButtonNoIcon));
charging->toggleOn(rpl::single(starsPerMessage > 0));
Ui::AddSkip(inner);
Ui::AddDividerText(inner, tr::lng_rights_charge_stars_about());
const auto chargeWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
chargeWrap->toggleOn(charging->toggledValue());
chargeWrap->finishAnimating();
const auto chargeInner = chargeWrap->entity();
Ui::AddSkip(chargeInner);
state->starsPerMessage = SetupChargeSlider(
chargeInner,
peer,
starsPerMessage);
}
static constexpr auto kSendRestrictions = Flag::EmbedLinks
| Flag::SendGames
| Flag::SendGifs
@ -1173,7 +1208,6 @@ void ShowEditPeerPermissionsBox(
| Flag::SendVoiceMessages
| Flag::SendFiles
| Flag::SendOther;
const auto state = inner->lifetime().make_state<State>();
state->hasSendRestrictions = ((restrictions & kSendRestrictions) != 0)
|| (peer->isChannel() && peer->asChannel()->slowmodeSeconds() > 0);
state->boostsUnrestrict = AddBoostsUnrestrictWrapped(
@ -1214,10 +1248,14 @@ void ShowEditPeerPermissionsBox(
const auto boostsUnrestrict = hasRestrictions
? state->boostsUnrestrict.current()
: 0;
const auto starsPerMessage = (charging && charging->toggled())
? state->starsPerMessage.current()
: 0;
done({
restrictions,
slowmodeSeconds,
boostsUnrestrict,
starsPerMessage,
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });

View file

@ -39,6 +39,7 @@ struct EditPeerPermissionsBoxResult final {
ChatRestrictions rights;
int slowmodeSeconds = 0;
int boostsUnrestrict = 0;
int starsPerMessage = 0;
};
void ShowEditPeerPermissionsBox(

View file

@ -363,12 +363,15 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
};
auto factory = [=](QStringView data, Fn<void()> update)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
auto context = Core::TextContext({
.session = session,
});
context.customEmojiFactory = [=](
QStringView data,
const Ui::Text::MarkedContext &context
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto id = Data::ParseCustomEmojiData(data);
auto result = owner->customEmojiManager().create(
data,
std::move(update));
auto result = Ui::Text::MakeCustomEmoji(data, context);
if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
return std::make_unique<MaybeDisabledEmoji>(
std::move(result),
@ -377,12 +380,10 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
using namespace Ui::Text;
return std::make_unique<FirstFrameEmoji>(std::move(result));
};
raw->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
});
}, customEmojiPaused, customEmojiPaused, std::move(factory));
raw->setCustomTextContext(
std::move(context),
customEmojiPaused,
customEmojiPaused);
const auto callback = args.callback;
const auto isCustom = [=](DocumentId id) {

View file

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_credits.h"
#include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "core/ui_integration.h" // TextContext
#include "data/components/credits.h"
#include "data/data_credits.h"
#include "data/data_photo.h"
@ -511,32 +511,28 @@ TextWithEntities CreditsEmoji(not_null<Main::Session*> session) {
}
TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::starIconSmall,
st::starIconSmallPadding,
true),
return Ui::Text::IconEmoji(
&st::starIconEmoji,
QString(QChar(0x2B50)));
}
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
Text::MarkedContext context,
const style::FlatLabel &st,
const style::color *textFg) {
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st);
context.repaint = [=] { buttonLabel->update(); };
rpl::duplicate(
text
) | rpl::filter([=](const TextWithEntities &text) {
return !text.text.isEmpty();
}) | rpl::start_with_next([=](const TextWithEntities &text) {
buttonLabel->setMarkedText(
text,
context([=] { buttonLabel->update(); }));
buttonLabel->setMarkedText(text, context);
}, buttonLabel->lifetime());
if (textFg) {
buttonLabel->setTextColorOverride((*textFg)->c);
@ -565,15 +561,12 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<Main::Session*> session,
const style::FlatLabel &st,
const style::color *textFg) {
return SetButtonMarkedLabel(button, text, [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = update,
};
}, st, textFg);
return SetButtonMarkedLabel(button, text, Core::TextContext({
.session = session,
}), st, textFg);
}
void SendStarGift(
void SendStarsForm(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) {

View file

@ -41,7 +41,7 @@ void SendCreditsBox(
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
Text::MarkedContext context,
const style::FlatLabel &st,
const style::color *textFg = nullptr);
@ -52,7 +52,7 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
const style::FlatLabel &st,
const style::color *textFg = nullptr);
void SendStarGift(
void SendStarsForm(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done);

View file

@ -59,9 +59,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include <QtCore/QMimeData>
@ -722,6 +722,18 @@ void SendFilesBox::openDialogToAddFileToAlbum() {
crl::guard(this, callback));
}
void SendFilesBox::refreshMessagesCount() {
const auto way = _sendWay.current();
const auto withCaption = _list.canAddCaption(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos());
const auto withComment = !withCaption
&& _caption
&& !_caption->isHidden()
&& !_caption->getTextWithTags().text.isEmpty();
_messagesCount = _list.files.size() + (withComment ? 1 : 0);
}
void SendFilesBox::refreshButtons() {
clearButtons();
@ -730,6 +742,15 @@ void SendFilesBox::refreshButtons() {
? tr::lng_send_button()
: tr::lng_create_group_next()),
[=] { send({}); });
refreshMessagesCount();
const auto perMessage = _captionToPeer
? _captionToPeer->starsPerMessageChecked()
: 0;
if (perMessage > 0) {
_send->setText(PaidSendButtonText(_messagesCount.value(
) | rpl::map(rpl::mappers::_1 * perMessage)));
}
if (_sendType == Api::SendType::Normal) {
SendMenu::SetupMenuAndShortcuts(
_send,
@ -846,10 +867,9 @@ void SendFilesBox::refreshPriceTag() {
QString(),
st::paidTagLabel);
std::move(text) | rpl::start_with_next([=](TextWithEntities &&text) {
label->setMarkedText(text, Core::MarkedTextContext{
label->setMarkedText(text, Core::TextContext({
.session = session,
.customEmojiRepaint = [=] { label->update(); },
});
}));
}, label->lifetime());
label->show();
label->sizeValue() | rpl::start_with_next([=](QSize size) {
@ -1489,6 +1509,7 @@ void SendFilesBox::setupCaption() {
_caption->changes()
) | rpl::start_with_next([=] {
checkCharsLimitation();
refreshMessagesCount();
}, _caption->lifetime());
}

View file

@ -246,6 +246,7 @@ private:
void addPreparedAsyncFile(Ui::PreparedFile &&file);
void checkCharsLimitation();
void refreshMessagesCount();
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor);
@ -261,6 +262,7 @@ private:
Ui::PreparedList _list;
std::optional<int> _removingIndex;
rpl::variable<int> _messagesCount;
SendFilesLimits _limits = {};
Fn<MenuDetails()> _sendMenuDetails;

View file

@ -123,12 +123,13 @@ private:
Ui::RoundImageCheckbox checkbox;
Ui::Text::String name;
Ui::Animations::Simple nameActive;
bool locked = false;
Api::MessageMoneyRestriction restriction;
RestrictionBadgeCache badgeCache;
};
void invalidateCache();
bool showLockedError(not_null<Chat*> chat);
void refreshLockedRows();
void refreshRestrictedRows();
[[nodiscard]] int displayedChatsCount() const;
[[nodiscard]] not_null<Data::Thread*> chatThread(
@ -137,7 +138,7 @@ private:
void paintChat(Painter &p, not_null<Chat*> chat, int index);
void updateChat(not_null<PeerData*> peer);
void updateChatName(not_null<Chat*> chat);
void initChatLocked(not_null<Chat*> chat);
void initChatRestriction(not_null<Chat*> chat);
void repaintChat(not_null<PeerData*> peer);
int chatIndex(not_null<PeerData*> peer) const;
void repaintChatAtIndex(int index);
@ -517,9 +518,19 @@ void ShareBox::keyPressEvent(QKeyEvent *e) {
SendMenu::Details ShareBox::sendMenuDetails() const {
const auto selected = _inner->selected();
const auto type = ranges::all_of(
selected | ranges::views::transform(&Data::Thread::peer),
HistoryView::CanScheduleUntilOnline)
const auto hasPaid = [&] {
for (const auto &thread : selected) {
if (thread->peer()->starsPerMessageChecked()) {
return true;
}
}
return false;
}();
const auto type = hasPaid
? SendMenu::Type::SilentOnly
: ranges::all_of(
selected | ranges::views::transform(&Data::Thread::peer),
HistoryView::CanScheduleUntilOnline)
? SendMenu::Type::ScheduledToUser
: (selected.size() == 1 && selected.front()->peer()->isSelf())
? SendMenu::Type::Reminder
@ -614,6 +625,9 @@ void ShareBox::createButtons() {
showMenu(send);
}
}, send->lifetime());
send->setText(PaidSendButtonText(
_starsToSend.value(),
tr::lng_share_confirm()));
} else if (_descriptor.copyCallback) {
addButton(_copyLinkText.value(), [=] { copyLink(); });
}
@ -657,6 +671,73 @@ void ShareBox::innerSelectedChanged(
}
void ShareBox::submit(Api::SendOptions options) {
_submitLifetime.destroy();
auto threads = _inner->selected();
const auto weak = Ui::MakeWeak(this);
const auto field = _comment->entity();
auto comment = field->getTextWithAppliedMarkdown();
const auto checkPaid = [=] {
if (!_descriptor.countMessagesCallback) {
return true;
}
const auto withPaymentApproved = crl::guard(weak, [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
submit(copy);
});
const auto messagesCount = _descriptor.countMessagesCallback(
comment);
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &thread : threads) {
const auto peer = thread->peer();
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()) {
_descriptor.session->changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
if (waiting.contains(update.peer)) {
withPaymentApproved(alreadyApproved);
}
}, _submitLifetime);
if (!_descriptor.session->credits().loaded()) {
_descriptor.session->credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
withPaymentApproved(alreadyApproved);
}, _submitLifetime);
}
return false;
} else if (totalStars > alreadyApproved) {
const auto show = uiShow();
const auto session = _descriptor.session;
const auto sessionShow = Main::MakeSessionShow(show, session);
const auto scheduleBoxSt = _descriptor.st.scheduleBox.get();
ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{
.messages = messagesCount,
.stars = totalStars,
}, [=] { withPaymentApproved(totalStars); }, PaidConfirmStyles{
.label = (scheduleBoxSt
? scheduleBoxSt->chooseDateTimeArgs.labelStyle
: nullptr),
.checkbox = _descriptor.st.checkbox,
});
return false;
}
return true;
};
if (const auto onstack = _descriptor.submitCallback) {
const auto forwardOptions = (_forwardOptions.captionsCount
&& _forwardOptions.dropCaptions)
@ -665,8 +746,9 @@ void ShareBox::submit(Api::SendOptions options) {
? Data::ForwardOptions::NoSenderNames
: Data::ForwardOptions::PreserveInfo;
onstack(
_inner->selected(),
_comment->entity()->getTextWithAppliedMarkdown(),
std::move(threads),
checkPaid,
std::move(comment),
options,
forwardOptions);
}
@ -686,9 +768,23 @@ void ShareBox::selectedChanged() {
_comment->toggle(_hasSelected, anim::type::normal);
_comment->resizeToWidth(st::boxWideWidth);
}
computeStarsCount();
update();
}
void ShareBox::computeStarsCount() {
auto perMessage = 0;
for (const auto &thread : _inner->selected()) {
perMessage += thread->peer()->starsPerMessageChecked();
}
const auto messagesCount = _descriptor.countMessagesCallback
? _descriptor.countMessagesCallback(_comment
? _comment->entity()->getTextWithTags()
: TextWithTags())
: 0;
_starsToSend = perMessage * messagesCount;
}
void ShareBox::scrollTo(Ui::ScrollToRequest request) {
scrollToY(request.ymin, request.ymax);
//auto scrollTop = scrollArea()->scrollTop(), scrollBottom = scrollTop + scrollArea()->height();
@ -726,13 +822,13 @@ ShareBox::Inner::Inner(
_rowHeight = st::shareRowHeight;
setAttribute(Qt::WA_OpaquePaintEvent);
if (_descriptor.premiumRequiredError) {
if (_descriptor.moneyRestrictionError) {
const auto session = _descriptor.session;
rpl::merge(
Data::AmPremiumValue(session) | rpl::to_empty,
session->api().premium().somePremiumRequiredResolved()
session->api().premium().someMessageMoneyRestrictionsResolved()
) | rpl::start_with_next([=] {
refreshLockedRows();
refreshRestrictedRows();
}, lifetime());
}
@ -793,38 +889,36 @@ void ShareBox::Inner::invalidateCache() {
}
bool ShareBox::Inner::showLockedError(not_null<Chat*> chat) {
if (!chat->locked) {
if (!chat->restriction.premiumRequired) {
return false;
}
::Settings::ShowPremiumPromoToast(
Main::MakeSessionShow(_show, _descriptor.session),
ChatHelpers::ResolveWindowDefault(),
_descriptor.premiumRequiredError(chat->peer->asUser()).text,
_descriptor.moneyRestrictionError(chat->peer->asUser()).text,
u"require_premium"_q);
return true;
}
void ShareBox::Inner::refreshLockedRows() {
void ShareBox::Inner::refreshRestrictedRows() {
auto changed = false;
for (const auto &[peer, data] : _dataMap) {
const auto history = data->history;
const auto locked = (Api::ResolveRequiresPremiumToWrite(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history
) == Api::RequirePremiumState::Yes);
if (data->locked != locked) {
data->locked = locked;
history);
if (data->restriction != restriction) {
data->restriction = restriction;
changed = true;
}
}
for (const auto &data : d_byUsernameFiltered) {
const auto history = data->history;
const auto locked = (Api::ResolveRequiresPremiumToWrite(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history
) == Api::RequirePremiumState::Yes);
if (data->locked != locked) {
data->locked = locked;
history);
if (data->restriction != restriction) {
data->restriction = restriction;
changed = true;
}
}
@ -891,14 +985,14 @@ void ShareBox::Inner::updateChatName(not_null<Chat*> chat) {
chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions());
}
void ShareBox::Inner::initChatLocked(not_null<Chat*> chat) {
if (_descriptor.premiumRequiredError) {
void ShareBox::Inner::initChatRestriction(not_null<Chat*> chat) {
if (_descriptor.moneyRestrictionError) {
const auto history = chat->history;
if (Api::ResolveRequiresPremiumToWrite(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history
) == Api::RequirePremiumState::Yes) {
chat->locked = true;
history);
if (restriction || restriction.known) {
chat->restriction = restriction;
}
}
}
@ -1024,14 +1118,15 @@ void ShareBox::Inner::loadProfilePhotos() {
void ShareBox::Inner::preloadUserpic(not_null<Dialogs::Entry*> entry) {
entry->chatListPreloadData();
const auto history = entry->asHistory();
if (!_descriptor.premiumRequiredError || !history) {
if (!_descriptor.moneyRestrictionError || !history) {
return;
} else if (Api::ResolveRequiresPremiumToWrite(
history->peer,
history
) == Api::RequirePremiumState::Unknown) {
const auto user = history->peer->asUser();
_descriptor.session->api().premium().resolvePremiumRequired(user);
} else if (!Api::ResolveMessageMoneyRestrictions(
history->peer,
history).known) {
if (const auto user = history->peer->asUser()) {
const auto api = &_descriptor.session->api();
api->premium().resolveMessageMoneyRestrictions(user);
}
}
}
@ -1054,7 +1149,7 @@ auto ShareBox::Inner::getChat(not_null<Dialogs::Row*> row)
repaintChat(peer);
}));
updateChatName(i->second.get());
initChatLocked(i->second.get());
initChatRestriction(i->second.get());
row->attached = i->second.get();
return i->second.get();
}
@ -1088,10 +1183,12 @@ void ShareBox::Inner::paintChat(
auto photoTop = st::sharePhotoTop;
chat->checkbox.paint(p, x + photoLeft, y + photoTop, outerWidth);
if (chat->locked) {
PaintPremiumRequiredLock(
if (chat->restriction) {
PaintRestrictionBadge(
p,
&_st.item,
chat->restriction.starsPerMessage,
chat->badgeCache,
x + photoLeft,
y + photoTop,
outerWidth,
@ -1446,7 +1543,7 @@ void ShareBox::Inner::peopleReceived(
_st.item,
[=] { repaintChat(peer); }));
updateChatName(d_byUsernameFiltered.back().get());
initChatLocked(d_byUsernameFiltered.back().get());
initChatRestriction(d_byUsernameFiltered.back().get());
}
}
};
@ -1499,6 +1596,15 @@ ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(
};
}
ShareBox::CountMessagesCallback ShareBox::DefaultForwardCountMessages(
not_null<History*> history,
MessageIdsList msgIds) {
return [=](const TextWithTags &comment) {
const auto items = history->owner().idsToItems(msgIds);
return int(items.size()) + (comment.empty() ? 0 : 1);
};
}
ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
std::shared_ptr<Ui::Show> show,
not_null<History*> history,
@ -1510,12 +1616,14 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
const auto state = std::make_shared<State>();
return [=](
std::vector<not_null<Data::Thread*>> &&result,
TextWithTags &&comment,
Fn<bool()> checkPaid,
TextWithTags comment,
Api::SendOptions options,
Data::ForwardOptions forwardOptions) {
if (!state->requests.empty()) {
return; // Share clicked already.
}
const auto items = history->owner().idsToItems(msgIds);
const auto existingIds = history->owner().itemsToIds(items);
if (existingIds.empty() || result.empty()) {
@ -1528,6 +1636,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
if (error.error) {
show->showBox(MakeSendErrorBox(error, result.size() > 1));
return;
} else if (!checkPaid()) {
return;
}
using Flag = MTPmessages_ForwardMessages::Flag;
@ -1576,6 +1686,12 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
: topicRootId;
const auto peer = thread->peer();
const auto threadHistory = thread->owningHistory();
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
options.starsApproved);
if (starsPaid) {
options.starsApproved -= starsPaid;
}
histories.sendRequest(threadHistory, requestType, [=](
Fn<void()> finish) {
const auto session = &threadHistory->session();
@ -1587,7 +1703,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
: Flag(0))
| (options.shortcutId
? Flag::f_quick_reply_shortcut
: Flag(0));
: Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag());
threadHistory->sendRequestId = api.request(
MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
@ -1599,7 +1716,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
MTP_int(options.scheduled),
MTP_inputPeerEmpty(), // send_as
Data::ShortcutIdToMTP(session, options.shortcutId),
MTP_int(videoTimestamp.value_or(0))
MTP_int(videoTimestamp.value_or(0)),
MTP_long(starsPaid)
)).done([=](const MTPUpdates &updates, mtpRequestId reqId) {
threadHistory->session().api().applyUpdates(updates);
state->requests.remove(reqId);
@ -1621,7 +1739,11 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
finish();
}).fail([=](const MTP::Error &error) {
if (error.type() == u"VOICE_MESSAGES_FORBIDDEN"_q) {
const auto type = error.type();
if (type.startsWith(u"ALLOW_PAYMENT_REQUIRED_"_q)) {
show->showToast(u"Payment requirements changed. "
"Please, try again."_q);
} else if (type == u"VOICE_MESSAGES_FORBIDDEN"_q) {
show->showToast(
tr::lng_restricted_send_voice_messages(
tr::now,
@ -1660,6 +1782,7 @@ ShareBoxStyleOverrides DarkShareBoxStyle() {
.comment = &st::groupCallShareBoxComment,
.peerList = &st::groupCallShareBoxList,
.label = &st::groupCallField,
.checkbox = &st::groupCallCheckbox,
.scheduleBox = std::make_shared<ScheduleBoxStyleArgs>(schedule()),
};
}
@ -1716,7 +1839,7 @@ void FastShareMessage(
const auto requiresInline = item->requiresSendInlineRight();
auto filterCallback = [=](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -1731,6 +1854,9 @@ void FastShareMessage(
show->show(Box<ShareBox>(ShareBox::Descriptor{
.session = session,
.copyCallback = std::move(copyLinkCallback),
.countMessagesCallback = ShareBox::DefaultForwardCountMessages(
history,
msgIds),
.submitCallback = ShareBox::DefaultForwardCallback(
show,
history,
@ -1742,7 +1868,7 @@ void FastShareMessage(
.captionsCount = ItemsForwardCaptionsCount(items),
.show = !hasOnlyForcedForwardedInfo,
},
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
}), Ui::LayerOption::CloseOther);
}
@ -1770,8 +1896,12 @@ void FastShareLink(
QGuiApplication::clipboard()->setText(url);
show->showToast(tr::lng_background_link_copied(tr::now));
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<::Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
::Data::ForwardOptions) {
@ -1788,6 +1918,8 @@ void FastShareLink(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
@ -1815,7 +1947,7 @@ void FastShareLink(
};
auto filterCallback = [](not_null<::Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -1825,16 +1957,17 @@ void FastShareLink(
Box<ShareBox>(ShareBox::Descriptor{
.session = &show->session(),
.copyCallback = std::move(copyCallback),
.countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.st = st,
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
}),
Ui::LayerOption::KeepOther,
anim::type::normal);
}
auto SharePremiumRequiredError()
-> Fn<RecipientPremiumRequiredError(not_null<UserData*>)> {
return WritePremiumRequiredError;
auto ShareMessageMoneyRestrictionError()
-> Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> {
return WriteMoneyRestrictionError;
}

View file

@ -66,6 +66,7 @@ struct ShareBoxStyleOverrides {
const style::InputField *comment = nullptr;
const style::PeerList *peerList = nullptr;
const style::InputField *label = nullptr;
const style::Checkbox *checkbox = nullptr;
std::shared_ptr<HistoryView::ScheduleBoxStyleArgs> scheduleBox;
};
[[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle();
@ -87,20 +88,25 @@ void FastShareLink(
const QString &url,
ShareBoxStyleOverrides st = {});
struct RecipientPremiumRequiredError;
[[nodiscard]] auto SharePremiumRequiredError()
-> Fn<RecipientPremiumRequiredError(not_null<UserData*>)>;
struct RecipientMoneyRestrictionError;
[[nodiscard]] auto ShareMessageMoneyRestrictionError()
-> Fn<RecipientMoneyRestrictionError(not_null<UserData*>)>;
class ShareBox final : public Ui::BoxContent {
public:
using CopyCallback = Fn<void()>;
using CountMessagesCallback = Fn<int(const TextWithTags&)>;
using SubmitCallback = Fn<void(
std::vector<not_null<Data::Thread*>>&&,
Fn<bool()> checkPaid,
TextWithTags&&,
Api::SendOptions,
Data::ForwardOptions)>;
using FilterCallback = Fn<bool(not_null<Data::Thread*>)>;
[[nodiscard]] static auto DefaultForwardCountMessages(
not_null<History*> history,
MessageIdsList msgIds) -> CountMessagesCallback;
[[nodiscard]] static SubmitCallback DefaultForwardCallback(
std::shared_ptr<Ui::Show> show,
not_null<History*> history,
@ -110,6 +116,7 @@ public:
struct Descriptor {
not_null<Main::Session*> session;
CopyCallback copyCallback;
CountMessagesCallback countMessagesCallback;
SubmitCallback submitCallback;
FilterCallback filterCallback;
object_ptr<Ui::RpWidget> bottomWidget = { nullptr };
@ -123,8 +130,9 @@ public:
bool show = false;
} forwardOptions;
using PremiumRequiredError = RecipientPremiumRequiredError;
Fn<PremiumRequiredError(not_null<UserData*>)> premiumRequiredError;
using MoneyRestrictionError = RecipientMoneyRestrictionError;
Fn<MoneyRestrictionError(
not_null<UserData*>)> moneyRestrictionError;
};
ShareBox(QWidget*, Descriptor &&descriptor);
@ -149,6 +157,7 @@ private:
void needSearchByUsername();
void applyFilterUpdate(const QString &query);
void selectedChanged();
void computeStarsCount();
void createButtons();
int getTopScrollSkip() const;
int getBottomScrollSkip() const;
@ -180,6 +189,7 @@ private:
bool _hasSelected = false;
rpl::variable<QString> _copyLinkText;
rpl::variable<int> _starsToSend;
base::Timer _searchTimer;
QString _peopleQuery;
@ -195,5 +205,6 @@ private:
PeopleQueries _peopleQueries;
Ui::Animations::Simple _scrollAnimation;
rpl::lifetime _submitLifetime;
};

View file

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_credits.h"
#include "data/data_document.h"
@ -80,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/slide_wrap.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
@ -106,6 +108,7 @@ constexpr auto kSentToastDuration = 3 * crl::time(1000);
constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
constexpr auto kCrossfadeDuration = crl::time(400);
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
using namespace HistoryView;
using namespace Info::PeerGifts;
@ -126,6 +129,7 @@ struct GiftDetails {
uint64 randomId = 0;
bool anonymous = false;
bool upgraded = false;
bool byStars = false;
};
class PreviewDelegate final : public DefaultElementDelegate {
@ -227,7 +231,7 @@ auto GenerateGiftMedia(
TextWithEntities text,
QMargins margins = {},
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {}) {
Ui::Text::MarkedContext context = {}) {
if (text.empty()) {
return;
}
@ -236,7 +240,7 @@ auto GenerateGiftMedia(
margins,
st::defaultTextStyle,
links,
context));
std::move(context)));
};
const auto sticker = [=] {
@ -306,10 +310,10 @@ auto GenerateGiftMedia(
auto description = data.text.empty()
? std::move(textFallback)
: data.text;
const auto context = Core::MarkedTextContext{
const auto context = Core::TextContext({
.session = &parent->history()->session(),
.customEmojiRepaint = [parent] { parent->repaint(); },
};
.repaint = [parent] { parent->repaint(); },
});
pushText(
std::move(title),
st::giftBoxPreviewTitlePadding,
@ -495,7 +499,14 @@ void PreviewWrap::prepare(rpl::producer<GiftDetails> details) {
std::move(details) | rpl::start_with_next([=](GiftDetails details) {
const auto &descriptor = details.descriptor;
const auto cost = v::match(descriptor, [&](GiftTypePremium data) {
return FillAmountAndCurrency(data.cost, data.currency, true);
const auto stars = (details.byStars && data.stars)
? data.stars
: (data.currency == kCreditsCurrency)
? data.cost
: 0;
return stars
? tr::lng_gift_stars_title(tr::now, lt_count, stars)
: FillAmountAndCurrency(data.cost, data.currency, true);
}, [&](GiftTypeStars data) {
const auto stars = data.info.stars
+ (details.upgraded ? data.info.starsToUpgrade : 0);
@ -622,14 +633,27 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
list.reserve(options.size());
auto minMonthsGift = GiftTypePremium();
for (const auto &option : options) {
list.push_back({
.cost = option.cost,
.currency = option.currency,
.months = option.months,
});
if (!minMonthsGift.months
|| option.months < minMonthsGift.months) {
minMonthsGift = list.back();
if (option.currency != kCreditsCurrency) {
list.push_back({
.cost = option.cost,
.currency = option.currency,
.months = option.months,
});
if (!minMonthsGift.months
|| option.months < minMonthsGift.months) {
minMonthsGift = list.back();
}
}
}
for (const auto &option : options) {
if (option.currency == kCreditsCurrency) {
const auto i = ranges::find(
list,
option.months,
&GiftTypePremium::months);
if (i != end(list)) {
i->stars = option.cost;
}
}
}
for (auto &gift : list) {
@ -735,15 +759,11 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
}
auto &manager = session->data().customEmojiManager();
auto result = Text::String();
const auto context = Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [] {},
};
result.setMarkedText(
st::semiboldTextStyle,
manager.creditsEmoji().append(QString::number(price)),
kMarkupTextOptions,
context);
Core::TextContext({ .session = session }));
return result;
}
@ -1103,16 +1123,35 @@ void SendGift(
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDetails &details,
Fn<void(Payments::CheckoutResult)> done) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
v::match(details.descriptor, [&](const GiftTypePremium &gift) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
if (details.byStars && gift.stars) {
auto invoice = Payments::InvoicePremiumGiftCode{
.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
},
.currency = Ui::kCreditsCurrency,
.randomId = details.randomId,
.amount = uint64(gift.stars),
.storeQuantity = 1,
.users = 1,
.months = gift.months,
};
Payments::CheckoutProcess::Start(
std::move(invoice),
done,
processNonPanelPaymentFormFactory);
} else {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}
}, [&](const GiftTypeStars &gift) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.info.id,
.randomId = details.randomId,
@ -1279,12 +1318,6 @@ void AddUpgradeButton(
button->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next(toggled, button->lifetime());
const auto makeContext = [session](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto star = session->data().customEmojiManager().creditsEmoji();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
@ -1296,7 +1329,7 @@ void AddUpgradeButton(
Text::WithEntities),
st::boxLabel,
st::defaultPopupMenu,
std::move(makeContext));
Core::TextContext({ .session = session }));
label->show();
label->setAttribute(Qt::WA_TransparentForMouseEvents);
button->widthValue() | rpl::start_with_next([=](int outer) {
@ -1424,6 +1457,7 @@ void SendGiftBox(
struct State {
rpl::variable<GiftDetails> details;
rpl::variable<bool> messageAllowed;
std::shared_ptr<Data::DocumentMedia> media;
bool submitting = false;
};
@ -1432,13 +1466,25 @@ void SendGiftBox(
.descriptor = descriptor,
.randomId = base::RandomValue<uint64>(),
};
peer->updateFull();
state->messageAllowed = peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::StarsPerMessage
) | rpl::map([=] {
return peer->starsPerMessageChecked() == 0;
});
auto cost = state->details.value(
) | rpl::map([session](const GiftDetails &details) {
return v::match(details.descriptor, [&](const GiftTypePremium &data) {
if (data.currency == kCreditsCurrency) {
const auto stars = (details.byStars && data.stars)
? data.stars
: (data.currency == kCreditsCurrency)
? data.cost
: 0;
if (stars) {
return CreditsEmojiSmall(session).append(
Lang::FormatCountDecimal(std::abs(data.cost)));
Lang::FormatCountDecimal(std::abs(stars)));
}
return TextWithEntities{
FillAmountAndCurrency(data.cost, data.currency),
@ -1462,10 +1508,17 @@ void SendGiftBox(
peer,
state->details.value()));
const auto messageWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
messageWrap->toggleOn(state->messageAllowed.value());
messageWrap->finishAnimating();
const auto messageInner = messageWrap->entity();
const auto limit = StarGiftMessageLimit(session);
const auto text = AddPartInput(
window,
container,
messageInner,
box->getDelegate()->outerContainer(),
tr::lng_gift_send_message(),
QString(),
@ -1509,7 +1562,6 @@ void SendGiftBox(
text,
session,
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
if (stars) {
const auto cost = stars->info.starsToUpgrade;
if (cost > 0 && !peer->isSelf()) {
@ -1551,20 +1603,73 @@ void SendGiftBox(
}, container->lifetime());
AddSkip(container);
}
v::match(descriptor, [&](const GiftTypePremium &) {
AddDividerText(container, tr::lng_gift_send_premium_about(
v::match(descriptor, [&](const GiftTypePremium &data) {
AddDividerText(messageInner, tr::lng_gift_send_premium_about(
lt_user,
rpl::single(peer->shortName())));
if (const auto byStars = data.stars) {
const auto star = Ui::Text::IconEmoji(&st::starIconEmojiColored);
AddSkip(container);
container->add(
object_ptr<SettingsButton>(
container,
tr::lng_gift_send_pay_with_stars(
lt_amount,
rpl::single(base::duplicate(star).append(Lang::FormatCountDecimal(byStars))),
Ui::Text::WithEntities),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
auto now = state->details.current();
now.byStars = toggled;
state->details = std::move(now);
}, container->lifetime());
AddSkip(container);
const auto balance = AddDividerText(
container,
tr::lng_gift_send_stars_balance(
lt_amount,
peer->session().credits().balanceValue(
) | rpl::map([=](StarsAmount amount) {
return base::duplicate(star).append(
Lang::FormatStarsAmountDecimal(amount));
}),
lt_link,
tr::lng_gift_send_stars_balance_link(
) | Ui::Text::ToLink(),
Ui::Text::WithEntities));
struct State {
Settings::BuyStarsHandler buyStars;
rpl::variable<bool> loading;
};
const auto state = balance->lifetime().make_state<State>();
state->loading = state->buyStars.loadingValue();
balance->setClickHandlerFilter([=](const auto &...) {
if (!state->loading.current()) {
state->buyStars.handler(window->uiShow())();
}
return false;
});
}
}, [&](const GiftTypeStars &) {
AddDividerText(container, peer->isSelf()
? tr::lng_gift_send_anonymous_self()
: peer->isBroadcast()
? tr::lng_gift_send_anonymous_about_channel()
: tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())));
: rpl::conditional(
state->messageAllowed.value(),
tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())),
tr::lng_gift_send_anonymous_about_paid(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName()))));
});
const auto buttonWidth = st::boxWideWidth
@ -1575,13 +1680,20 @@ void SendGiftBox(
return;
}
state->submitting = true;
const auto details = state->details.current();
auto details = state->details.current();
if (!state->messageAllowed.current()) {
details.text = {};
}
const auto weak = MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Paid) {
if (details.byStars
|| v::is<GiftTypeStars>(details.descriptor)) {
window->session().credits().load(true);
}
const auto copy = state->media;
window->showPeerHistory(peer);
ShowSentToast(window, descriptor, details);
ShowSentToast(window, details.descriptor, details);
}
if (const auto strong = weak.data()) {
box->closeBox();
@ -1814,6 +1926,8 @@ void GiftBox(
box->setCustomCornersFilling(RectPart::FullTop);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
window->session().credits().load();
FillBg(box);
const auto &stUser = st::premiumGiftsUserpicButton;
@ -2070,7 +2184,66 @@ void ChooseStarGiftRecipient(
void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer) {
controller->show(Box(GiftBox, controller, peer));
struct Session {
PeerData *peer = nullptr;
bool premiumGiftsReady = false;
bool starsGiftsReady = false;
rpl::lifetime lifetime;
};
static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
const auto session = &controller->session();
auto i = Map.find(session);
if (i == end(Map)) {
i = Map.emplace(session).first;
session->lifetime().add([=] { Map.remove(session); });
} else if (i->second.peer == peer) {
return;
}
i->second = Session{ .peer = peer };
const auto weak = base::make_weak(controller);
const auto show = [=] {
Map[session] = Session();
if (const auto strong = weak.get()) {
strong->show(Box(GiftBox, strong, peer));
}
};
base::timer_once(
kGiftsPreloadTimeout
) | rpl::start_with_next(show, i->second.lifetime);
const auto user = peer->asUser();
if (user && !user->isSelf()) {
GiftsPremium(
session,
peer
) | rpl::start_with_next([=](PremiumGiftsDescriptor &&gifts) {
if (!gifts.list.empty()) {
auto &entry = Map[session];
entry.premiumGiftsReady = true;
if (entry.starsGiftsReady) {
show();
}
}
}, i->second.lifetime);
} else {
i->second.premiumGiftsReady = true;
}
GiftsStars(
session,
peer
) | rpl::start_with_next([=](std::vector<GiftTypeStars> &&gifts) {
if (!gifts.empty()) {
auto &entry = Map[session];
entry.starsGiftsReady = true;
if (entry.premiumGiftsReady) {
show();
}
}
}, i->second.lifetime);
}
void AddUniqueGiftCover(

View file

@ -150,10 +150,7 @@ void TranslateBox(
original->entity()->setAnimationsPausedCallback(animationsPaused);
original->entity()->setMarkedText(
text,
Core::MarkedTextContext{
.session = &peer->session(),
.customEmojiRepaint = [=] { original->entity()->update(); },
});
Core::TextContext({ .session = &peer->session() }));
original->setMinimalHeight(lineHeight);
original->hide(anim::type::instant);
@ -221,10 +218,7 @@ void TranslateBox(
const auto label = translated->entity();
label->setMarkedText(
text,
Core::MarkedTextContext{
.session = &peer->session(),
.customEmojiRepaint = [=] { label->update(); },
});
Core::TextContext({ .session = &peer->session() }));
translated->show(anim::type::instant);
loading->hide(anim::type::instant);
};

View file

@ -132,8 +132,12 @@ object_ptr<ShareBox> ShareInviteLinkBox(
QGuiApplication::clipboard()->setText(currentLink());
show->showToast(tr::lng_group_invite_copied(tr::now));
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@ -150,6 +154,8 @@ object_ptr<ShareBox> ShareInviteLinkBox(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
@ -178,7 +184,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
};
auto filterCallback = [](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -189,6 +195,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
auto result = Box<ShareBox>(ShareBox::Descriptor{
.session = &peer->session(),
.copyCallback = std::move(copyCallback),
.countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.bottomWidget = std::move(bottom),
@ -199,7 +206,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
tr::lng_group_call_copy_speaker_link(),
tr::lng_group_call_copy_listener_link()),
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
});
*box = result.data();
return result;

View file

@ -149,6 +149,7 @@ EmojiButton {
SendButton {
inner: IconButton;
stars: RoundButton;
record: icon;
recordOver: icon;
round: icon;
@ -855,6 +856,10 @@ historyComposeButton: FlatButton {
color: historyComposeButtonBgRipple;
}
}
historyComposeButtonText: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
textFg: windowActiveTextFg;
}
historyGiftToChannel: IconButton(defaultIconButton) {
width: 46px;
height: 46px;
@ -913,6 +918,10 @@ historyBusinessBotSettings: IconButton(defaultIconButton) {
height: 58px;
width: 48px;
}
paysStatusLabel: FlatLabel(historyBusinessBotStatus) {
align: align(top);
minWidth: 240px;
}
historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }};
historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }};
@ -1289,6 +1298,12 @@ historySend: SendButton {
icon: historySendIcon;
iconOver: historySendIconOver;
}
stars: RoundButton(defaultActiveButton) {
height: 28px;
padding: margins(0px, 0px, 6px, 0px);
textTop: 5px;
width: -8px;
}
record: historyRecordVoice;
recordOver: historyRecordVoiceOver;
round: historyRecordRound;

View file

@ -1747,7 +1747,8 @@ void InitFieldAutocomplete(
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (!shortcutMessages
|| shortcutMessages->shortcuts().list.empty())) {
|| shortcutMessages->shortcuts().list.empty()
|| peer->starsPerMessageChecked() != 0)) {
parsed = {};
}
raw->showFiltered(peer, parsed.query, parsed.fromStart);

View file

@ -695,14 +695,15 @@ GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareSavedGif(
}
GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareInlineResult(
not_null<InlineResult*> result) {
auto it = _inlineLayouts.find(result);
std::shared_ptr<InlineResult> result) {
const auto raw = result.get();
auto it = _inlineLayouts.find(raw);
if (it == _inlineLayouts.cend()) {
if (auto layout = LayoutItem::createLayout(
this,
result,
std::move(result),
_inlineWithThumb)) {
it = _inlineLayouts.emplace(result, std::move(layout)).first;
it = _inlineLayouts.emplace(raw, std::move(layout)).first;
it->second->initDimensions();
} else {
return nullptr;
@ -775,8 +776,8 @@ int GifsListWidget::refreshInlineRows(const InlineCacheEntry *entry, bool result
from,
count
) | ranges::views::transform([&](
const std::unique_ptr<InlineBots::Result> &r) {
return layoutPrepareInlineResult(r.get());
const std::shared_ptr<InlineBots::Result> &r) {
return layoutPrepareInlineResult(r);
}) | ranges::views::filter([](const LayoutItem *item) {
return item != nullptr;
}) | ranges::to<std::vector<not_null<LayoutItem*>>>;
@ -799,7 +800,7 @@ int GifsListWidget::validateExistingInlineRows(const InlineResults &results) {
const auto until = _mosaic.validateExistingRows([&](
not_null<const LayoutItem*> item,
int untilIndex) {
return item->getResult() != results[untilIndex].get();
return item->getResult().get() != results[untilIndex].get();
}, results.size());
if (_mosaic.empty()) {

View file

@ -131,7 +131,7 @@ private:
};
using InlineResult = InlineBots::Result;
using InlineResults = std::vector<std::unique_ptr<InlineResult>>;
using InlineResults = std::vector<std::shared_ptr<InlineResult>>;
using LayoutItem = InlineBots::Layout::ItemBase;
struct InlineCacheEntry {
@ -162,7 +162,8 @@ private:
void clearInlineRows(bool resultsDeleted);
LayoutItem *layoutPrepareSavedGif(not_null<DocumentData*> document);
LayoutItem *layoutPrepareInlineResult(not_null<InlineResult*> result);
LayoutItem *layoutPrepareInlineResult(
std::shared_ptr<InlineResult> result);
void deleteUnusedGifLayouts();

View file

@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h"
@ -432,12 +433,9 @@ void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
const auto session = args.session;
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, args.allowPremiumEmoji));
field->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
});
}, [paused] {
field->setCustomTextContext(Core::TextContext({
.session = session
}), [paused] {
return On(PowerSaving::kEmojiChat) || paused();
}, [paused] {
return On(PowerSaving::kChatSpoiler) || paused();
@ -1280,3 +1278,26 @@ void SelectTextInFieldWithMargins(
textCursor.setPosition(selection.to, QTextCursor::KeepAnchor);
field->setTextCursor(textCursor);
}
TextWithEntities PaidSendButtonText(tr::now_t, int stars) {
return Ui::Text::IconEmoji(&st::starIconEmoji).append(
Lang::FormatCountToShort(stars).string);
}
rpl::producer<TextWithEntities> PaidSendButtonText(
rpl::producer<int> stars,
rpl::producer<QString> fallback) {
if (fallback) {
return rpl::combine(
std::move(fallback),
std::move(stars)
) | rpl::map([=](QString zero, int count) {
return count
? PaidSendButtonText(tr::now, count)
: TextWithEntities{ zero };
});
}
return std::move(stars) | rpl::map([=](int count) {
return PaidSendButtonText(tr::now, count);
});
}

View file

@ -19,6 +19,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QClipboard>
namespace tr {
struct now_t;
} // namespace tr
namespace Main {
class Session;
class SessionShow;
@ -169,3 +173,8 @@ private:
void SelectTextInFieldWithMargins(
not_null<Ui::InputField*> field,
const TextSelection &selection);
[[nodiscard]] TextWithEntities PaidSendButtonText(tr::now_t, int stars);
[[nodiscard]] rpl::producer<TextWithEntities> PaidSendButtonText(
rpl::producer<int> stars,
rpl::producer<QString> fallback = nullptr);

View file

@ -37,7 +37,7 @@ std::map<int, const char*> BetaLogs() {
"- Nice looking code blocks with syntax highlight.\n"
"- Copy full code block by click on its header.\n"
"- Send a highlighted code block using ```language syntax.\n"
}
};

View file

@ -205,13 +205,13 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const {
});
};
if (_bot->isVerified()
|| _bot->session().local().isBotTrustedOpenGame(_bot->id)) {
|| _bot->session().local().isPeerTrustedOpenGame(_bot->id)) {
openGame();
} else {
if (const auto controller = my.sessionWindow.get()) {
const auto callback = [=, bot = _bot](Fn<void()> close) {
close();
bot->session().local().markBotTrustedOpenGame(bot->id);
bot->session().local().markPeerTrustedOpenGame(bot->id);
openGame();
};
controller->show(Ui::MakeConfirmBox({

View file

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/edit_birthday_box.h"
#include "ui/integration.h"
#include "payments/payments_non_panel_process.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "boxes/share_box.h"
#include "boxes/connection_box.h"
#include "boxes/gift_premium_box.h"
@ -68,6 +69,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "inline_bots/bot_attach_web_view.h"
#include "history/history.h"
#include "history/history_item.h"
@ -1018,6 +1021,45 @@ bool CopyUsername(
return true;
}
bool EditPaidMessagesFee(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto peerId = PeerId(match->captured(1).toULongLong());
if (const auto id = peerToChannel(peerId)) {
const auto channel = controller->session().data().channelLoaded(id);
if (channel && channel->canEditPermissions()) {
ShowEditChatPermissions(controller, channel);
}
} else {
controller->show(Box(EditMessagesPrivacyBox, controller));
}
return true;
}
bool ShowCommonGroups(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto peerId = PeerId(match->captured(1).toULongLong());
if (const auto id = peerToUser(peerId)) {
const auto user = controller->session().data().userLoaded(id);
if (user) {
controller->showSection(
std::make_shared<Info::Memento>(
user,
Info::Section::Type::CommonGroups));
}
}
return true;
}
bool ShowStarsExamples(
Window::SessionController *controller,
const Match &match,
@ -1529,6 +1571,14 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
u"^username_regular/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
CopyUsername,
},
{
u"^edit_paid_messages_fee/([0-9]+)$"_q,
EditPaidMessagesFee,
},
{
u"^common_groups/([0-9]+)$"_q,
ShowCommonGroups,
},
{
u"^stars_examples$"_q,
ShowStarsExamples,

View file

@ -76,6 +76,14 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ u"first_chat"_q , Command::ChatFirst },
{ u"last_chat"_q , Command::ChatLast },
{ u"self_chat"_q , Command::ChatSelf },
{ u"pinned_chat1"_q , Command::ChatPinned1 },
{ u"pinned_chat2"_q , Command::ChatPinned2 },
{ u"pinned_chat3"_q , Command::ChatPinned3 },
{ u"pinned_chat4"_q , Command::ChatPinned4 },
{ u"pinned_chat5"_q , Command::ChatPinned5 },
{ u"pinned_chat6"_q , Command::ChatPinned6 },
{ u"pinned_chat7"_q , Command::ChatPinned7 },
{ u"pinned_chat8"_q , Command::ChatPinned8 },
{ u"previous_folder"_q , Command::FolderPrevious },
{ u"next_folder"_q , Command::FolderNext },
@ -168,6 +176,7 @@ private:
void set(const QKeySequence &result, Command command, bool replace);
void remove(const QString &keys);
void remove(const QKeySequence &keys);
void remove(const QKeySequence &keys, Command command);
void unregister(base::unique_qptr<QAction> shortcut);
void pruneListened();
@ -293,7 +302,7 @@ void Manager::change(
Command command,
std::optional<Command> restore) {
if (!was.isEmpty()) {
remove(was);
remove(was, command);
}
if (!now.isEmpty()) {
set(now, command, true);
@ -397,6 +406,7 @@ bool Manager::readCustomFile() {
const auto entry = (*i).toObject();
const auto keys = entry.constFind(u"keys"_q);
const auto command = entry.constFind(u"command"_q);
const auto removed = entry.constFind(u"removed"_q);
if (keys == entry.constEnd()
|| command == entry.constEnd()
|| !(*keys).isString()
@ -410,7 +420,11 @@ bool Manager::readCustomFile() {
const auto name = (*command).toString();
const auto i = CommandByName.find(name);
if (i != end(CommandByName)) {
set((*keys).toString(), i->second, true);
if (removed != entry.constEnd() && removed->toBool()) {
remove((*keys).toString(), i->second);
} else {
set((*keys).toString(), i->second, true);
}
} else {
LOG(("Shortcut Warning: "
"could not find shortcut command handler '%1'"
@ -565,12 +579,36 @@ void Manager::writeCustomFile() {
}
}
}
for (const auto &[sequence, command] : _defaults) {
if (!_shortcuts.contains(sequence)) {
const auto has = [&](not_null<QObject*> shortcut, Command command) {
for (auto i = _commandByObject.findFirst(shortcut)
; i != end(_commandByObject) && i->first == shortcut
; ++i) {
if (i->second == command) {
return true;
}
}
return false;
};
for (const auto &[sequence, commands] : _defaults) {
const auto i = _shortcuts.find(sequence);
if (i == end(_shortcuts)) {
QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, QJsonValue());
shortcuts.append(entry);
continue;
}
for (const auto command : commands) {
if (!has(i->second.get(), command)) {
const auto j = CommandNames().find(command);
if (j != CommandNames().end()) {
QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, j->second);
entry.insert(u"removed"_q, true);
shortcuts.append(entry);
}
}
}
}
@ -669,6 +707,17 @@ void Manager::remove(const QKeySequence &keys) {
}
}
void Manager::remove(const QKeySequence &keys, Command command) {
const auto i = _shortcuts.find(keys);
if (i != end(_shortcuts)) {
_commandByObject.remove(i->second.get(), command);
if (!_commandByObject.contains(i->second.get())) {
unregister(std::move(i->second));
_shortcuts.erase(i);
}
}
}
void Manager::unregister(base::unique_qptr<QAction> shortcut) {
if (shortcut) {
_commandByObject.removeAll(shortcut.get());

View file

@ -60,6 +60,11 @@ public:
normalize();
return *this;
}
inline StarsAmount operator-() const {
auto result = *this;
result *= -1;
return result;
}
friend inline auto operator<=>(StarsAmount, StarsAmount) = default;
friend inline bool operator==(StarsAmount, StarsAmount) = default;
@ -97,3 +102,7 @@ private:
[[nodiscard]] inline StarsAmount operator*(StarsAmount a, int64 b) {
return a *= b;
}
[[nodiscard]] inline StarsAmount operator*(int64 a, StarsAmount b) {
return b *= a;
}

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "iv/iv_instance.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_utilities.h"
#include "ui/basic_click_handlers.h"
#include "ui/emoji_config.h"
#include "lang/lang_keys.h"
@ -112,6 +113,40 @@ const auto kBadPrefix = u"http://"_q;
} // namespace
Ui::Text::MarkedContext TextContext(TextContextArgs &&args) {
using Context = Ui::Text::MarkedContext;
using Factory = Ui::Text::CustomEmojiFactory;
const auto session = args.session;
auto simple = [session](QStringView data, const Context &context) {
return session->data().customEmojiManager().create(
data,
context.repaint);
};
auto factory = !args.customEmojiLoopLimit
? Factory(simple)
: (args.customEmojiLoopLimit > 0)
? Factory([simple, loop = args.customEmojiLoopLimit](
QStringView data,
const Context &context) {
return std::make_unique<Ui::Text::LimitedLoopsEmoji>(
simple(data, context),
loop);
})
: Factory([simple](
QStringView data,
const Context &context) {
return std::make_unique<Ui::Text::FirstFrameEmoji>(
simple(data, context));
});
args.details.session = session;
return {
.repaint = std::move(args.repaint),
.customEmojiFactory = std::move(factory),
.other = std::move(args.details),
};
}
void UiIntegration::postponeCall(FnMut<void()> &&callable) {
Sandbox::Instance().postponeCall(std::move(callable));
}
@ -152,8 +187,8 @@ bool UiIntegration::screenIsLocked() {
std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
const EntityLinkData &data,
const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
const Ui::Text::MarkedContext &context) {
const auto my = std::any_cast<Core::TextContextDetails>(&context.other);
switch (data.type) {
case EntityType::Url:
return (!data.data.isEmpty()
@ -170,7 +205,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<BotCommandClickHandler>(data.data);
case EntityType::Hashtag:
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
using HashtagMentionType = TextContextDetails::HashtagMentionType;
if (my && my->type == HashtagMentionType::Twitter) {
return std::make_shared<UrlClickHandler>(
(u"https://twitter.com/hashtag/"_q
@ -190,7 +225,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<CashtagClickHandler>(data.data);
case EntityType::Mention:
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
using HashtagMentionType = TextContextDetails::HashtagMentionType;
if (my && my->type == HashtagMentionType::Twitter) {
return std::make_shared<UrlClickHandler>(
u"https://twitter.com/"_q + data.data.mid(1),
@ -222,7 +257,9 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
case EntityType::Pre:
return std::make_shared<MonospaceClickHandler>(data.text, data.type);
case EntityType::Phone:
return std::make_shared<PhoneClickHandler>(my->session, data.text);
return my->session
? std::make_shared<PhoneClickHandler>(my->session, data.text)
: nullptr;
}
return Integration::createLinkHandler(data, context);
}
@ -280,36 +317,6 @@ bool UiIntegration::copyPreOnClick(const QVariant &context) {
return true;
}
std::unique_ptr<Ui::Text::CustomEmoji> UiIntegration::createCustomEmoji(
QStringView data,
const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
if (!my || !my->session) {
return nullptr;
}
auto result = my->session->data().customEmojiManager().create(
data,
my->customEmojiRepaint);
if (my->customEmojiLoopLimit > 0) {
return std::make_unique<Ui::Text::LimitedLoopsEmoji>(
std::move(result),
my->customEmojiLoopLimit);
} else if (my->customEmojiLoopLimit) {
return std::make_unique<Ui::Text::FirstFrameEmoji>(
std::move(result));
}
return result;
}
Fn<void()> UiIntegration::createSpoilerRepaint(const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
if (my) {
return my->customEmojiRepaint;
}
const auto common = std::any_cast<CommonTextContext>(&context);
return common ? common->repaint : nullptr;
}
rpl::producer<> UiIntegration::forcePopupMenuHideRequests() {
return Core::App().passcodeLockChanges() | rpl::to_empty;
}

View file

@ -19,7 +19,7 @@ class ElementDelegate;
namespace Core {
struct MarkedTextContext {
struct TextContextDetails {
enum class HashtagMentionType : uchar {
Telegram,
Twitter,
@ -28,9 +28,15 @@ struct MarkedTextContext {
Main::Session *session = nullptr;
HashtagMentionType type = HashtagMentionType::Telegram;
Fn<void()> customEmojiRepaint;
};
struct TextContextArgs {
not_null<Main::Session*> session;
TextContextDetails details;
Fn<void()> repaint;
int customEmojiLoopLimit = 0;
};
[[nodiscard]] Ui::Text::MarkedContext TextContext(TextContextArgs &&args);
class UiIntegration final : public Ui::Integration {
public:
@ -49,7 +55,7 @@ public:
std::shared_ptr<ClickHandler> createLinkHandler(
const EntityLinkData &data,
const std::any &context) override;
const Ui::Text::MarkedContext &context) override;
bool handleUrlClick(
const QString &url,
const QVariant &context) override;
@ -57,10 +63,6 @@ public:
rpl::producer<> forcePopupMenuHideRequests() override;
const Ui::Emoji::One *defaultEmojiVariant(
const Ui::Emoji::One *emoji) override;
std::unique_ptr<Ui::Text::CustomEmoji> createCustomEmoji(
QStringView data,
const std::any &context) override;
Fn<void()> createSpoilerRepaint(const std::any &context) override;
QString phraseContextCopyText() override;
QString phraseContextCopyEmail() override;

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5011001;
constexpr auto AppVersionStr = "5.11.1";
constexpr auto AppVersion = 5012001;
constexpr auto AppVersionStr = "5.12.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -91,7 +91,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTP_int(shortcutId),
MTP_long(data.veffect().value_or_empty()),
(data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()),
MTP_int(data.vreport_delivery_until_date().value_or_empty()));
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
MTP_long(data.vpaid_message_stars().value_or_empty()));
});
}

View file

@ -37,10 +37,7 @@ void Credits::apply(const MTPDupdateStarsBalance &data) {
rpl::producer<float64> Credits::rateValue(
not_null<PeerData*> ownedBotOrChannel) {
return rpl::single(
_session->appConfig().get<float64>(
u"stars_usd_withdraw_rate_x1000"_q,
1200) / 1000.);
return rpl::single(_session->appConfig().starsWithdrawRate());
}
void Credits::load(bool force) {

View file

@ -95,7 +95,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTPint(), // quick_reply_shortcut_id
MTP_long(data.veffect().value_or_empty()), // effect
data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck(),
MTP_int(data.vreport_delivery_until_date().value_or_empty()));
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
MTP_long(data.vpaid_message_stars().value_or_empty()));
});
}
@ -269,7 +270,8 @@ void ScheduledMessages::sendNowSimpleMessage(
MTPint(), // quick_reply_shortcut_id
MTP_long(local->effectId()), // effect
MTPFactCheck(),
MTPint()), // report_delivery_until_date
MTPint(), // report_delivery_until_date
MTPlong()), // paid_message_stars
localFlags,
NewMessageType::Unread);

View file

@ -75,46 +75,48 @@ struct PeerUpdate {
BackgroundEmoji = (1ULL << 15),
StoriesState = (1ULL << 16),
VerifyInfo = (1ULL << 17),
StarsPerMessage = (1ULL << 18),
// For users
CanShareContact = (1ULL << 18),
IsContact = (1ULL << 19),
PhoneNumber = (1ULL << 20),
OnlineStatus = (1ULL << 21),
BotCommands = (1ULL << 22),
BotCanBeInvited = (1ULL << 23),
BotStartToken = (1ULL << 24),
CommonChats = (1ULL << 25),
PeerGifts = (1ULL << 26),
HasCalls = (1ULL << 27),
SupportInfo = (1ULL << 28),
IsBot = (1ULL << 29),
EmojiStatus = (1ULL << 30),
BusinessDetails = (1ULL << 31),
Birthday = (1ULL << 32),
PersonalChannel = (1ULL << 33),
StarRefProgram = (1ULL << 34),
CanShareContact = (1ULL << 19),
IsContact = (1ULL << 20),
PhoneNumber = (1ULL << 21),
OnlineStatus = (1ULL << 22),
BotCommands = (1ULL << 23),
BotCanBeInvited = (1ULL << 24),
BotStartToken = (1ULL << 25),
CommonChats = (1ULL << 26),
PeerGifts = (1ULL << 27),
HasCalls = (1ULL << 28),
SupportInfo = (1ULL << 29),
IsBot = (1ULL << 30),
EmojiStatus = (1ULL << 31),
BusinessDetails = (1ULL << 32),
Birthday = (1ULL << 33),
PersonalChannel = (1ULL << 34),
StarRefProgram = (1ULL << 35),
PaysPerMessage = (1ULL << 36),
// For chats and channels
InviteLinks = (1ULL << 35),
Members = (1ULL << 36),
Admins = (1ULL << 37),
BannedUsers = (1ULL << 38),
Rights = (1ULL << 39),
PendingRequests = (1ULL << 40),
Reactions = (1ULL << 41),
InviteLinks = (1ULL << 37),
Members = (1ULL << 38),
Admins = (1ULL << 39),
BannedUsers = (1ULL << 40),
Rights = (1ULL << 41),
PendingRequests = (1ULL << 42),
Reactions = (1ULL << 43),
// For channels
ChannelAmIn = (1ULL << 42),
StickersSet = (1ULL << 43),
EmojiSet = (1ULL << 44),
ChannelLinkedChat = (1ULL << 45),
ChannelLocation = (1ULL << 46),
Slowmode = (1ULL << 47),
GroupCall = (1ULL << 48),
ChannelAmIn = (1ULL << 44),
StickersSet = (1ULL << 45),
EmojiSet = (1ULL << 46),
ChannelLinkedChat = (1ULL << 47),
ChannelLocation = (1ULL << 48),
Slowmode = (1ULL << 49),
GroupCall = (1ULL << 50),
// For iteration
LastUsedBit = (1ULL << 48),
LastUsedBit = (1ULL << 50),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "api/api_global_privacy.h"
#include "data/components/credits.h"
#include "data/data_changes.h"
#include "data/data_channel_admins.h"
#include "data/data_user.h"
@ -30,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_invite.h"
#include "api/api_invite_links.h"
#include "apiwrap.h"
#include "storage/storage_account.h"
#include "ui/unread_badge.h"
#include "window/notifications_manager.h"
@ -859,6 +861,21 @@ void ChannelData::growSlowmodeLastMessage(TimeId when) {
session().changes().peerUpdated(this, UpdateFlag::Slowmode);
}
int ChannelData::starsPerMessage() const {
if (const auto info = mgInfo.get()) {
return info->_starsPerMessage;
}
return 0;
}
void ChannelData::setStarsPerMessage(int stars) {
if (mgInfo && starsPerMessage() != stars) {
mgInfo->_starsPerMessage = stars;
session().changes().peerUpdated(this, UpdateFlag::StarsPerMessage);
}
checkTrustedPayForMessage();
}
int ChannelData::peerGiftsCount() const {
return _peerGiftsCount;
}
@ -1150,7 +1167,8 @@ void ApplyChannelUpdate(
| Flag::CanViewRevenue
| Flag::PaidMediaAllowed
| Flag::CanViewCreditsRevenue
| Flag::StargiftsAvailable;
| Flag::StargiftsAvailable
| Flag::PaidMessagesAvailable;
channel->setFlags((channel->flags() & ~mask)
| (update.is_can_set_username() ? Flag::CanSetUsername : Flag())
| (update.is_can_view_participants()
@ -1174,6 +1192,9 @@ void ApplyChannelUpdate(
: Flag())
| (update.is_stargifts_available()
? Flag::StargiftsAvailable
: Flag())
| (update.is_paid_messages_available()
? Flag::PaidMessagesAvailable
: Flag()));
channel->setUserpicPhoto(update.vchat_photo());
if (const auto migratedFrom = update.vmigrated_from_chat_id()) {

View file

@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer_bot_commands.h"
#include "data/data_user_names.h"
class ChannelData;
struct ChannelLocation {
QString address;
Data::LocationPoint point;
@ -70,6 +72,7 @@ enum class ChannelDataFlag : uint64 {
CanViewCreditsRevenue = (1ULL << 34),
SignatureProfiles = (1ULL << 35),
StargiftsAvailable = (1ULL << 36),
PaidMessagesAvailable = (1ULL << 37),
};
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>;
@ -150,6 +153,9 @@ private:
ChannelLocation _location;
Data::ChatBotCommands _botCommands;
std::unique_ptr<Data::Forum> _forum;
int _starsPerMessage = 0;
friend class ChannelData;
};
@ -257,6 +263,9 @@ public:
[[nodiscard]] bool stargiftsAvailable() const {
return flags() & Flag::StargiftsAvailable;
}
[[nodiscard]] bool paidMessagesAvailable() const {
return flags() & Flag::PaidMessagesAvailable;
}
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(
not_null<PeerData*> participant);
@ -456,6 +465,9 @@ public:
[[nodiscard]] TimeId slowmodeLastMessage() const;
void growSlowmodeLastMessage(TimeId when);
void setStarsPerMessage(int stars);
[[nodiscard]] int starsPerMessage() const;
[[nodiscard]] int peerGiftsCount() const;
void setPeerGiftsCount(int count);

View file

@ -17,10 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "window/window_session_controller.h"
#include "styles/style_widgets.h"
namespace {
@ -120,7 +123,7 @@ bool CanSendAnyOf(
|| user->isRepliesChat()
|| user->isVerifyCodes()) {
return false;
} else if (user->meRequiresPremiumToWrite()
} else if (user->requiresPremiumToWrite()
&& !user->session().premium()) {
return false;
} else if (rights
@ -177,7 +180,7 @@ SendError RestrictionError(
using Flag = ChatRestriction;
if (const auto restricted = peer->amRestricted(restriction)) {
if (const auto user = peer->asUser()) {
if (user->meRequiresPremiumToWrite()
if (user->requiresPremiumToWrite()
&& !user->session().premium()) {
return SendError({
.text = tr::lng_restricted_send_non_premium(

View file

@ -66,10 +66,11 @@ struct CreditsHistoryEntry final {
uint64 bareGiftStickerId = 0;
uint64 bareGiftOwnerId = 0;
uint64 bareActorId = 0;
uint64 bareGiftListPeerId = 0;
uint64 giftSavedId = 0;
uint64 bareEntryOwnerId = 0;
uint64 giftChannelSavedId = 0;
uint64 stargiftId = 0;
std::shared_ptr<UniqueGift> uniqueGift;
Fn<std::vector<CreditsHistoryEntry>()> pinnedSavedGifts;
StarsAmount starrefAmount;
int starrefCommission = 0;
uint64 starrefRecipientId = 0;
@ -77,11 +78,15 @@ struct CreditsHistoryEntry final {
QDateTime subscriptionUntil;
QDateTime successDate;
QString successLink;
int paidMessagesCount = 0;
StarsAmount paidMessagesAmount;
int paidMessagesCommission = 0;
int limitedCount = 0;
int limitedLeft = 0;
int starsConverted = 0;
int starsToUpgrade = 0;
int starsUpgradedBySender = 0;
int premiumMonthsForStars = 0;
int floodSkip = 0;
bool converted : 1 = false;
bool anonymous : 1 = false;
@ -89,6 +94,7 @@ struct CreditsHistoryEntry final {
bool giftTransferred : 1 = false;
bool giftRefunded : 1 = false;
bool giftUpgraded : 1 = false;
bool giftPinned : 1 = false;
bool savedToProfile : 1 = false;
bool fromGiftsList : 1 = false;
bool fromGiftSlug : 1 = false;

View file

@ -16,7 +16,10 @@ namespace Data {
struct CreditsEarnStatistics final {
explicit operator bool() const {
return !!usdRate;
return usdRate
&& currentBalance
&& availableBalance
&& overallRevenue;
}
Data::StatisticalGraph revenueGraph;
StarsAmount currentBalance;

View file

@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/history_item.h"
#include "storage/file_download.h"
#include "storage/storage_account.h"
#include "storage/storage_facade.h"
#include "storage/storage_shared_media.h"
@ -65,6 +66,28 @@ using UpdateFlag = Data::PeerUpdate::Flag;
return session->appConfig().ignoredRestrictionReasons();
}
[[nodiscard]] int ParseRegistrationDate(const QString &text) {
// MM.YYYY
if (text.size() != 7 || text[2] != '.') {
return 0;
}
const auto month = text.mid(0, 2).toInt();
const auto year = text.mid(3, 4).toInt();
return (year > 2012 && year < 2100 && month > 0 && month <= 12)
? (year * 100) + month
: 0;
}
[[nodiscard]] int RegistrationYear(int date) {
const auto year = date / 100;
return (year > 2012 && year < 2100) ? year : 0;
}
[[nodiscard]] int RegistrationMonth(int date) {
const auto month = date % 100;
return (month > 0 && month <= 12) ? month : 0;
}
} // namespace
namespace Data {
@ -311,6 +334,17 @@ void PeerData::invalidateEmptyUserpic() {
_userpicEmpty = nullptr;
}
void PeerData::checkTrustedPayForMessage() {
if (!_checkedTrustedPayForMessage
&& !starsPerMessage()
&& session().local().peerTrustedPayForMessageRead()) {
_checkedTrustedPayForMessage = 1;
if (session().local().hasPeerTrustedPayForMessageEntry(id)) {
session().local().clearPeerTrustedPayForMessage(id);
}
}
}
ClickHandlerPtr PeerData::createOpenLink() {
return std::make_shared<PeerClickHandler>(this);
}
@ -692,7 +726,9 @@ void PeerData::checkFolder(FolderId folderId) {
void PeerData::clearBusinessBot() {
if (const auto details = _barDetails.get()) {
if (details->requestChatDate) {
if (details->requestChatDate
|| details->paysPerMessage
|| !details->phoneCountryCode.isEmpty()) {
details->businessBot = nullptr;
details->businessBotManageUrl = QString();
} else {
@ -735,12 +771,27 @@ void PeerData::saveTranslationDisabled(bool disabled) {
void PeerData::setBarSettings(const MTPPeerSettings &data) {
data.match([&](const MTPDpeerSettings &data) {
if (!data.vbusiness_bot_id() && !data.vrequest_chat_title()) {
const auto wasPaysPerMessage = paysPerMessage();
if (!data.vbusiness_bot_id()
&& !data.vrequest_chat_title()
&& !data.vcharge_paid_message_stars()
&& !data.vphone_country()
&& !data.vregistration_month()
&& !data.vname_change_date()
&& !data.vphoto_change_date()) {
_barDetails = nullptr;
} else if (!_barDetails) {
_barDetails = std::make_unique<PeerBarDetails>();
}
if (_barDetails) {
_barDetails->phoneCountryCode
= qs(data.vphone_country().value_or_empty());
_barDetails->registrationDate = ParseRegistrationDate(
data.vregistration_month().value_or_empty());
_barDetails->nameChangeDate
= data.vname_change_date().value_or_empty();
_barDetails->photoChangeDate
= data.vphoto_change_date().value_or_empty();
_barDetails->requestChatTitle
= qs(data.vrequest_chat_title().value_or_empty());
_barDetails->requestChatDate
@ -750,6 +801,8 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) {
: nullptr;
_barDetails->businessBotManageUrl
= qs(data.vbusiness_bot_manage_url().value_or_empty());
_barDetails->paysPerMessage
= data.vcharge_paid_message_stars().value_or_empty();
}
using Flag = PeerBarSetting;
setBarSettings((data.is_add_contact() ? Flag::AddContact : Flag())
@ -773,8 +826,35 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) {
| (data.is_business_bot_can_reply()
? Flag::BusinessBotCanReply
: Flag()));
if (wasPaysPerMessage != paysPerMessage()) {
session().changes().peerUpdated(
this,
UpdateFlag::PaysPerMessage);
}
});
}
int PeerData::paysPerMessage() const {
return _barDetails ? _barDetails->paysPerMessage : 0;
}
void PeerData::clearPaysPerMessage() {
if (const auto details = _barDetails.get()) {
if (details->paysPerMessage) {
if (details->businessBot
|| details->requestChatDate
|| !details->phoneCountryCode.isEmpty()) {
details->paysPerMessage = 0;
} else {
_barDetails = nullptr;
}
session().changes().peerUpdated(
this,
UpdateFlag::PaysPerMessage);
}
}
}
QString PeerData::requestChatTitle() const {
return _barDetails ? _barDetails->requestChatTitle : QString();
}
@ -791,6 +871,28 @@ QString PeerData::businessBotManageUrl() const {
return _barDetails ? _barDetails->businessBotManageUrl : QString();
}
QString PeerData::phoneCountryCode() const {
return _barDetails ? _barDetails->phoneCountryCode : QString();
}
int PeerData::registrationMonth() const {
return _barDetails
? RegistrationMonth(_barDetails->registrationDate)
: 0;
}
int PeerData::registrationYear() const {
return _barDetails ? RegistrationYear(_barDetails->registrationDate) : 0;
}
TimeId PeerData::nameChangeDate() const {
return _barDetails ? _barDetails->nameChangeDate : 0;
}
TimeId PeerData::photoChangeDate() const {
return _barDetails ? _barDetails->photoChangeDate : 0;
}
bool PeerData::changeColorIndex(
const tl::conditional<MTPint> &cloudColorIndex) {
return cloudColorIndex
@ -1301,7 +1403,7 @@ Data::RestrictionCheckResult PeerData::amRestricted(
}
};
if (const auto user = asUser()) {
if (user->meRequiresPremiumToWrite() && !user->session().premium()) {
if (user->requiresPremiumToWrite() && !user->session().premium()) {
return Result::Explicit();
}
return (right == ChatRestriction::SendVoiceMessages
@ -1420,6 +1522,24 @@ bool PeerData::canManageGroupCall() const {
return false;
}
int PeerData::starsPerMessage() const {
if (const auto user = asUser()) {
return user->starsPerMessage();
} else if (const auto channel = asChannel()) {
return channel->starsPerMessage();
}
return 0;
}
int PeerData::starsPerMessageChecked() const {
if (const auto channel = asChannel()) {
return (channel->adminRights() || channel->amCreator())
? 0
: channel->starsPerMessage();
}
return starsPerMessage();
}
Data::GroupCall *PeerData::groupCall() const {
if (const auto chat = asChat()) {
return chat->groupCall();

View file

@ -173,10 +173,15 @@ inline constexpr bool is_flag_type(PeerBarSetting) { return true; };
using PeerBarSettings = base::flags<PeerBarSetting>;
struct PeerBarDetails {
QString phoneCountryCode;
int registrationDate = 0; // YYYYMM or 0, YYYY > 2012, MM > 0.
TimeId nameChangeDate = 0;
TimeId photoChangeDate = 0;
QString requestChatTitle;
TimeId requestChatDate;
UserData *businessBot = nullptr;
QString businessBotManageUrl;
int paysPerMessage = 0;
};
class PeerData {
@ -268,6 +273,9 @@ public:
[[nodiscard]] int slowmodeSecondsLeft() const;
[[nodiscard]] bool canManageGroupCall() const;
[[nodiscard]] int starsPerMessage() const;
[[nodiscard]] int starsPerMessageChecked() const;
[[nodiscard]] UserData *asBot();
[[nodiscard]] const UserData *asBot() const;
[[nodiscard]] UserData *asUser();
@ -409,11 +417,18 @@ public:
? _barSettings.changes()
: (_barSettings.value() | rpl::type_erased());
}
[[nodiscard]] int paysPerMessage() const;
void clearPaysPerMessage();
[[nodiscard]] QString requestChatTitle() const;
[[nodiscard]] TimeId requestChatDate() const;
[[nodiscard]] UserData *businessBot() const;
[[nodiscard]] QString businessBotManageUrl() const;
void clearBusinessBot();
[[nodiscard]] QString phoneCountryCode() const;
[[nodiscard]] int registrationMonth() const;
[[nodiscard]] int registrationYear() const;
[[nodiscard]] TimeId nameChangeDate() const;
[[nodiscard]] TimeId photoChangeDate() const;
enum class TranslationFlag : uchar {
Unknown,
@ -501,6 +516,7 @@ protected:
void updateUserpic(PhotoId photoId, MTP::DcId dcId, bool hasVideo);
void clearUserpic();
void invalidateEmptyUserpic();
void checkTrustedPayForMessage();
private:
void fillNames();
@ -535,9 +551,10 @@ private:
crl::time _lastFullUpdate = 0;
QString _name;
uint32 _nameVersion : 30 = 1;
uint32 _nameVersion : 29 = 1;
uint32 _sensitiveContent : 1 = 0;
uint32 _wallPaperOverriden : 1 = 0;
uint32 _checkedTrustedPayForMessage : 1 = 0;
TimeId _ttlPeriod = 0;

View file

@ -228,11 +228,11 @@ inline auto DefaultRestrictionValue(
| ChatRestriction::SendVideoMessages);
auto allowedAny = PeerFlagsValue(
user,
(UserDataFlag::Deleted | UserDataFlag::MeRequiresPremiumToWrite)
(UserDataFlag::Deleted | UserDataFlag::RequiresPremiumToWrite)
) | rpl::map([=](UserDataFlags flags) {
return (flags & UserDataFlag::Deleted)
? rpl::single(false)
: !(flags & UserDataFlag::MeRequiresPremiumToWrite)
: !(flags & UserDataFlag::RequiresPremiumToWrite)
? rpl::single(true)
: AmPremiumValue(&user->session());
}) | rpl::flatten_latest();

View file

@ -542,14 +542,22 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
| Flag::BotInlineGeo
| Flag::Premium
| Flag::Support
| Flag::SomeRequirePremiumToWrite
| Flag::RequirePremiumToWriteKnown
| Flag::HasRequirePremiumToWrite
| Flag::HasStarsPerMessage
| Flag::MessageMoneyRestrictionsKnown
| (!minimal
? Flag::Contact
| Flag::MutualContact
| Flag::DiscardMinPhoto
| Flag::StoriesHidden
: Flag());
const auto hasRequirePremiumToWrite
= data.is_contact_require_premium();
const auto hasStarsPerMessage
= data.vsend_paid_messages_stars().has_value();
if (!hasStarsPerMessage) {
result->setStarsPerMessage(0);
}
const auto storiesState = minimal
? std::optional<Data::Stories::PeerSourceState>()
: data.is_stories_unavailable()
@ -564,14 +572,25 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
| (data.is_bot_inline_geo() ? Flag::BotInlineGeo : Flag())
| (data.is_premium() ? Flag::Premium : Flag())
| (data.is_support() ? Flag::Support : Flag())
| (data.is_contact_require_premium()
? (Flag::SomeRequirePremiumToWrite
| (result->someRequirePremiumToWrite()
? (result->requirePremiumToWriteKnown()
? Flag::RequirePremiumToWriteKnown
| (hasRequirePremiumToWrite
? (Flag::HasRequirePremiumToWrite
| (result->hasRequirePremiumToWrite()
? (result->messageMoneyRestrictionsKnown()
? Flag::MessageMoneyRestrictionsKnown
: Flag())
: Flag()))
: Flag())
| (hasStarsPerMessage
? (Flag::HasStarsPerMessage
| (result->hasStarsPerMessage()
? (result->messageMoneyRestrictionsKnown()
? Flag::MessageMoneyRestrictionsKnown
: Flag())
: Flag()))
: Flag())
| ((!hasRequirePremiumToWrite && !hasStarsPerMessage)
? Flag::MessageMoneyRestrictionsKnown
: Flag())
| (!minimal
? (data.is_contact() ? Flag::Contact : Flag())
| (data.is_mutual_contact() ? Flag::MutualContact : Flag())
@ -1009,6 +1028,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
}
channel->setPhoto(data.vphoto());
channel->setStarsPerMessage(
data.vsend_paid_messages_stars().value_or_empty());
if (wasInChannel != channel->amIn()) {
flags |= UpdateFlag::ChannelAmIn;
@ -4689,7 +4710,8 @@ void Session::serviceNotification(
MTPPeerColor(), // color
MTPPeerColor(), // profile_color
MTPint(), // bot_active_users
MTPlong())); // bot_verification_icon
MTPlong(), // bot_verification_icon
MTPlong())); // send_paid_messages_stars
}
const auto history = this->history(PeerData::kServiceNotificationsId);
const auto insert = [=] {
@ -4748,7 +4770,8 @@ void Session::insertCheckedServiceNotification(
MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck(),
MTPint()), // report_delivery_until_date
MTPint(), // report_delivery_until_date
MTPlong()), // paid_message_stars
localFlags,
NewMessageType::Unread);
}

View file

@ -87,6 +87,8 @@ struct GiftUpdate {
Convert,
Transfer,
Delete,
Pin,
Unpin,
};
Data::SavedStarGiftId id;

View file

@ -132,6 +132,7 @@ struct SavedStarGift {
TimeId date = 0;
bool upgradable = false;
bool anonymous = false;
bool pinned = false;
bool hidden = false;
bool mine = false;
};

View file

@ -349,6 +349,8 @@ enum class MessageFlag : uint64 {
EstimatedDate = (1ULL << 49),
ReactionsAllowed = (1ULL << 50),
HideDisplayDate = (1ULL << 51),
};
inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>;

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_sensitive_content.h"
#include "api/api_statistics.h"
#include "storage/localstorage.h"
#include "storage/storage_account.h"
#include "storage/storage_user_photos.h"
#include "main/main_session.h"
#include "data/business/data_business_common.h"
@ -526,19 +527,23 @@ bool UserData::hasStoriesHidden() const {
return (flags() & UserDataFlag::StoriesHidden);
}
bool UserData::someRequirePremiumToWrite() const {
return (flags() & UserDataFlag::SomeRequirePremiumToWrite);
bool UserData::hasRequirePremiumToWrite() const {
return (flags() & UserDataFlag::HasRequirePremiumToWrite);
}
bool UserData::meRequiresPremiumToWrite() const {
return !isSelf() && (flags() & UserDataFlag::MeRequiresPremiumToWrite);
bool UserData::hasStarsPerMessage() const {
return (flags() & UserDataFlag::HasStarsPerMessage);
}
bool UserData::requirePremiumToWriteKnown() const {
return (flags() & UserDataFlag::RequirePremiumToWriteKnown);
bool UserData::requiresPremiumToWrite() const {
return !isSelf() && (flags() & UserDataFlag::RequiresPremiumToWrite);
}
bool UserData::canSendIgnoreRequirePremium() const {
bool UserData::messageMoneyRestrictionsKnown() const {
return (flags() & UserDataFlag::MessageMoneyRestrictionsKnown);
}
bool UserData::canSendIgnoreMoneyRestrictions() const {
return !isInaccessible() && !isRepliesChat() && !isVerifyCodes();
}
@ -546,6 +551,18 @@ bool UserData::readDatesPrivate() const {
return (flags() & UserDataFlag::ReadDatesPrivate);
}
int UserData::starsPerMessage() const {
return _starsPerMessage;
}
void UserData::setStarsPerMessage(int stars) {
if (_starsPerMessage != stars) {
_starsPerMessage = stars;
session().changes().peerUpdated(this, UpdateFlag::StarsPerMessage);
}
checkTrustedPayForMessage();
}
bool UserData::canAddContact() const {
return canShareThisContact() && !isContact();
}
@ -624,7 +641,6 @@ void UserData::setCallsStatus(CallsStatus callsStatus) {
}
}
Data::Birthday UserData::birthday() const {
return _birthday;
}
@ -699,6 +715,8 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
if (const auto pinned = update.vpinned_msg_id()) {
SetTopPinnedMessageId(user, pinned->v);
}
user->setStarsPerMessage(
update.vsend_paid_messages_stars().value_or_empty());
using Flag = UserDataFlag;
const auto mask = Flag::Blocked
| Flag::HasPhoneCalls
@ -706,8 +724,8 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
| Flag::CanPinMessages
| Flag::VoiceMessagesForbidden
| Flag::ReadDatesPrivate
| Flag::RequirePremiumToWriteKnown
| Flag::MeRequiresPremiumToWrite;
| Flag::MessageMoneyRestrictionsKnown
| Flag::RequiresPremiumToWrite;
user->setFlags((user->flags() & ~mask)
| (update.is_phone_calls_private()
? Flag::PhoneCallsPrivate
@ -719,9 +737,9 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
? Flag::VoiceMessagesForbidden
: Flag())
| (update.is_read_dates_private() ? Flag::ReadDatesPrivate : Flag())
| Flag::RequirePremiumToWriteKnown
| Flag::MessageMoneyRestrictionsKnown
| (update.is_contact_require_premium()
? Flag::MeRequiresPremiumToWrite
? Flag::RequiresPremiumToWrite
: Flag()));
user->setIsBlocked(update.is_blocked());
user->setCallsStatus(update.is_phone_calls_private()

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "core/stars_amount.h"
#include "data/components/credits.h"
#include "data/data_birthday.h"
#include "data/data_peer.h"
#include "data/data_chat_participant_status.h"
@ -109,10 +110,11 @@ enum class UserDataFlag : uint32 {
StoriesHidden = (1 << 18),
HasActiveStories = (1 << 19),
HasUnreadStories = (1 << 20),
MeRequiresPremiumToWrite = (1 << 21),
SomeRequirePremiumToWrite = (1 << 22),
RequirePremiumToWriteKnown = (1 << 23),
ReadDatesPrivate = (1 << 24),
RequiresPremiumToWrite = (1 << 21),
HasRequirePremiumToWrite = (1 << 22),
HasStarsPerMessage = (1 << 23),
MessageMoneyRestrictionsKnown = (1 << 24),
ReadDatesPrivate = (1 << 25),
};
inline constexpr bool is_flag_type(UserDataFlag) { return true; };
using UserDataFlags = base::flags<UserDataFlag>;
@ -173,12 +175,16 @@ public:
[[nodiscard]] bool applyMinPhoto() const;
[[nodiscard]] bool hasPersonalPhoto() const;
[[nodiscard]] bool hasStoriesHidden() const;
[[nodiscard]] bool someRequirePremiumToWrite() const;
[[nodiscard]] bool meRequiresPremiumToWrite() const;
[[nodiscard]] bool requirePremiumToWriteKnown() const;
[[nodiscard]] bool canSendIgnoreRequirePremium() const;
[[nodiscard]] bool hasRequirePremiumToWrite() const;
[[nodiscard]] bool hasStarsPerMessage() const;
[[nodiscard]] bool requiresPremiumToWrite() const;
[[nodiscard]] bool messageMoneyRestrictionsKnown() const;
[[nodiscard]] bool canSendIgnoreMoneyRestrictions() const;
[[nodiscard]] bool readDatesPrivate() const;
void setStarsPerMessage(int stars);
[[nodiscard]] int starsPerMessage() const;
[[nodiscard]] bool canShareThisContact() const;
[[nodiscard]] bool canAddContact() const;
@ -268,6 +274,7 @@ private:
Data::Birthday _birthday;
int _commonChatsCount = 0;
int _peerGiftsCount = 0;
int _starsPerMessage = 0;
ContactStatus _contactStatus = ContactStatus::Unknown;
CallsStatus _callsStatus = CallsStatus::Unknown;

View file

@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h" // giftBoxByStarsStyle
namespace Data {
namespace {
@ -518,8 +519,8 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
Ui::Text::CustomEmojiFactory CustomEmojiManager::factory(
SizeTag tag,
int sizeOverride) {
return [=](QStringView data, Fn<void()> update) {
return create(data, std::move(update), tag, sizeOverride);
return [=](QStringView data, const Ui::Text::MarkedContext &context) {
return create(data, context.repaint, tag, sizeOverride);
};
}
@ -1027,6 +1028,14 @@ TextWithEntities CustomEmojiManager::creditsEmoji(QMargins padding) {
false));
}
TextWithEntities CustomEmojiManager::ministarEmoji(QMargins padding) {
return Ui::Text::SingleCustomEmoji(
registerInternalEmoji(
Ui::GenerateStars(st::giftBoxByStarsStyle.font->height, 1),
padding,
false));
}
QString CustomEmojiManager::registerInternalEmoji(
QImage emoji,
QMargins padding,
@ -1136,8 +1145,9 @@ void InsertCustomEmoji(
Ui::Text::CustomEmojiFactory ReactedMenuFactory(
not_null<Main::Session*> session) {
return [owner = &session->data()](
QStringView data,
Fn<void()> repaint) -> std::unique_ptr<Ui::Text::CustomEmoji> {
QStringView data,
const Ui::Text::MarkedContext &context
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto prefix = u"default:"_q;
if (data.startsWith(prefix)) {
const auto &list = owner->reactions().list(
@ -1157,13 +1167,13 @@ Ui::Text::CustomEmojiFactory ReactedMenuFactory(
std::make_unique<Ui::Text::ShiftedEmoji>(
owner->customEmojiManager().create(
document,
std::move(repaint),
context.repaint,
tag,
size),
QPoint(skip, skip)));
}
}
return owner->customEmojiManager().create(data, std::move(repaint));
return owner->customEmojiManager().create(data, context.repaint);
};
}

View file

@ -100,6 +100,7 @@ public:
[[nodiscard]] uint64 coloredSetId() const;
[[nodiscard]] TextWithEntities creditsEmoji(QMargins padding = {});
[[nodiscard]] TextWithEntities ministarEmoji(QMargins padding = {});
private:
static constexpr auto kSizeCount = int(SizeTag::kCount);

View file

@ -789,7 +789,3 @@ dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px);
dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) {
minWidth: 128px;
}
foldersMenu: Menu(menuWithIcons) {
itemPadding: margins(54px, 8px, 44px, 8px);
}

View file

@ -4320,10 +4320,7 @@ QImage *InnerWidget::cacheChatsFilterTag(
const auto color = Ui::EmptyUserpic::UserpicColor(colorIndex).color2;
entry.context.color = color->c;
entry.context.active = active;
entry.context.textContext = Core::MarkedTextContext{
.session = &session(),
.customEmojiRepaint = [] {},
};
entry.context.textContext = Core::TextContext({ .session = &session() });
entry.frame = Ui::ChatsFilterTag(roundedText, entry.context);
return &entry.frame;
}

View file

@ -60,11 +60,10 @@ namespace {
st::dialogsSearchTagArrow,
st::dialogsSearchTagArrowPadding));
auto result = Ui::Text::String();
const auto context = Core::MarkedTextContext{
const auto context = Core::TextContext({
.session = &owner->session(),
.customEmojiRepaint = [] {},
.customEmojiLoopLimit = 1,
};
});
const auto attempt = [&](const auto &phrase) {
result.setMarkedText(
st::dialogsSearchTagPromo,

Some files were not shown because too many files have changed in this diff Show more