diff --git a/.devcontainer.json b/.devcontainer.json index 30bff840d..366f6ed46 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -9,10 +9,7 @@ "--compile-commands-dir=${workspaceFolder}/out" ], "cmake.generator": "Ninja Multi-Config", - "cmake.buildDirectory": "${workspaceFolder}/out", - "cmake.configureSettings": { - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" - } + "cmake.buildDirectory": "${workspaceFolder}/out" }, "extensions": [ "ms-vscode.cpptools-extension-pack", diff --git a/Telegram/Resources/icons/chat/mini_info_alert.png b/Telegram/Resources/icons/chat/mini_info_alert.png new file mode 100644 index 000000000..8c79b97d3 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_info_alert.png differ diff --git a/Telegram/Resources/icons/chat/mini_info_alert@2x.png b/Telegram/Resources/icons/chat/mini_info_alert@2x.png new file mode 100644 index 000000000..e36a5ed41 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_info_alert@2x.png differ diff --git a/Telegram/Resources/icons/chat/mini_info_alert@3x.png b/Telegram/Resources/icons/chat/mini_info_alert@3x.png new file mode 100644 index 000000000..1c8cfe8ed Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_info_alert@3x.png differ diff --git a/Telegram/Resources/icons/payments/premium_emoji.png b/Telegram/Resources/icons/payments/premium_emoji.png new file mode 100644 index 000000000..e8b8fcf29 Binary files /dev/null and b/Telegram/Resources/icons/payments/premium_emoji.png differ diff --git a/Telegram/Resources/icons/payments/premium_emoji@2x.png b/Telegram/Resources/icons/payments/premium_emoji@2x.png new file mode 100644 index 000000000..8824f11b9 Binary files /dev/null and b/Telegram/Resources/icons/payments/premium_emoji@2x.png differ diff --git a/Telegram/Resources/icons/payments/premium_emoji@3x.png b/Telegram/Resources/icons/payments/premium_emoji@3x.png new file mode 100644 index 000000000..5bd6ad013 Binary files /dev/null and b/Telegram/Resources/icons/payments/premium_emoji@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1cf93bef0..6b8fbbe24 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1218,6 +1218,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_contacts" = "My contacts"; "lng_edit_privacy_close_friends" = "Close friends"; "lng_edit_privacy_contacts_and_premium" = "Contacts & Premium"; +"lng_edit_privacy_paid" = "Paid"; "lng_edit_privacy_contacts_and_miniapps" = "Contacts & Mini Apps"; "lng_edit_privacy_nobody" = "Nobody"; "lng_edit_privacy_premium" = "Premium users"; @@ -1356,6 +1357,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_messages_privacy_premium_about" = "Subscribe now to change this setting and get access to other exclusive features of Telegram Premium."; "lng_messages_privacy_premium" = "Only subscribers of {link} can select this option."; "lng_messages_privacy_premium_link" = "Telegram Premium"; +"lng_messages_privacy_charge" = "Charge for messages"; +"lng_messages_privacy_charge_about" = "Charge a fee for messages from people outside your contacts or those you haven't messaged first."; +"lng_messages_privacy_price" = "Set your price per message"; +"lng_messages_privacy_price_about" = "You will receive {percent} of the selected fee ({amount}) for each incoming message."; +"lng_messages_privacy_exceptions" = "Exceptions"; +"lng_messages_privacy_remove_fee" = "Remove Fee"; +"lng_messages_privacy_remove_about" = "Add users or entire groups who won't be charged for sending messages to you."; "lng_self_destruct_title" = "Account self-destruction"; "lng_self_destruct_description" = "If you don't come online at least once within this period, your account will be deleted along with all groups, messages and contacts."; @@ -2158,6 +2166,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_boost_apply#other" = "{from} boosted the group {count} times"; "lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?"; "lng_action_payment_refunded" = "{peer} refunded {amount}"; +"lng_action_paid_message_sent#one" = "You paid {count} Star to {action}"; +"lng_action_paid_message_sent#other" = "You paid {count} Stars to {action}"; +"lng_action_paid_message_one" = "send a message"; +"lng_action_paid_message_some#one" = "send {count} message"; +"lng_action_paid_message_some#other" = "send {count} messages"; +"lng_action_paid_message_got#one" = "You received {count} Star from {name}"; +"lng_action_paid_message_got#other" = "You received {count} Stars from {name}"; +"lng_you_paid_stars#one" = "You paid {count} Star."; +"lng_you_paid_stars#other" = "You paid {count} Stars."; "lng_similar_channels_title" = "Similar channels"; "lng_similar_channels_view_all" = "View all"; @@ -2664,6 +2681,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_history_entry_inner_in" = "In-App Purchase"; "lng_credits_summary_balance" = "Balance"; "lng_credits_commission" = "{amount} commission"; +"lng_credits_paid_messages_fee#one" = "Fee for {count} Message"; +"lng_credits_paid_messages_fee#other" = "Fee for {count} Messages"; +"lng_credits_paid_messages_fee_about" = "You receive {percent} of the price that you charge for each incoming message. {link}"; +"lng_credits_paid_messages_fee_about_link" = "Change Fee {emoji}"; +"lng_credits_paid_messages_full" = "Full Price"; +"lng_credits_premium_gift_duration" = "Duration"; "lng_credits_more_options" = "More Options"; "lng_credits_balance_me" = "your balance"; "lng_credits_buy_button" = "Buy More Stars"; @@ -2777,6 +2800,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts."; "lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels."; "lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts."; +"lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}."; +"lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages."; "lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; "lng_credits_enough" = "You have enough stars at the moment. {link}"; @@ -3310,6 +3335,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}"; "lng_gift_premium_features" = "See Features >"; "lng_gift_premium_label" = "Premium"; +"lng_gift_premium_by_stars" = "or {amount}"; "lng_gift_stars_subtitle" = "Gift Stars"; "lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}"; "lng_gift_stars_link" = "What are Stars >"; @@ -3321,8 +3347,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_send_title" = "Send a Gift"; "lng_gift_send_message" = "Enter Message"; "lng_gift_send_anonymous" = "Hide My Name"; +"lng_gift_send_pay_with_stars" = "Pay with {amount}"; +"lng_gift_send_stars_balance" = "Your balance is {amount}. {link}"; +"lng_gift_send_stars_balance_link" = "Get More Stars >"; "lng_gift_send_anonymous_self" = "Hide my name and message from visitors to my profile."; "lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message."; +"lng_gift_send_anonymous_about_paid" = "You can hide your name from visitors to {user}'s profile. {recipient} will still see your name."; "lng_gift_send_anonymous_about_channel" = "You can hide your name and message from all visitors of this channel except its admins."; "lng_gift_send_unique" = "Make Unique for {price}"; "lng_gift_send_unique_about" = "Enable this to let {user} turn your gift into a unique collectible. {link}"; @@ -3398,6 +3428,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_display_done_channel" = "The gift is now shown in channel's Gifts."; "lng_gift_display_done_hide" = "The gift is now hidden from your profile page."; "lng_gift_display_done_hide_channel" = "The gift is now hidden from channel's Gifts."; +"lng_gift_pinned_done" = "The gift will always be shown on top."; "lng_gift_got_stars#one" = "You got **{count} Star** for this gift."; "lng_gift_got_stars#other" = "You got **{count} Stars** for this gift."; "lng_gift_channel_got#one" = "Channel got **{count} Star** for this gift."; @@ -3456,6 +3487,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_transfer_button_for" = "Transfer for {price}"; "lng_gift_transfer_wear" = "Wear"; "lng_gift_transfer_take_off" = "Take Off"; +"lng_gift_menu_show" = "Show"; +"lng_gift_menu_hide" = "Hide"; "lng_gift_wear_title" = "Wear {name}"; "lng_gift_wear_about" = "and get these benefits:"; "lng_gift_wear_badge_title" = "Radiant Badge"; @@ -3609,6 +3642,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_new_contact_from_request_group" = "{user} is an admin of {name}, a group you requested to join."; "lng_new_contact_about_status" = "This account uses {emoji} as a custom status next to its\nname. Such emoji statuses are available to all\nsubscribers of {link}."; "lng_new_contact_about_status_link" = "Telegram Premium"; +"lng_new_contact_not_contact" = "Not a contact"; +"lng_new_contact_phone_number" = "Phone number"; +"lng_new_contact_registration" = "Registration"; +"lng_new_contact_common_groups" = "Common groups"; +"lng_new_contact_groups#one" = "{count} group {emoji} {arrow}"; +"lng_new_contact_groups#other" = "{count} groups {emoji} {arrow}"; +"lng_new_contact_not_official" = "Not an official account"; +"lng_new_contact_updated_name" = "User updated name {when}"; +"lng_new_contact_updated_photo" = "User updated photo {when}"; +"lng_new_contact_updated_now" = "less than an hour ago"; +"lng_new_contact_updated_hours#one" = "{count} hour ago"; +"lng_new_contact_updated_hours#other" = "{count} hours ago"; +"lng_new_contact_updated_days#one" = "{count} day ago"; +"lng_new_contact_updated_days#other" = "{count} days ago"; +"lng_new_contact_updated_months#one" = "{count} month ago"; +"lng_new_contact_updated_months#other" = "{count} months ago"; "lng_from_request_title_channel" = "Response to your join request"; "lng_from_request_title_group" = "Response to your join request"; "lng_from_request_body" = "You received this message because you requested to join {name} on {date}."; @@ -3637,6 +3686,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_anonymous_ph" = "Send anonymously..."; "lng_story_reply_ph" = "Reply privately..."; "lng_story_comment_ph" = "Comment story..."; +"lng_message_paid_ph" = "Message for {amount}"; "lng_send_text_no" = "Text not allowed."; "lng_send_text_no_about" = "The admins of this group only allow sending {types}."; "lng_send_text_type_and_last" = "{types} and {last}"; @@ -4798,6 +4848,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_boosts_no_restrict" = "Do not restrict boosters"; "lng_rights_boosts_about" = "Turn this on to always allow users who boosted your group to send messages and media."; "lng_rights_boosts_about_on" = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages."; +"lng_rights_charge_stars" = "Charge Stars for Messages"; +"lng_rights_charge_stars_about" = "If you turn this on, regular members of the group will have to pay Stars to send messages."; +"lng_rights_charge_price" = "Set price per message"; +"lng_rights_charge_price_about" = "Your group will receive {percent} of the selected fee ({amount}) for each incoming message."; "lng_slowmode_enabled" = "Slow Mode is active.\nYou can send your next message in {left}."; "lng_slowmode_no_many" = "Slow mode is enabled. You can't send more than one message at a time."; @@ -4805,6 +4859,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_slowmode_seconds#one" = "{count} second"; "lng_slowmode_seconds#other" = "{count} seconds"; +"lng_payment_confirm_title" = "Confirm payment"; +"lng_payment_confirm_text#one" = "{name} charges **{count}** Star per message."; +"lng_payment_confirm_text#other" = "{name} charges **{count}** Stars per message."; +"lng_payment_confirm_amount#one" = "**{count}** Star"; +"lng_payment_confirm_amount#other" = "**{count}** Stars"; +"lng_payment_confirm_users#one" = "You selected **{count}** user who charge Stars for messages."; +"lng_payment_confirm_users#other" = "You selected **{count}** users who charge Stars for messages."; +"lng_payment_confirm_chats#one" = "You selected **{count}** chat where you pay Stars for messages."; +"lng_payment_confirm_chats#other" = "You selected **{count}** chats where you pay Stars for messages."; +"lng_payment_confirm_sure#one" = "Would you like to pay {amount} to send **{count}** message?"; +"lng_payment_confirm_sure#other" = "Would you like to pay {amount} to send **{count}** messages?"; +"lng_payment_confirm_dont_ask" = "Don't ask me again"; +"lng_payment_confirm_button#one" = "Pay for {count} Message"; +"lng_payment_confirm_button#other" = "Pay for {count} Messages"; +"lng_payment_bar_text" = "{name} must pay {cost} for each message to you."; +"lng_payment_bar_button" = "Remove Fee"; +"lng_payment_refund_title" = "Remove Fee"; +"lng_payment_refund_text" = "Are you sure you want to allow {name} to message you for free?"; +"lng_payment_refund_also#one" = "Refund already paid {count} Star"; +"lng_payment_refund_also#other" = "Refund already paid {count} Stars"; +"lng_payment_refund_confirm" = "Confirm"; + "lng_rights_gigagroup_title" = "Broadcast group"; "lng_rights_gigagroup_convert" = "Convert to Broadcast Group"; "lng_rights_gigagroup_about" = "Broadcast groups can have over 200,000 members, but only admins can send messages in them."; @@ -4936,6 +5012,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers."; "lng_send_non_premium_message_toast_link" = "Telegram Premium"; +"lng_send_charges_stars_text" = "{user} charges {amount} for each message."; +"lng_send_charges_stars_go" = "Buy Stars"; + "lng_exceptions_list_title" = "Exceptions"; "lng_removed_list_title" = "Removed users"; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index f6451e78d..65aaca6cc 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.12.1.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index a5a4b7eaa..21f7f0286 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,11,1,0 - PRODUCTVERSION 5,11,1,0 + FILEVERSION 5,12,1,0 + PRODUCTVERSION 5,12,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.11.1.0" + VALUE "FileVersion", "5.12.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.11.1.0" + VALUE "ProductVersion", "5.12.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 783f6f30f..6f72c4477 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,11,1,0 - PRODUCTVERSION 5,11,1,0 + FILEVERSION 5,12,1,0 + PRODUCTVERSION 5,12,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "5.11.1.0" + VALUE "FileVersion", "5.12.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.11.1.0" + VALUE "ProductVersion", "5.12.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index d7a1636b3..55bbabbc5 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -149,18 +149,14 @@ void InitFilterLinkHeader( iconEmoji ).value_or(Ui::FilterIcon::Custom)).active; const auto isStatic = title.isStatic; - const auto makeContext = [=](Fn repaint) { - return Core::MarkedTextContext{ - .session = &box->peerListUiShow()->session(), - .customEmojiRepaint = std::move(repaint), - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; auto header = Ui::MakeFilterLinkHeader(box, { .type = type, .title = TitleText(type)(tr::now), .about = AboutText(type, title.text), - .makeAboutContext = makeContext, + .aboutContext = Core::TextContext({ + .session = &box->peerListUiShow()->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }), .folderTitle = title.text, .folderIcon = icon, .badge = (type == Ui::FilterLinkHeaderType::AddingChats @@ -560,16 +556,12 @@ void ShowImportToast( text.append('\n').append(phrase(tr::now, lt_count, added)); } const auto isStatic = title.isStatic; - const auto makeContext = [=](not_null widget) { - return Core::MarkedTextContext{ - .session = &strong->session(), - .customEmojiRepaint = [=] { widget->update(); }, - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; strong->showToast({ .text = std::move(text), - .textContext = makeContext, + .textContext = Core::TextContext({ + .session = &strong->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }) }); } @@ -640,18 +632,14 @@ void ProcessFilterInvite( raw->setRealContentHeight(box->heightValue()); const auto isStatic = title.isStatic; - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = &strong->session(), - .customEmojiRepaint = update, - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; auto owned = Ui::FilterLinkProcessButton( box, type, title.text, - makeContext, + Core::TextContext({ + .session = &strong->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }), std::move(badge)); const auto button = owned.data(); @@ -873,18 +861,14 @@ void ProcessFilterRemove( }, type, title, iconEmoji, rpl::single(0), horizontalFilters); const auto isStatic = title.isStatic; - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = &strong->session(), - .customEmojiRepaint = update, - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; auto owned = Ui::FilterLinkProcessButton( box, type, title.text, - makeContext, + Core::TextContext({ + .session = &strong->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }), std::move(badge)); const auto button = owned.data(); diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 77c30d095..c58f525c9 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -25,6 +25,7 @@ struct SendOptions { TimeId scheduled = 0; BusinessShortcutId shortcutId = 0; EffectId effectId = 0; + int starsApproved = 0; bool silent = false; bool handleSupportSwitch = false; bool invertCaption = false; diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index bdb10d02d..0dc21c636 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -90,7 +90,13 @@ constexpr auto kTransactionsLimit = 100; ? peerFromMTP(*tl.data().vstarref_peer()).value : 0; const auto incoming = (amount >= StarsAmount()); - const auto saveActorId = (reaction || !extended.empty()) && incoming; + const auto paidMessagesCount + = tl.data().vpaid_messages().value_or_empty(); + const auto premiumMonthsForStars + = tl.data().vpremium_gift_months().value_or_empty(); + const auto saveActorId = (reaction + || !extended.empty() + || paidMessagesCount) && incoming; const auto parsedGift = stargift ? FromTL(&peer->session(), *stargift) : std::optional(); @@ -110,9 +116,9 @@ constexpr auto kTransactionsLimit = 100; .bareGiftStickerId = giftStickerId, .bareActorId = saveActorId ? barePeerId : uint64(0), .uniqueGift = parsedGift ? parsedGift->unique : nullptr, - .starrefAmount = starrefAmount, - .starrefCommission = starrefCommission, - .starrefRecipientId = starrefBarePeerId, + .starrefAmount = paidMessagesCount ? StarsAmount() : starrefAmount, + .starrefCommission = paidMessagesCount ? 0 : starrefCommission, + .starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId, .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { return Data::CreditsHistoryEntry::PeerType::Peer; }, [](const MTPDstarsTransactionPeerPlayMarket &) { @@ -138,9 +144,15 @@ constexpr auto kTransactionsLimit = 100; ? base::unixtime::parse(tl.data().vtransaction_date()->v) : QDateTime(), .successLink = qs(tl.data().vtransaction_url().value_or_empty()), + .paidMessagesCount = paidMessagesCount, + .paidMessagesAmount = (paidMessagesCount + ? starrefAmount + : StarsAmount()), + .paidMessagesCommission = paidMessagesCount ? starrefCommission : 0, .starsConverted = int(nonUniqueGift ? nonUniqueGift->vconvert_stars().v : 0), + .premiumMonthsForStars = premiumMonthsForStars, .floodSkip = int(tl.data().vfloodskip_number().value_or(0)), .converted = stargift && incoming, .stargift = stargift.has_value(), diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index 9e2ab1a38..90c56ae4e 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -114,7 +114,8 @@ void GlobalPrivacy::updateHideReadTime(bool hide) { archiveAndMuteCurrent(), unarchiveOnNewMessageCurrent(), hide, - newRequirePremiumCurrent()); + newRequirePremiumCurrent(), + newChargeStarsCurrent()); } bool GlobalPrivacy::hideReadTimeCurrent() const { @@ -125,14 +126,6 @@ rpl::producer GlobalPrivacy::hideReadTime() const { return _hideReadTime.value(); } -void GlobalPrivacy::updateNewRequirePremium(bool value) { - update( - archiveAndMuteCurrent(), - unarchiveOnNewMessageCurrent(), - hideReadTimeCurrent(), - value); -} - bool GlobalPrivacy::newRequirePremiumCurrent() const { return _newRequirePremium.current(); } @@ -141,6 +134,25 @@ rpl::producer GlobalPrivacy::newRequirePremium() const { return _newRequirePremium.value(); } +int GlobalPrivacy::newChargeStarsCurrent() const { + return _newChargeStars.current(); +} + +rpl::producer GlobalPrivacy::newChargeStars() const { + return _newChargeStars.value(); +} + +void GlobalPrivacy::updateMessagesPrivacy( + bool requirePremium, + int chargeStars) { + update( + archiveAndMuteCurrent(), + unarchiveOnNewMessageCurrent(), + hideReadTimeCurrent(), + requirePremium, + chargeStars); +} + void GlobalPrivacy::loadPaidReactionShownPeer() { if (_paidReactionShownPeerLoaded) { return; @@ -169,7 +181,8 @@ void GlobalPrivacy::updateArchiveAndMute(bool value) { value, unarchiveOnNewMessageCurrent(), hideReadTimeCurrent(), - newRequirePremiumCurrent()); + newRequirePremiumCurrent(), + newChargeStarsCurrent()); } void GlobalPrivacy::updateUnarchiveOnNewMessage( @@ -178,14 +191,16 @@ void GlobalPrivacy::updateUnarchiveOnNewMessage( archiveAndMuteCurrent(), value, hideReadTimeCurrent(), - newRequirePremiumCurrent()); + newRequirePremiumCurrent(), + newChargeStarsCurrent()); } void GlobalPrivacy::update( bool archiveAndMute, UnarchiveOnNewMessage unarchiveOnNewMessage, bool hideReadTime, - bool newRequirePremium) { + bool newRequirePremium, + int newChargeStars) { using Flag = MTPDglobalPrivacySettings::Flag; _api.request(_requestId).cancel(); @@ -204,35 +219,44 @@ void GlobalPrivacy::update( | (hideReadTime ? Flag::f_hide_read_marks : Flag()) | ((newRequirePremium && newRequirePremiumAllowed) ? Flag::f_new_noncontact_peers_require_premium - : Flag()); + : Flag()) + | Flag::f_noncontact_peers_paid_stars; _requestId = _api.request(MTPaccount_SetGlobalPrivacySettings( - MTP_globalPrivacySettings(MTP_flags(flags)) + MTP_globalPrivacySettings( + MTP_flags(flags), + MTP_long(newChargeStars)) )).done([=](const MTPGlobalPrivacySettings &result) { _requestId = 0; apply(result); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) { - update(archiveAndMute, unarchiveOnNewMessage, hideReadTime, {}); + update( + archiveAndMute, + unarchiveOnNewMessage, + hideReadTime, + false, + 0); } }).send(); _archiveAndMute = archiveAndMute; _unarchiveOnNewMessage = unarchiveOnNewMessage; _hideReadTime = hideReadTime; _newRequirePremium = newRequirePremium; + _newChargeStars = newChargeStars; } -void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) { - data.match([&](const MTPDglobalPrivacySettings &data) { - _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers(); - _unarchiveOnNewMessage = data.is_keep_archived_unmuted() - ? UnarchiveOnNewMessage::None - : data.is_keep_archived_folders() - ? UnarchiveOnNewMessage::NotInFoldersUnmuted - : UnarchiveOnNewMessage::AnyUnmuted; - _hideReadTime = data.is_hide_read_marks(); - _newRequirePremium = data.is_new_noncontact_peers_require_premium(); - }); +void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) { + const auto &data = settings.data(); + _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers(); + _unarchiveOnNewMessage = data.is_keep_archived_unmuted() + ? UnarchiveOnNewMessage::None + : data.is_keep_archived_folders() + ? UnarchiveOnNewMessage::NotInFoldersUnmuted + : UnarchiveOnNewMessage::AnyUnmuted; + _hideReadTime = data.is_hide_read_marks(); + _newRequirePremium = data.is_new_noncontact_peers_require_premium(); + _newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty(); } } // namespace Api diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index 6c5848961..6b0fc064b 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -49,23 +49,28 @@ public: [[nodiscard]] bool hideReadTimeCurrent() const; [[nodiscard]] rpl::producer hideReadTime() const; - void updateNewRequirePremium(bool value); [[nodiscard]] bool newRequirePremiumCurrent() const; [[nodiscard]] rpl::producer newRequirePremium() const; + [[nodiscard]] int newChargeStarsCurrent() const; + [[nodiscard]] rpl::producer newChargeStars() const; + + void updateMessagesPrivacy(bool requirePremium, int chargeStars); + void loadPaidReactionShownPeer(); void updatePaidReactionShownPeer(PeerId shownPeer); [[nodiscard]] PeerId paidReactionShownPeerCurrent() const; [[nodiscard]] rpl::producer paidReactionShownPeer() const; private: - void apply(const MTPGlobalPrivacySettings &data); + void apply(const MTPGlobalPrivacySettings &settings); void update( bool archiveAndMute, UnarchiveOnNewMessage unarchiveOnNewMessage, bool hideReadTime, - bool newRequirePremium); + bool newRequirePremium, + int newChargeStars); const not_null _session; MTP::Sender _api; @@ -76,6 +81,7 @@ private: rpl::variable _showArchiveAndMute = false; rpl::variable _hideReadTime = false; rpl::variable _newRequirePremium = false; + rpl::variable _newChargeStars = 0; rpl::variable _paidReactionShownPeer = false; std::vector> _callbacks; bool _paidReactionShownPeerLoaded = false; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index a56f25d0e..8e41f95cd 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -42,7 +42,7 @@ Polls::Polls(not_null api) void Polls::create( const PollData &data, - const SendAction &action, + SendAction action, Fn done, Fn fail) { _session->api().sendAction(action); @@ -64,6 +64,9 @@ void Polls::create( history->startSavingCloudDraft(topicRootId); } const auto silentPost = ShouldSendSilent(peer, action.options); + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } @@ -76,6 +79,10 @@ void Polls::create( if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; + } const auto sendAs = action.options.sendAs; if (sendAs) { sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; @@ -98,7 +105,8 @@ void Polls::create( MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), - MTP_long(action.options.effectId) + MTP_long(action.options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h index 2ff08a1ac..f77e34d67 100644 --- a/Telegram/SourceFiles/api/api_polls.h +++ b/Telegram/SourceFiles/api/api_polls.h @@ -27,7 +27,7 @@ public: void create( const PollData &data, - const SendAction &action, + SendAction action, Fn done, Fn fail); void sendVotes( diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 3bd36b40c..d7dbcd37f 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "apiwrap.h" #include "base/random.h" +#include "data/data_channel.h" #include "data/data_document.h" #include "data/data_peer.h" #include "data/data_peer_values.h" @@ -377,15 +378,15 @@ const Data::PremiumSubscriptionOptions &Premium::subscriptionOptions() const { return _subscriptionOptions; } -rpl::producer<> Premium::somePremiumRequiredResolved() const { - return _somePremiumRequiredResolved.events(); +rpl::producer<> Premium::someMessageMoneyRestrictionsResolved() const { + return _someMessageMoneyRestrictionsResolved.events(); } -void Premium::resolvePremiumRequired(not_null user) { - _resolvePremiumRequiredUsers.emplace(user); - if (!_premiumRequiredRequestScheduled - && _resolvePremiumRequestedUsers.empty()) { - _premiumRequiredRequestScheduled = true; +void Premium::resolveMessageMoneyRestrictions(not_null user) { + _resolveMessageMoneyRequiredUsers.emplace(user); + if (!_messageMoneyRequestScheduled + && _resolveMessageMoneyRequestedUsers.empty()) { + _messageMoneyRequestScheduled = true; crl::on_main(_session, [=] { requestPremiumRequiredSlice(); }); @@ -393,50 +394,65 @@ void Premium::resolvePremiumRequired(not_null user) { } void Premium::requestPremiumRequiredSlice() { - _premiumRequiredRequestScheduled = false; - if (!_resolvePremiumRequestedUsers.empty() - || _resolvePremiumRequiredUsers.empty()) { + _messageMoneyRequestScheduled = false; + if (!_resolveMessageMoneyRequestedUsers.empty() + || _resolveMessageMoneyRequiredUsers.empty()) { return; } constexpr auto kPerRequest = 100; - auto users = MTP_vector_from_range(_resolvePremiumRequiredUsers + auto users = MTP_vector_from_range(_resolveMessageMoneyRequiredUsers | ranges::views::transform(&UserData::inputUser)); if (users.v.size() > kPerRequest) { auto shortened = users.v; shortened.resize(kPerRequest); users = MTP_vector(std::move(shortened)); - const auto from = begin(_resolvePremiumRequiredUsers); - _resolvePremiumRequestedUsers = { from, from + kPerRequest }; - _resolvePremiumRequiredUsers.erase(from, from + kPerRequest); + const auto from = begin(_resolveMessageMoneyRequiredUsers); + _resolveMessageMoneyRequestedUsers = { from, from + kPerRequest }; + _resolveMessageMoneyRequiredUsers.erase(from, from + kPerRequest); } else { - _resolvePremiumRequestedUsers - = base::take(_resolvePremiumRequiredUsers); + _resolveMessageMoneyRequestedUsers + = base::take(_resolveMessageMoneyRequiredUsers); } - const auto finish = [=](const QVector &list) { - constexpr auto me = UserDataFlag::MeRequiresPremiumToWrite; - constexpr auto known = UserDataFlag::RequirePremiumToWriteKnown; - constexpr auto mask = me | known; + const auto finish = [=](const QVector &list) { auto index = 0; - for (const auto &user : base::take(_resolvePremiumRequestedUsers)) { - const auto require = (index < list.size()) - && mtpIsTrue(list[index++]); - user->setFlags((user->flags() & ~mask) - | known - | (require ? me : UserDataFlag())); + for (const auto &user : base::take(_resolveMessageMoneyRequestedUsers)) { + const auto set = [&](bool requirePremium, int stars) { + using Flag = UserDataFlag; + constexpr auto me = Flag::RequiresPremiumToWrite; + constexpr auto known = Flag::MessageMoneyRestrictionsKnown; + constexpr auto hasPrem = Flag::HasRequirePremiumToWrite; + constexpr auto hasStars = Flag::HasStarsPerMessage; + user->setStarsPerMessage(stars); + user->setFlags((user->flags() & ~(me | hasPrem | hasStars)) + | known + | (requirePremium ? (me | hasPrem) : Flag()) + | (stars ? hasStars : Flag())); + }; + if (index >= list.size()) { + set(false, 0); + continue; + } + list[index++].match([&](const MTPDrequirementToContactEmpty &) { + set(false, 0); + }, [&](const MTPDrequirementToContactPremium &) { + set(true, 0); + }, [&](const MTPDrequirementToContactPaidMessages &data) { + set(false, data.vstars_amount().v); + }); } - if (!_premiumRequiredRequestScheduled - && !_resolvePremiumRequiredUsers.empty()) { - _premiumRequiredRequestScheduled = true; + if (!_messageMoneyRequestScheduled + && !_resolveMessageMoneyRequiredUsers.empty()) { + _messageMoneyRequestScheduled = true; crl::on_main(_session, [=] { requestPremiumRequiredSlice(); }); } - _somePremiumRequiredResolved.fire({}); + _someMessageMoneyRestrictionsResolved.fire({}); }; _session->api().request( - MTPusers_GetIsPremiumRequiredToContact(std::move(users)) - ).done([=](const MTPVector &result) { + MTPusers_GetRequirementsToContact(std::move(users)) + ).done([=](const MTPVector &result) { finish(result.v); }).fail([=] { finish({}); @@ -463,10 +479,14 @@ rpl::producer PremiumGiftCodeOptions::request() { for (const auto &tlOption : result.v) { const auto &data = tlOption.data(); tlMapOptions[data.vusers().v].push_back(tlOption); + if (qs(data.vcurrency()) == Ui::kCreditsCurrency) { + continue; + } const auto token = Token{ data.vusers().v, data.vmonths().v }; _stores[token] = Store{ .amount = data.vamount().v, + .currency = qs(data.vcurrency()), .product = qs(data.vstore_product().value_or_empty()), .quantity = data.vstore_quantity().value_or_empty(), }; @@ -475,14 +495,14 @@ rpl::producer PremiumGiftCodeOptions::request() { } } for (const auto &[amount, tlOptions] : tlMapOptions) { - if (amount == 1 && _optionsForOnePerson.currency.isEmpty()) { - _optionsForOnePerson.currency = qs( - tlOptions.front().data().vcurrency()); + if (amount == 1 && _optionsForOnePerson.currencies.empty()) { for (const auto &option : tlOptions) { _optionsForOnePerson.months.push_back( option.data().vmonths().v); _optionsForOnePerson.totalCosts.push_back( option.data().vamount().v); + _optionsForOnePerson.currencies.push_back( + qs(option.data().vcurrency())); } } _subscriptionOptions[amount] = GiftCodesFromTL(tlOptions); @@ -509,7 +529,7 @@ rpl::producer PremiumGiftCodeOptions::applyPrepaid( _api.request(MTPpayments_LaunchPrepaidGiveaway( _peer->input, MTP_long(prepaidId), - invoice.creditsAmount + invoice.giveawayCredits ? Payments::InvoiceCreditsGiveawayToTL(invoice) : Payments::InvoicePremiumGiftCodeGiveawayToTL(invoice) )).done([=](const MTPUpdates &result) { @@ -540,7 +560,7 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice( const auto token = Token{ users, months }; const auto &store = _stores[token]; return Payments::InvoicePremiumGiftCode{ - .currency = _optionsForOnePerson.currency, + .currency = store.currency, .storeProduct = store.product, .randomId = randomId, .amount = store.amount, @@ -553,14 +573,15 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice( std::vector PremiumGiftCodeOptions::optionsForPeer() const { auto result = std::vector(); - if (!_optionsForOnePerson.currency.isEmpty()) { + if (!_optionsForOnePerson.currencies.empty()) { const auto count = int(_optionsForOnePerson.months.size()); result.reserve(count); for (auto i = 0; i != count; ++i) { Assert(i < _optionsForOnePerson.totalCosts.size()); + Assert(i < _optionsForOnePerson.currencies.size()); result.push_back({ .cost = _optionsForOnePerson.totalCosts[i], - .currency = _optionsForOnePerson.currency, + .currency = _optionsForOnePerson.currencies[i], .months = _optionsForOnePerson.months[i], }); } @@ -581,7 +602,7 @@ Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) { MTP_int(_optionsForOnePerson.months[i]), MTPstring(), MTPint(), - MTP_string(_optionsForOnePerson.currency), + MTP_string(_optionsForOnePerson.currencies[i]), MTP_long(_optionsForOnePerson.totalCosts[i] * amount))); } _subscriptionOptions[amount] = GiftCodesFromTL(tlOptions); @@ -694,28 +715,38 @@ rpl::producer SponsoredToggle::setToggled(bool v) { }; } -RequirePremiumState ResolveRequiresPremiumToWrite( +MessageMoneyRestriction ResolveMessageMoneyRestrictions( not_null peer, History *maybeHistory) { - const auto user = peer->asUser(); - if (!user - || !user->someRequirePremiumToWrite() - || user->session().premium()) { - return RequirePremiumState::No; - } else if (user->requirePremiumToWriteKnown()) { - return user->meRequiresPremiumToWrite() - ? RequirePremiumState::Yes - : RequirePremiumState::No; - } else if (user->flags() & UserDataFlag::MutualContact) { - return RequirePremiumState::No; - } else if (!maybeHistory) { - return RequirePremiumState::Unknown; + if (const auto channel = peer->asChannel()) { + return { + .starsPerMessage = channel->starsPerMessageChecked(), + .known = true, + }; + } + const auto user = peer->asUser(); + if (!user) { + return { .known = true }; + } else if (user->messageMoneyRestrictionsKnown()) { + return { + .starsPerMessage = user->starsPerMessageChecked(), + .premiumRequired = (user->requiresPremiumToWrite() + && !user->session().premium()), + .known = true, + }; + } else if (user->hasStarsPerMessage()) { + return {}; + } else if (!user->hasRequirePremiumToWrite()) { + return { .known = true }; + } else if (user->flags() & UserDataFlag::MutualContact) { + return { .known = true }; + } else if (!maybeHistory) { + return {}; } - const auto update = [&](bool require) { using Flag = UserDataFlag; - constexpr auto known = Flag::RequirePremiumToWriteKnown; - constexpr auto me = Flag::MeRequiresPremiumToWrite; + constexpr auto known = Flag::MessageMoneyRestrictionsKnown; + constexpr auto me = Flag::RequiresPremiumToWrite; user->setFlags((user->flags() & ~me) | known | (require ? me : Flag())); @@ -727,16 +758,19 @@ RequirePremiumState ResolveRequiresPremiumToWrite( const auto item = view->data(); if (!item->out() && !item->isService()) { update(false); - return RequirePremiumState::No; + return { .known = true }; } } } if (user->isContact() // Here we know, that we're not in his contacts. && maybeHistory->loadedAtTop() // And no incoming messages. && maybeHistory->loadedAtBottom()) { - update(true); + return { + .premiumRequired = !user->session().premium(), + .known = true, + }; } - return RequirePremiumState::Unknown; + return {}; } rpl::producer RandomHelloStickerValue( @@ -870,6 +904,7 @@ std::optional FromTL( .date = data.vdate().v, .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), + .pinned = data.is_pinned_to_top(), .hidden = data.is_unsaved(), .mine = to->isSelf(), }; diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index a7757a490..2b692a484 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -116,8 +116,9 @@ public: [[nodiscard]] auto subscriptionOptions() const -> const Data::PremiumSubscriptionOptions &; - [[nodiscard]] rpl::producer<> somePremiumRequiredResolved() const; - void resolvePremiumRequired(not_null user); + [[nodiscard]] auto someMessageMoneyRestrictionsResolved() const + -> rpl::producer<>; + void resolveMessageMoneyRestrictions(not_null user); private: void reloadPromo(); @@ -166,10 +167,10 @@ private: Data::PremiumSubscriptionOptions _subscriptionOptions; - rpl::event_stream<> _somePremiumRequiredResolved; - base::flat_set> _resolvePremiumRequiredUsers; - base::flat_set> _resolvePremiumRequestedUsers; - bool _premiumRequiredRequestScheduled = false; + rpl::event_stream<> _someMessageMoneyRestrictionsResolved; + base::flat_set> _resolveMessageMoneyRequiredUsers; + base::flat_set> _resolveMessageMoneyRequestedUsers; + bool _messageMoneyRequestScheduled = false; }; @@ -208,6 +209,7 @@ private: }; struct Store final { uint64 amount = 0; + QString currency; QString product; int quantity = 0; }; @@ -218,7 +220,7 @@ private: struct { std::vector months; std::vector totalCosts; - QString currency; + std::vector currencies; } _optionsForOnePerson; std::vector _availablePresets; @@ -244,12 +246,20 @@ private: }; -enum class RequirePremiumState { - Unknown, - Yes, - No, +struct MessageMoneyRestriction { + int starsPerMessage = 0; + bool premiumRequired = false; + bool known = false; + + explicit operator bool() const { + return starsPerMessage != 0 || premiumRequired; + } + + friend inline bool operator==( + const MessageMoneyRestriction &, + const MessageMoneyRestriction &) = default; }; -[[nodiscard]] RequirePremiumState ResolveRequiresPremiumToWrite( +[[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions( not_null peer, History *maybeHistory); diff --git a/Telegram/SourceFiles/api/api_premium_option.cpp b/Telegram/SourceFiles/api/api_premium_option.cpp index bd3056a75..d3c67e23b 100644 --- a/Telegram/SourceFiles/api/api_premium_option.cpp +++ b/Telegram/SourceFiles/api/api_premium_option.cpp @@ -26,7 +26,7 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption( }(); return { .duration = Ui::FormatTTL(months * 86400 * 31), - .discount = discount + .discount = (discount > 0) ? QString::fromUtf8("\xe2\x88\x92%1%").arg(discount) : QString(), .costPerMonth = Ui::FillAmountAndCurrency( diff --git a/Telegram/SourceFiles/api/api_premium_option.h b/Telegram/SourceFiles/api/api_premium_option.h index afe66ac4a..2648a7f9c 100644 --- a/Telegram/SourceFiles/api/api_premium_option.h +++ b/Telegram/SourceFiles/api/api_premium_option.h @@ -24,15 +24,26 @@ template if (tlOpts.isEmpty()) { return {}; } + auto monthlyAmountPerCurrency = base::flat_map(); auto result = Data::PremiumSubscriptionOptions(); - const auto monthlyAmount = [&] { + const auto monthlyAmount = [&](const QString ¤cy) -> int { + const auto it = monthlyAmountPerCurrency.find(currency); + if (it != end(monthlyAmountPerCurrency)) { + return it->second; + } const auto &min = ranges::min_element( tlOpts, ranges::less(), - [](const Option &o) { return o.data().vamount().v; } + [&](const Option &o) { + return currency == qs(o.data().vcurrency()) + ? o.data().vamount().v + : std::numeric_limits::max(); + } )->data(); - return min.vamount().v / float64(min.vmonths().v); - }(); + const auto monthly = min.vamount().v / float64(min.vmonths().v); + monthlyAmountPerCurrency.emplace(currency, monthly); + return monthly; + }; result.reserve(tlOpts.size()); for (const auto &tlOption : tlOpts) { const auto &option = tlOption.data(); @@ -45,7 +56,7 @@ template const auto currency = qs(option.vcurrency()); result.push_back(CreateSubscriptionOption( months, - monthlyAmount, + monthlyAmount(currency), amount, currency, botUrl)); diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 80b6f1007..814b0a983 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -95,7 +95,9 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) { const auto messagePostAuthor = peer->isBroadcast() ? session->user()->name() : QString(); - + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; @@ -111,6 +113,10 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) { flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; } + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; + } auto &histories = history->owner().histories(); histories.sendPreparedMessage( @@ -129,7 +135,8 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) { MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(session, action.options.shortcutId), - MTP_long(action.options.effectId) + MTP_long(action.options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId); @@ -160,7 +167,7 @@ void SendExistingMedia( ? (*localMessageId) : session->data().nextLocalMessageId()); const auto randomId = base::RandomValue(); - const auto &action = message.action; + auto &action = message.action; auto flags = NewMessageFlags(peer); auto sendFlags = MTPmessages_SendMedia::Flags(0); @@ -190,7 +197,9 @@ void SendExistingMedia( sendFlags |= MTPmessages_SendMedia::Flag::f_entities; } const auto captionText = caption.text; - + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; @@ -206,6 +215,10 @@ void SendExistingMedia( flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; } + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; + } session->data().registerMessageRandomId(randomId, newId); @@ -216,6 +229,7 @@ void SendExistingMedia( .replyTo = action.replyTo, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, + .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, }, media, caption); @@ -240,7 +254,8 @@ void SendExistingMedia( MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(session, action.options.shortcutId), - MTP_long(action.options.effectId) + MTP_long(action.options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -341,7 +356,7 @@ bool SendDice(MessageToSend &message) { message.action.generateLocal = true; - const auto &action = message.action; + auto &action = message.action; api->sendAction(action); const auto newId = FullMsgId( @@ -380,6 +395,13 @@ bool SendDice(MessageToSend &message) { flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; } + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; + } session->data().registerMessageRandomId(randomId, newId); @@ -390,6 +412,7 @@ bool SendDice(MessageToSend &message) { .replyTo = action.replyTo, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, + .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, }, TextWithEntities(), MTP_messageMediaDice( @@ -411,7 +434,8 @@ bool SendDice(MessageToSend &message) { MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(session, action.options.shortcutId), - MTP_long(action.options.effectId) + MTP_long(action.options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); @@ -610,6 +634,9 @@ void SendConfirmedFile( .replyTo = file->to.replyTo, .date = NewMessageDate(file->to.options), .shortcutId = file->to.options.shortcutId, + .starsPaid = std::min( + history->peer->starsPerMessageChecked(), + file->to.options.starsApproved), .postAuthor = NewMessagePostAuthor(action), .groupedId = groupId, .effectId = file->to.options.effectId, diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index e34ef373d..9c959bbf9 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1227,7 +1227,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPint(), // quick_reply_shortcut_id MTPlong(), // effect MTPFactCheck(), - MTPint()), // report_delivery_until_date + MTPint(), // report_delivery_until_date + MTPlong()), // paid_message_stars MessageFlags(), NewMessageType::Unread); } break; @@ -1265,7 +1266,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPint(), // quick_reply_shortcut_id MTPlong(), // effect MTPFactCheck(), - MTPint()), // report_delivery_until_date + MTPint(), // report_delivery_until_date + MTPlong()), // paid_message_stars MessageFlags(), NewMessageType::Unread); } break; diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index 78f863bed..e07858dda 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -210,6 +210,7 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) { case Key::About: return MTP_inputPrivacyKeyAbout(); case Key::Birthday: return MTP_inputPrivacyKeyBirthday(); case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave(); + case Key::NoPaidMessages: return MTP_inputPrivacyKeyNoPaidMessages(); } Unexpected("Key in Api::UserPrivacy::KetToTL."); } @@ -241,6 +242,8 @@ std::optional TLToKey(mtpTypeId type) { case mtpc_inputPrivacyKeyBirthday: return Key::Birthday; case mtpc_privacyKeyStarGiftsAutoSave: case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave; + case mtpc_privacyKeyNoPaidMessages: + case mtpc_inputPrivacyKeyNoPaidMessages: return Key::NoPaidMessages; } return std::nullopt; } diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index a1f66189f..676e9be11 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -32,6 +32,7 @@ public: About, Birthday, GiftsAutoSave, + NoPaidMessages, }; enum class Option { Everyone, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 86a10680a..7d2c1c31f 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -546,6 +546,7 @@ void ApiWrap::sendMessageFail( uint64 randomId, FullMsgId itemId) { const auto show = ShowForPeer(peer); + const auto paidStarsPrefix = u"ALLOW_PAYMENT_REQUIRED_"_q; if (show && error == u"PEER_FLOOD"_q) { show->showBox( Ui::MakeInformBox( @@ -600,6 +601,19 @@ void ApiWrap::sendMessageFail( if (show) { show->showToast(tr::lng_error_schedule_limit(tr::now)); } + } else if (error.startsWith(paidStarsPrefix)) { + if (show) { + show->showToast( + u"Payment requirements changed. Please, try again."_q); + } + if (const auto stars = error.mid(paidStarsPrefix.size()).toInt()) { + if (const auto user = peer->asUser()) { + user->setStarsPerMessage(stars); + } else if (const auto channel = peer->asChannel()) { + channel->setStarsPerMessage(stars); + } + } + peer->updateFull(); } if (const auto item = _session->data().message(itemId)) { Assert(randomId != 0); @@ -3342,7 +3356,7 @@ void ApiWrap::finishForwarding(const SendAction &action) { void ApiWrap::forwardMessages( Data::ResolvedForwardDraft &&draft, - const SendAction &action, + SendAction action, FnMut &&successCallback) { Expects(!draft.items.empty()); @@ -3417,9 +3431,17 @@ void ApiWrap::forwardMessages( const auto requestType = Data::Histories::RequestType::Send; const auto idsCopy = localIds; const auto scheduled = action.options.scheduled; + const auto starsPaid = std::min( + action.options.starsApproved, + int(ids.size() * peer->starsPerMessageChecked())); + auto oneFlags = sendFlags; + if (starsPaid) { + action.options.starsApproved -= starsPaid; + oneFlags |= SendFlag::f_allow_paid_stars; + } histories.sendRequest(history, requestType, [=](Fn finish) { history->sendRequestId = request(MTPmessages_ForwardMessages( - MTP_flags(sendFlags), + MTP_flags(oneFlags), forwardFrom->input, MTP_vector(ids), MTP_vector(randomIds), @@ -3428,7 +3450,8 @@ void ApiWrap::forwardMessages( MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), - MTPint() // video_timestamp + MTPint(), // video_timestamp + MTP_long(starsPaid) )).done([=](const MTPUpdates &result) { if (!scheduled) { this->updates().checkForSentToScheduled(result); @@ -3480,6 +3503,7 @@ void ApiWrap::forwardMessages( .replyTo = { .topicRootId = topMsgId }, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, + .starsPaid = action.options.starsApproved, .postAuthor = NewMessagePostAuthor(action), // forwarded messages don't have effects @@ -3573,6 +3597,7 @@ void ApiWrap::sendSharedContact( .replyTo = action.replyTo, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, + .starsPaid = action.options.starsApproved, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, }, TextWithEntities(), MTP_messageMediaContact( @@ -3944,6 +3969,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_effect; mediaFlags |= MTPmessages_SendMedia::Flag::f_effect; } + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= MTPmessages_SendMessage::Flag::f_allow_paid_stars; + mediaFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; + } lastMessage = history->addNewLocalMessage({ .id = newId.msg, .flags = flags, @@ -3951,6 +3984,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { .replyTo = action.replyTo, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, + .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, }, sending, media); @@ -4001,7 +4035,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), mtpShortcut, - MTP_long(action.options.effectId) + MTP_long(action.options.effectId), + MTP_long(starsPaid) ), done, fail); } else { histories.sendPreparedMessage( @@ -4019,7 +4054,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), mtpShortcut, - MTP_long(action.options.effectId) + MTP_long(action.options.effectId), + MTP_long(starsPaid) ), done, fail); } isFirst = false; @@ -4084,7 +4120,7 @@ void ApiWrap::sendBotStart( void ApiWrap::sendInlineResult( not_null bot, not_null data, - const SendAction &action, + SendAction action, std::optional localMessageId, Fn done) { sendAction(action); @@ -4124,6 +4160,13 @@ void ApiWrap::sendInlineResult( if (action.options.hideViaBot) { sendFlags |= SendFlag::f_hide_via; } + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= SendFlag::f_allow_paid_stars; + } const auto sendAs = action.options.sendAs; if (sendAs) { @@ -4138,6 +4181,7 @@ void ApiWrap::sendInlineResult( .replyTo = action.replyTo, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, + .starsPaid = starsPaid, .viaBotId = ((bot && !action.options.hideViaBot) ? peerToUser(bot->id) : UserId()), @@ -4161,7 +4205,8 @@ void ApiWrap::sendInlineResult( MTP_string(data->getId()), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - Data::ShortcutIdToMTP(_session, action.options.shortcutId) + Data::ShortcutIdToMTP(_session, action.options.shortcutId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, @@ -4298,6 +4343,7 @@ void ApiWrap::sendMediaWithRandomId( const auto history = item->history(); const auto replyTo = item->replyTo(); + const auto peer = history->peer; auto caption = item->originalText(); TextUtilities::Trim(caption); @@ -4307,6 +4353,12 @@ void ApiWrap::sendMediaWithRandomId( Api::ConvertOption::SkipLocal); const auto updateRecentStickers = Api::HasAttachedStickers(media); + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + options.starsApproved); + if (starsPaid) { + options.starsApproved -= starsPaid; + } using Flag = MTPmessages_SendMedia::Flag; const auto flags = Flag(0) @@ -4319,10 +4371,10 @@ void ApiWrap::sendMediaWithRandomId( | (options.sendAs ? Flag::f_send_as : Flag(0)) | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) | (options.effectId ? Flag::f_effect : Flag(0)) - | (options.invertCaption ? Flag::f_invert_media : Flag(0)); + | (options.invertCaption ? Flag::f_invert_media : Flag(0)) + | (starsPaid ? Flag::f_allow_paid_stars : Flag(0)); auto &histories = history->owner().histories(); - const auto peer = history->peer; const auto itemId = item->fullId(); histories.sendPreparedMessage( history, @@ -4346,7 +4398,8 @@ void ApiWrap::sendMediaWithRandomId( MTP_int(options.scheduled), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, options.shortcutId), - MTP_long(options.effectId) + MTP_long(options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4367,7 +4420,7 @@ void ApiWrap::sendMultiPaidMedia( Expects(album->options.price > 0); const auto groupId = album->groupId; - const auto &options = album->options; + auto &options = album->options; const auto randomId = album->items.front().randomId; auto medias = album->items | ranges::view::transform([]( const SendingAlbum::Item &part) { @@ -4377,6 +4430,7 @@ void ApiWrap::sendMultiPaidMedia( const auto history = item->history(); const auto replyTo = item->replyTo(); + const auto peer = history->peer; auto caption = item->originalText(); TextUtilities::Trim(caption); @@ -4384,6 +4438,12 @@ void ApiWrap::sendMultiPaidMedia( _session, caption.entities, Api::ConvertOption::SkipLocal); + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + options.starsApproved); + if (starsPaid) { + options.starsApproved -= starsPaid; + } using Flag = MTPmessages_SendMedia::Flag; const auto flags = Flag(0) @@ -4396,10 +4456,10 @@ void ApiWrap::sendMultiPaidMedia( | (options.sendAs ? Flag::f_send_as : Flag(0)) | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) | (options.effectId ? Flag::f_effect : Flag(0)) - | (options.invertCaption ? Flag::f_invert_media : Flag(0)); + | (options.invertCaption ? Flag::f_invert_media : Flag(0)) + | (starsPaid ? Flag::f_allow_paid_stars : Flag(0)); auto &histories = history->owner().histories(); - const auto peer = history->peer; const auto itemId = item->fullId(); album->sent = true; histories.sendPreparedMessage( @@ -4422,7 +4482,8 @@ void ApiWrap::sendMultiPaidMedia( MTP_int(options.scheduled), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, options.shortcutId), - MTP_long(options.effectId) + MTP_long(options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (const auto album = _sendingAlbums.take(groupId)) { const auto copy = (*album)->items; @@ -4518,6 +4579,12 @@ void ApiWrap::sendAlbumIfReady(not_null album) { const auto history = sample->history(); const auto replyTo = sample->replyTo(); const auto sendAs = album->options.sendAs; + const auto starsPaid = std::min( + history->peer->starsPerMessageChecked() * int(medias.size()), + album->options.starsApproved); + if (starsPaid) { + album->options.starsApproved -= starsPaid; + } using Flag = MTPmessages_SendMultiMedia::Flag; const auto flags = Flag(0) | (replyTo ? Flag::f_reply_to : Flag(0)) @@ -4530,7 +4597,8 @@ void ApiWrap::sendAlbumIfReady(not_null album) { ? Flag::f_quick_reply_shortcut : Flag(0)) | (album->options.effectId ? Flag::f_effect : Flag(0)) - | (album->options.invertCaption ? Flag::f_invert_media : Flag(0)); + | (album->options.invertCaption ? Flag::f_invert_media : Flag(0)) + | (starsPaid ? Flag::f_allow_paid_stars : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; album->sent = true; @@ -4546,7 +4614,8 @@ void ApiWrap::sendAlbumIfReady(not_null album) { MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, album->options.shortcutId), - MTP_long(album->options.effectId) + MTP_long(album->options.effectId), + MTP_long(starsPaid) ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index d6e2bbc67..0817560b7 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -306,7 +306,7 @@ public: void finishForwarding(const SendAction &action); void forwardMessages( Data::ResolvedForwardDraft &&draft, - const SendAction &action, + SendAction action, FnMut &&successCallback = nullptr); void shareContact( const QString &phone, @@ -368,7 +368,7 @@ public: void sendInlineResult( not_null bot, not_null data, - const SendAction &action, + SendAction action, std::optional localMessageId, Fn done = nullptr); void sendMessageFail( diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 828b2d095..0faa67772 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -1078,7 +1078,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector &bg) { ? tr::lng_background_other_group(tr::now) : forChannel() ? tr::lng_background_other_channel(tr::now) - : (_forPeer && !_fromMessageId) + : (_forPeer + && !_fromMessageId + && !_forPeer->starsPerMessageChecked()) ? tr::lng_background_other_info( tr::now, lt_user, diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index be1a42ae2..43b3cab7d 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1122,3 +1122,10 @@ profileQrBackgroundRadius: 12px; profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }}; profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px); profileQrBackgroundPadding: margins(0px, 24px, 0px, 24px); + +foldersMenu: PopupMenu(popupMenuWithIcons) { + maxHeight: 320px; + menu: Menu(menuWithIcons) { + itemPadding: margins(54px, 8px, 44px, 8px); + } +} diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 685ef547f..93164126b 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -172,13 +172,6 @@ void ChangeFilterById( const auto account = not_null(&history->session().account()); if (const auto controller = Core::App().windowFor(account)) { const auto isStatic = name.isStatic; - const auto textContext = [=](not_null widget) { - return Core::MarkedTextContext{ - .session = &history->session(), - .customEmojiRepaint = [=] { widget->update(); }, - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; controller->showToast({ .text = (add ? tr::lng_filters_toast_add @@ -189,7 +182,10 @@ void ChangeFilterById( lt_folder, Ui::Text::Wrapped(name.text, EntityType::Bold), Ui::Text::WithEntities), - .textContext = textContext, + .textContext = Core::TextContext({ + .session = &history->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }), }); } }).fail([=](const MTP::Error &error) { @@ -290,19 +286,17 @@ void FillChooseFilterMenu( const auto title = filter.title(); auto item = base::make_unique_q( menu.get(), - st::foldersMenu, + menu->st().menu, Ui::Menu::CreateAction( menu.get(), Ui::Text::FixAmpersandInAction(title.text.text), std::move(callback)), contains ? &st::mediaPlayerMenuCheck : nullptr, contains ? &st::mediaPlayerMenuCheck : nullptr); - const auto context = Core::MarkedTextContext{ + item->setMarkedText(title.text, QString(), Core::TextContext({ .session = &history->session(), - .customEmojiRepaint = [raw = item.get()] { raw->update(); }, .customEmojiLoopLimit = title.isStatic ? -1 : 0, - }; - item->setMarkedText(title.text, QString(), context); + })); item->setIcon(Icon(showColors ? filter : filter.withColorIndex({}))); const auto action = menu->addAction(std::move(item)); diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 223c5cab4..fab9731cc 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -817,13 +817,15 @@ CreatePollBox::CreatePollBox( not_null controller, PollData::Flags chosen, PollData::Flags disabled, + rpl::producer starsRequired, Api::SendType sendType, SendMenu::Details sendMenuDetails) : _controller(controller) , _chosen(chosen) , _disabled(disabled) , _sendType(sendType) -, _sendMenuDetails([result = sendMenuDetails] { return result; }) { +, _sendMenuDetails([result = sendMenuDetails] { return result; }) +, _starsRequired(std::move(starsRequired)) { } rpl::producer CreatePollBox::submitRequests() const { @@ -1226,10 +1228,11 @@ object_ptr CreatePollBox::setupContent() { _sendMenuDetails()); }; const auto submit = addButton( - (isNormal - ? tr::lng_polls_create_button() - : tr::lng_schedule_button()), + tr::lng_polls_create_button(), [=] { isNormal ? send({}) : schedule(); }); + submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal + ? tr::lng_polls_create_button() + : tr::lng_schedule_button())); const auto sendMenuDetails = [=] { collectError(); return (*error) ? SendMenu::Details() : _sendMenuDetails(); diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index 91fc290ca..33fbdd3f1 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -42,6 +42,7 @@ public: not_null controller, PollData::Flags chosen, PollData::Flags disabled, + rpl::producer starsRequired, Api::SendType sendType, SendMenu::Details sendMenuDetails); @@ -76,6 +77,7 @@ private: const PollData::Flags _disabled = PollData::Flags(); const Api::SendType _sendType = Api::SendType(); const Fn _sendMenuDetails; + rpl::variable _starsRequired; base::unique_qptr _emojiPanel; Fn _setInnerFocus; Fn()> _dataIsValidValue; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 94c4af054..5c7463261 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" #include "ui/widgets/checkbox.h" +#include "ui/widgets/continuous_sliders.h" #include "ui/widgets/shadow.h" +#include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/wrap/slide_wrap.h" @@ -21,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "boxes/peer_list_controllers.h" #include "settings/settings_premium.h" +#include "settings/settings_privacy_controllers.h" #include "settings/settings_privacy_security.h" #include "calls/calls_instance.h" #include "lang/lang_keys.h" @@ -42,6 +45,8 @@ namespace { constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value; constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value; +constexpr auto kStarsMin = 1; +constexpr auto kDefaultChargeStars = 10; using Exceptions = Api::UserPrivacy::Exceptions; @@ -452,6 +457,143 @@ auto PrivacyExceptionsBoxController::createRow(not_null history) return result; } +[[nodiscard]] object_ptr MakeChargeStarsSlider( + QWidget *parent, + not_null sliderStyle, + not_null labelStyle, + int valuesCount, + Fn valueByIndex, + int value, + int maxValue, + Fn valueProgress, + Fn valueFinished) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto labels = raw->add(object_ptr(raw)); + const auto min = Ui::CreateChild( + raw, + QString::number(kStarsMin), + *labelStyle); + const auto max = Ui::CreateChild( + raw, + QString::number(maxValue), + *labelStyle); + const auto current = Ui::CreateChild( + raw, + QString::number(value), + *labelStyle); + min->setTextColorOverride(st::windowSubTextFg->c); + max->setTextColorOverride(st::windowSubTextFg->c); + const auto slider = raw->add(object_ptr( + raw, + *sliderStyle)); + labels->resize( + labels->width(), + current->height() + st::defaultVerticalListSkip); + struct State { + int indexMin = 0; + int index = 0; + }; + const auto state = raw->lifetime().make_state(); + const auto updateByIndex = [=] { + const auto outer = labels->width(); + const auto minWidth = min->width(); + const auto maxWidth = max->width(); + const auto currentWidth = current->width(); + if (minWidth + maxWidth + currentWidth > outer) { + return; + } + + min->moveToLeft(0, 0, outer); + max->moveToRight(0, 0, outer); + current->moveToLeft((outer - current->width()) / 2, 0, outer); + }; + const auto updateByValue = [=](int value) { + current->setText( + tr::lng_action_gift_for_stars(tr::now, lt_count, value)); + + state->index = 0; + auto maxIndex = valuesCount - 1; + while (state->index < maxIndex) { + const auto mid = (state->index + maxIndex) / 2; + const auto midValue = valueByIndex(mid); + if (midValue == value) { + state->index = mid; + break; + } else if (midValue < value) { + state->index = mid + 1; + } else { + maxIndex = mid - 1; + } + } + updateByIndex(); + }; + const auto progress = [=](int value) { + updateByValue(value); + valueProgress(value); + }; + const auto finished = [=](int value) { + updateByValue(value); + valueFinished(value); + }; + style::PaletteChanged() | rpl::start_with_next([=] { + min->setTextColorOverride(st::windowSubTextFg->c); + max->setTextColorOverride(st::windowSubTextFg->c); + }, raw->lifetime()); + updateByValue(value); + state->indexMin = 0; + + slider->setPseudoDiscrete( + valuesCount, + valueByIndex, + value, + progress, + finished, + state->indexMin); + slider->resize(slider->width(), sliderStyle->seekSize.height()); + + raw->widthValue() | rpl::start_with_next([=](int width) { + labels->resizeToWidth(width); + updateByIndex(); + }, slider->lifetime()); + + return result; +} + +void EditNoPaidMessagesExceptions( + not_null window, + const Api::UserPrivacy::Rule &value) { + auto controller = std::make_unique( + &window->session(), + tr::lng_messages_privacy_remove_fee(), + value.always, + std::optional()); + auto initBox = [=, controller = controller.get()]( + not_null box) { + box->addButton(tr::lng_settings_save(), [=] { + auto copy = value; + auto &setTo = copy.always; + setTo.peers = box->collectSelectedRows(); + setTo.premiums = false; + setTo.miniapps = false; + auto &removeFrom = copy.never; + for (const auto peer : setTo.peers) { + removeFrom.peers.erase( + ranges::remove(removeFrom.peers, peer), + end(removeFrom.peers)); + } + window->session().api().userPrivacy().save( + Api::UserPrivacy::Key::NoPaidMessages, + copy); + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + window->show( + Box(std::move(controller), std::move(initBox))); +} + } // namespace bool EditPrivacyController::hasOption(Option option) const { @@ -812,19 +954,27 @@ void EditMessagesPrivacyBox( constexpr auto kOptionAll = 0; constexpr auto kOptionPremium = 1; + constexpr auto kOptionCharge = 2; + const auto session = &controller->session(); const auto allowed = [=] { - return controller->session().premium() - || controller->session().appConfig().newRequirePremiumFree(); + return session->premium() + || session->appConfig().newRequirePremiumFree(); }; - const auto privacy = &controller->session().api().globalPrivacy(); + const auto privacy = &session->api().globalPrivacy(); const auto inner = box->verticalLayout(); inner->add(object_ptr(box)); Ui::AddSkip(inner, st::messagePrivacyTopSkip); Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle()); const auto group = std::make_shared( - privacy->newRequirePremiumCurrent() ? kOptionPremium : kOptionAll); + (!allowed() + ? kOptionAll + : privacy->newRequirePremiumCurrent() + ? kOptionPremium + : privacy->newChargeStarsCurrent() + ? kOptionCharge + : kOptionAll)); inner->add( object_ptr( inner, @@ -846,6 +996,92 @@ void EditMessagesPrivacyBox( 0, st::messagePrivacyBottomSkip)); + Ui::AddDividerText(inner, tr::lng_messages_privacy_about()); + + const auto available = session->appConfig().paidMessagesAvailable(); + + const auto charged = available + ? inner->add( + object_ptr( + inner, + group, + kOptionCharge, + tr::lng_messages_privacy_charge(tr::now), + st::messagePrivacyCheck), + st::settingsSendTypePadding + style::margins( + 0, + st::messagePrivacyBottomSkip, + 0, + st::messagePrivacyBottomSkip)) + : nullptr; + + struct State { + rpl::variable stars; + }; + const auto state = std::make_shared(); + const auto savedValue = privacy->newChargeStarsCurrent(); + + if (available) { + Ui::AddDividerText(inner, tr::lng_messages_privacy_charge_about()); + + const auto chargeWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto chargeInner = chargeWrap->entity(); + + Ui::AddSkip(chargeInner); + + state->stars = SetupChargeSlider( + chargeInner, + session->user(), + savedValue); + + Ui::AddSkip(chargeInner); + Ui::AddSubsectionTitle( + chargeInner, + tr::lng_messages_privacy_exceptions()); + + const auto key = Api::UserPrivacy::Key::NoPaidMessages; + session->api().userPrivacy().reload(key); + auto label = session->api().userPrivacy().value( + key + ) | rpl::map([=](const Api::UserPrivacy::Rule &value) { + using namespace Settings; + const auto always = ExceptionUsersCount(value.always.peers); + return always + ? tr::lng_edit_privacy_exceptions_count( + tr::now, + lt_count, + always) + : QString(); + }); + + const auto exceptions = Settings::AddButtonWithLabel( + chargeInner, + tr::lng_messages_privacy_remove_fee(), + std::move(label), + st::settingsButtonNoIcon); + + const auto shower = exceptions->lifetime().make_state(); + exceptions->setClickedCallback([=] { + *shower = session->api().userPrivacy().value( + key + ) | rpl::take( + 1 + ) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) { + EditNoPaidMessagesExceptions(controller, value); + }); + }); + Ui::AddSkip(chargeInner); + Ui::AddDividerText( + chargeInner, + tr::lng_messages_privacy_remove_about()); + + using namespace rpl::mappers; + chargeWrap->toggleOn(group->value() | rpl::map(_1 == kOptionCharge)); + chargeWrap->finishAnimating(); + } using WeakToast = base::weak_ptr; const auto toast = std::make_shared(); const auto showToast = [=] { @@ -875,19 +1111,20 @@ void EditMessagesPrivacyBox( }), }); }; + if (!allowed()) { CreateRadiobuttonLock(restricted, st::messagePrivacyCheck); + if (charged) { + CreateRadiobuttonLock(charged, st::messagePrivacyCheck); + } group->setChangedCallback([=](int value) { - if (value == kOptionPremium) { + if (value == kOptionPremium || value == kOptionCharge) { group->setValue(kOptionAll); showToast(); } }); - } - Ui::AddDividerText(inner, tr::lng_messages_privacy_about()); - if (!allowed()) { Ui::AddSkip(inner); Settings::AddButtonWithIcon( inner, @@ -907,8 +1144,12 @@ void EditMessagesPrivacyBox( } else { box->addButton(tr::lng_settings_save(), [=] { if (allowed()) { - privacy->updateNewRequirePremium( - group->current() == kOptionPremium); + const auto value = group->current(); + const auto premiumRequired = (value == kOptionPremium); + const auto chargeStars = (value == kOptionCharge) + ? state->stars.current() + : 0; + privacy->updateMessagesPrivacy(premiumRequired, chargeStars); box->closeBox(); } else { showToast(); @@ -919,3 +1160,78 @@ void EditMessagesPrivacyBox( }); } } + +rpl::producer SetupChargeSlider( + not_null container, + not_null peer, + int savedValue) { + struct State { + rpl::variable stars; + }; + const auto group = !peer->isUser(); + const auto state = container->lifetime().make_state(); + const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars; + state->stars = chargeStars; + + Ui::AddSubsectionTitle(container, group + ? tr::lng_rights_charge_price() + : tr::lng_messages_privacy_price()); + + auto values = std::vector(); + const auto maxStars = peer->session().appConfig().paidMessageStarsMax(); + if (chargeStars < kStarsMin) { + values.push_back(chargeStars); + } + for (auto i = kStarsMin; i < std::min(100, maxStars); ++i) { + values.push_back(i); + } + for (auto i = 100; i < std::min(1000, maxStars); i += 10) { + if (i < chargeStars + 10 && chargeStars < i) { + values.push_back(chargeStars); + } + values.push_back(i); + } + for (auto i = 1000; i < maxStars + 1; i += 100) { + if (i < chargeStars + 100 && chargeStars < i) { + values.push_back(chargeStars); + } + values.push_back(i); + } + const auto valuesCount = int(values.size()); + const auto setStars = [=](int value) { + state->stars = value; + }; + container->add( + MakeChargeStarsSlider( + container, + &st::settingsScale, + &st::settingsScaleLabel, + valuesCount, + [=](int index) { return values[index]; }, + chargeStars, + maxStars, + setStars, + setStars), + st::boxRowPadding); + + const auto skip = 2 * st::defaultVerticalListSkip; + Ui::AddSkip(container, skip); + + auto dollars = state->stars.value() | rpl::map([=](int stars) { + const auto ratio = peer->session().appConfig().starsWithdrawRate(); + const auto dollars = int(base::SafeRound(stars * ratio)); + return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q); + }); + const auto percent = peer->session().appConfig().paidMessageCommission(); + Ui::AddDividerText( + container, + (group + ? tr::lng_rights_charge_price_about + : tr::lng_messages_privacy_price_about)( + lt_percent, + rpl::single(QString::number(percent / 10.) + '%'), + lt_amount, + std::move(dollars))); + + return state->stars.value(); +} diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index bd87e90f2..256ebe5b5 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -169,3 +169,8 @@ private: void EditMessagesPrivacyBox( not_null box, not_null controller); + +[[nodiscard]] rpl::producer SetupChargeSlider( + not_null container, + not_null peer, + int savedValue); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index f9a05a557..be4531aec 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -441,13 +441,10 @@ void EditFilterBox( using namespace Window; return window->isGifPausedAtLeastFor(GifPauseReason::Layer); }; - name->setCustomTextContext([=](Fn repaint) { - return std::any(Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = std::move(repaint), - .customEmojiLoopLimit = value ? -1 : 0, - }); - }, [paused] { + name->setCustomTextContext(Core::TextContext({ + .session = session, + .customEmojiLoopLimit = value ? -1 : 0, + }), [paused] { return On(PowerSaving::kEmojiChat) || paused(); }, [paused] { return On(PowerSaving::kChatSpoiler) || paused(); @@ -609,10 +606,7 @@ void EditFilterBox( float64 alpha = 1.; }; const auto tag = preview->lifetime().make_state(); - tag->context.textContext = Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [] {}, - }; + tag->context.textContext = Core::TextContext({ .session = session }); preview->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(preview); p.setOpacity(tag->alpha); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 2ed72aa25..a200bcecb 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -163,10 +163,10 @@ ExceptionRow::ExceptionRow( st::defaultTextStyle, filters, kMarkupTextOptions, - Core::MarkedTextContext{ + Core::TextContext({ .session = &history->session(), - .customEmojiRepaint = repaint, - }); + .repaint = repaint, + })); } else if (peer()->isSelf()) { setCustomStatus(tr::lng_saved_forward_here(tr::now)); } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 61bbd85a0..90dd083bd 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -537,13 +537,6 @@ void LinkController::addHeader(not_null container) { verticalLayout->add(std::move(icon.widget)); const auto isStatic = _filterTitle.isStatic; - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = &_window->session(), - .customEmojiRepaint = update, - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; verticalLayout->add( object_ptr>( verticalLayout, @@ -559,7 +552,10 @@ void LinkController::addHeader(not_null container) { Ui::Text::WithEntities)), st::settingsFilterDividerLabel, st::defaultPopupMenu, - makeContext)), + Core::TextContext({ + .session = &_window->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }))), st::filterLinkDividerLabelPadding); verticalLayout->geometryValue( diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index 80e859eb1..761b387e5 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_credits.h" #include "boxes/peer_list_controllers.h" +#include "core/ui_integration.h" // TextContext. #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_user.h" @@ -67,14 +68,9 @@ void GiftCreditsBox( 2.); { Ui::AddSkip(content); - const auto arrow = Ui::Text::SingleCustomEmoji( - peer->owner().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); auto link = tr::lng_credits_box_history_entry_gift_about_link( lt_emoji, - rpl::single(arrow), + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), Ui::Text::RichLangValue ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link( @@ -92,7 +88,7 @@ void GiftCreditsBox( lt_link, std::move(link), Ui::Text::RichLangValue), - { .session = &peer->session() }, + Core::TextContext({ .session = &peer->session() }), st::creditsBoxAbout)), st::boxRowPadding); } diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index db9eaf3a9..691821c16 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox. #include "boxes/star_gift_box.h" // ShowStarGiftBox. #include "boxes/transfer_gift_box.h" // ShowTransferGiftBox. +#include "core/ui_integration.h" #include "data/data_boosts.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -58,7 +59,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/gradient_round_button.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/tooltip.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" // ShowChooseRecipientBox. #include "window/window_session_controller.h" #include "styles/style_boxes.h" +#include "styles/style_credits.h" #include "styles/style_giveaway.h" #include "styles/style_info.h" #include "styles/style_layers.h" @@ -516,13 +517,13 @@ not_null AddTableRow( not_null table, rpl::producer label, rpl::producer value, - const Fn)> &makeContext = nullptr) { + const Ui::Text::MarkedContext &context = {}) { auto widget = object_ptr( table, std::move(value), table->st().defaultValue, st::defaultPopupMenu, - std::move(makeContext)); + context); const auto result = widget.data(); AddTableRow( table, @@ -1272,8 +1273,8 @@ void AddStarGiftTable( const auto selfBareId = session->userPeerId().value; const auto giftToSelf = (peerId == session->userPeerId()) && (entry.in || entry.bareGiftOwnerId == selfBareId); - const auto giftToChannel = entry.giftSavedId - && peerIsChannel(PeerId(entry.bareGiftListPeerId)); + const auto giftToChannel = entry.giftChannelSavedId + && peerIsChannel(PeerId(entry.bareEntryOwnerId)); const auto raw = std::make_shared(nullptr); const auto showTooltip = [=]( @@ -1394,14 +1395,14 @@ void AddStarGiftTable( ? MakePeerTableValue(table, show, PeerId(entry.bareActorId)) : MakeHiddenPeerTableValue(table)), st::giveawayGiftCodePeerMargin); - if (entry.bareGiftListPeerId) { + if (entry.bareEntryOwnerId) { AddTableRow( table, tr::lng_credits_box_history_entry_peer(), MakePeerTableValue( table, show, - PeerId(entry.bareGiftListPeerId)), + PeerId(entry.bareEntryOwnerId)), st::giveawayGiftCodePeerMargin); } } else if (peerId && !giftToSelf) { @@ -1526,12 +1527,6 @@ void AddStarGiftTable( : nullptr; const auto date = base::unixtime::parse(original.date).date(); const auto dateText = TextWithEntities{ langDayOfMonth(date) }; - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = std::move(update), - }; - }; auto label = object_ptr( table, (from @@ -1573,7 +1568,7 @@ void AddStarGiftTable( ? *st.tableValueMessage : st::giveawayGiftMessage), st::defaultPopupMenu, - makeContext); + Core::TextContext({ .session = session })); const auto showBoxLink = [=](not_null peer) { return std::make_shared([=] { show->showBox(PrepareShortInfoBox(peer, show)); @@ -1591,12 +1586,6 @@ void AddStarGiftTable( st::giveawayGiftCodeValueMargin); } } else if (!entry.description.empty()) { - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = std::move(update), - }; - }; auto label = object_ptr( table, rpl::single(entry.description), @@ -1604,7 +1593,7 @@ void AddStarGiftTable( ? *st.tableValueMessage : st::giveawayGiftMessage), st::defaultPopupMenu, - makeContext); + Core::TextContext({ .session = session })); label->setSelectable(true); table->addRow( nullptr, @@ -1775,6 +1764,25 @@ void AddCreditsHistoryEntryTable( tr::lng_credits_box_history_entry_subscription( Ui::Text::WithEntities)); } + if (entry.paidMessagesAmount) { + auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored); + const auto full = (entry.in ? 1 : -1) + * (entry.credits + entry.paidMessagesAmount); + const auto starsText = Lang::FormatStarsAmountDecimal(full); + AddTableRow( + table, + tr::lng_credits_paid_messages_full(), + rpl::single(value.append(' ' + starsText))); + } + if (const auto months = entry.premiumMonthsForStars) { + AddTableRow( + table, + tr::lng_credits_premium_gift_duration(), + tr::lng_months( + lt_count, + rpl::single(1. * months), + Ui::Text::WithEntities)); + } if (!entry.id.isEmpty()) { auto label = MakeMaybeMultilineTokenValue(table, entry.id, st); label->setClickHandlerFilter([=](const auto &...) { diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 8007b9383..d278d2681 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -453,10 +453,7 @@ void CreateModerateMessagesBox( ) | rpl::start_with_next([=](const TextWithEntities &text) { raw->setMarkedText( Ui::Text::Link(text, u"internal:"_q), - Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [=] { raw->update(); }, - }); + Core::TextContext({ .session = session })); }, label->lifetime()); Ui::AddSkip(inner); diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index 308384e16..96d5f2d33 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -1154,8 +1154,7 @@ RecoverBox::RecoverBox( rpl::single(Ui::Text::WrapEmailPattern(pattern)), Ui::Text::WithEntities), st::termsContent, - st::defaultPopupMenu, - [=](Fn update) { return CommonTextContext{ std::move(update) }; }) + st::defaultPopupMenu) , _closeParent(std::move(closeParent)) { _patternLabel->setAttribute(Qt::WA_TransparentForMouseEvents); if (_cloudFields.pendingResetDate != 0 || !session) { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index a9e33955b..d9df69287 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -883,6 +883,7 @@ void PeerListRow::paintUserpic( } else if (const auto callback = generatePaintUserpicCallback(false)) { callback(p, x, y, outerWidth, st.photoSize); } + paintUserpicOverlay(p, st, x, y, outerWidth); } // Emulates Ui::RoundImageCheckbox::paint() in a checked state. diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index c4a79c456..b8dbecb8c 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -95,6 +95,13 @@ public: [[nodiscard]] virtual QString generateShortName(); [[nodiscard]] virtual auto generatePaintUserpicCallback( bool forceRound) -> PaintRoundImageCallback; + virtual void paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) { + } [[nodiscard]] virtual auto generateNameFirstLetters() const -> const base::flat_set &; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index ff3e2722a..3b2214753 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "api/api_chat_participants.h" -#include "api/api_premium.h" +#include "api/api_premium.h" // MessageMoneyRestriction. #include "base/random.h" #include "boxes/filters/edit_filter_chats_list.h" #include "settings/settings_premium.h" @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "dialogs/dialogs_main_list.h" +#include "payments/ui/payments_reaction_box.h" #include "ui/effects/outline_segments.h" #include "ui/wrap/slide_wrap.h" #include "window/window_separate_id.h" @@ -275,40 +276,71 @@ bool PeerListGlobalSearchController::isLoading() { return _timer.isActive() || _requestId; } +struct RecipientRow::Restriction { + Api::MessageMoneyRestriction value; + RestrictionBadgeCache cache; +}; + RecipientRow::RecipientRow( not_null peer, const style::PeerListItem *maybeLockedSt, History *maybeHistory) : PeerListRow(peer) , _maybeHistory(maybeHistory) -, _resolvePremiumRequired(maybeLockedSt != nullptr) { - if (maybeLockedSt - && (Api::ResolveRequiresPremiumToWrite(peer, maybeHistory) - == Api::RequirePremiumState::Yes)) { - _lockedSt = maybeLockedSt; +, _maybeLockedSt(maybeLockedSt) { + if (_maybeLockedSt) { + setRestriction(Api::ResolveMessageMoneyRestrictions( + peer, + maybeHistory)); } } -PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback( - bool forceRound) { - auto result = PeerListRow::generatePaintUserpicCallback(forceRound); - if (const auto st = _lockedSt) { - return [=](Painter &p, int x, int y, int outerWidth, int size) { - result(p, x, y, outerWidth, size); - PaintPremiumRequiredLock(p, st, x, y, outerWidth, size); - }; +Api::MessageMoneyRestriction RecipientRow::restriction() const { + return _restriction + ? _restriction->value + : Api::MessageMoneyRestriction(); +} + +void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) { + if (!restriction) { + _restriction = nullptr; + return; + } else if (!_restriction) { + _restriction = std::make_unique(); + } + _restriction->value = restriction; +} + +void RecipientRow::paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) { + if (const auto &r = _restriction) { + PaintRestrictionBadge( + p, + _maybeLockedSt, + r->value.starsPerMessage, + r->cache, + x, + y, + outerWidth, + st.photoSize); } - return result; } bool RecipientRow::refreshLock( not_null maybeLockedSt) { if (const auto user = peer()->asUser()) { - const auto locked = _resolvePremiumRequired - && (Api::ResolveRequiresPremiumToWrite(user, _maybeHistory) - == Api::RequirePremiumState::Yes); - if (this->locked() != locked) { - setLocked(locked ? maybeLockedSt.get() : nullptr); + using Restriction = Api::MessageMoneyRestriction; + const auto r = _maybeLockedSt + ? Api::ResolveMessageMoneyRestrictions( + user, + _maybeHistory) + : Restriction(); + if ((_restriction ? _restriction->value : Restriction()) != r) { + setRestriction(r); return true; } } @@ -318,22 +350,30 @@ bool RecipientRow::refreshLock( void RecipientRow::preloadUserpic() { PeerListRow::preloadUserpic(); - if (!_resolvePremiumRequired) { + if (!_maybeLockedSt) { return; - } else if (Api::ResolveRequiresPremiumToWrite(peer(), _maybeHistory) - == Api::RequirePremiumState::Unknown) { - const auto user = peer()->asUser(); - user->session().api().premium().resolvePremiumRequired(user); + } + const auto peer = this->peer(); + const auto known = Api::ResolveMessageMoneyRestrictions( + peer, + _maybeHistory).known; + if (known) { + return; + } else if (const auto user = peer->asUser()) { + const auto api = &user->session().api(); + api->premium().resolveMessageMoneyRestrictions(user); + } else if (const auto group = peer->asChannel()) { + group->updateFull(); } } -void TrackPremiumRequiredChanges( +void TrackMessageMoneyRestrictionsChanges( not_null controller, rpl::lifetime &lifetime) { const auto session = &controller->session(); rpl::merge( Data::AmPremiumValue(session) | rpl::to_empty, - session->api().premium().somePremiumRequiredResolved() + session->api().premium().someMessageMoneyRestrictionsResolved() ) | rpl::start_with_next([=] { const auto st = &controller->computeListSt().item; const auto delegate = controller->delegate(); @@ -726,7 +766,7 @@ std::unique_ptr ContactsBoxController::createRow( return std::make_unique(user); } -RecipientPremiumRequiredError WritePremiumRequiredError( +RecipientMoneyRestrictionError WriteMoneyRestrictionError( not_null user) { return { .text = tr::lng_send_non_premium_message_toast( @@ -759,7 +799,7 @@ ChooseRecipientBoxController::ChooseRecipientBoxController( , _session(args.session) , _callback(std::move(args.callback)) , _filter(std::move(args.filter)) -, _premiumRequiredError(std::move(args.premiumRequiredError)) { +, _moneyRestrictionError(std::move(args.moneyRestrictionError)) { } Main::Session &ChooseRecipientBoxController::session() const { @@ -769,14 +809,17 @@ Main::Session &ChooseRecipientBoxController::session() const { void ChooseRecipientBoxController::prepareViewHook() { delegate()->peerListSetTitle(tr::lng_forward_choose()); - if (_premiumRequiredError) { - TrackPremiumRequiredChanges(this, lifetime()); + if (_moneyRestrictionError) { + TrackMessageMoneyRestrictionsChanges(this, lifetime()); } } bool ChooseRecipientBoxController::showLockedError( not_null row) { - return RecipientRow::ShowLockedError(this, row, _premiumRequiredError); + return RecipientRow::ShowLockedError( + this, + row, + _moneyRestrictionError); } void ChooseRecipientBoxController::rowClicked(not_null row) { @@ -836,8 +879,9 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { bool RecipientRow::ShowLockedError( not_null controller, not_null row, - Fn)> error) { - if (!static_cast(row.get())->locked()) { + Fn)> error) { + const auto recipient = static_cast(row.get()); + if (!recipient->restriction().premiumRequired) { return false; } ::Settings::ShowPremiumPromoToast( @@ -860,15 +904,15 @@ auto ChooseRecipientBoxController::createRow( : ((peer->isBroadcast() && !Data::CanSendAnything(peer)) || peer->isRepliesChat() || peer->isVerifyCodes() - || (peer->isUser() && (_premiumRequiredError - ? !peer->asUser()->canSendIgnoreRequirePremium() + || (peer->isUser() && (_moneyRestrictionError + ? !peer->asUser()->canSendIgnoreMoneyRestrictions() : !Data::CanSendAnything(peer)))); if (skip) { return nullptr; } auto result = std::make_unique( history, - _premiumRequiredError ? &computeListSt().item : nullptr); + _moneyRestrictionError ? &computeListSt().item : nullptr); return result; } @@ -1093,25 +1137,61 @@ auto ChooseTopicBoxController::createRow(not_null topic) return skip ? nullptr : std::make_unique(topic); }; -void PaintPremiumRequiredLock( +void PaintRestrictionBadge( Painter &p, not_null st, + int stars, + RestrictionBadgeCache &cache, int x, int y, int outerWidth, int size) { - auto hq = PainterHighQualityEnabler(p); + const auto paletteVersion = style::PaletteVersion(); + const auto good = !cache.badge.isNull() + && (cache.stars == stars) + && (cache.paletteVersion == paletteVersion); const auto &check = st->checkbox.check; - auto pen = check.border->p; - pen.setWidthF(check.width); - p.setPen(pen); - p.setBrush(st::premiumButtonBg2); - const auto &icon = st::stickersPremiumLock; - const auto width = icon.width(); - const auto height = icon.height(); - const auto rect = QRect( - QPoint(x + size - width, y + size - height), - icon.size()); - p.drawEllipse(rect); - icon.paintInCenter(p, rect); + const auto add = check.width; + if (!good) { + cache.stars = stars; + cache.paletteVersion = paletteVersion; + if (stars) { + const auto text = (stars >= 1000) + ? (QString::number(stars / 1000) + 'K') + : QString::number(stars); + cache.badge = Ui::GenerateSmallBadgeImage( + text, + st::paidReactTopStarIcon, + check.bgActive->c, + st::premiumButtonFg->c, + &check); + } else { + auto hq = PainterHighQualityEnabler(p); + const auto &icon = st::stickersPremiumLock; + const auto width = icon.width(); + const auto height = icon.height(); + const auto rect = QRect( + QPoint(x + size - width, y + size - height), + icon.size()); + const auto added = QMargins(add, add, add, add); + const auto ratio = style::DevicePixelRatio(); + cache.badge = QImage( + (rect + added).size() * ratio, + QImage::Format_ARGB32_Premultiplied); + cache.badge.setDevicePixelRatio(ratio); + cache.badge.fill(Qt::transparent); + const auto inner = QRect(add, add, rect.width(), rect.height()); + auto q = QPainter(&cache.badge); + auto pen = check.border->p; + pen.setWidthF(check.width); + q.setPen(pen); + q.setBrush(st::premiumButtonBg2); + q.drawEllipse(inner); + icon.paintInCenter(q, inner); + } + } + const auto cached = cache.badge.size() / cache.badge.devicePixelRatio(); + const auto left = x + size + add - cached.width(); + const auto top = stars ? (y - add) : (y + size + add - cached.height()); + p.drawImage(left, top, cache.badge); } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 07f71534a..de9c67dbf 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -19,6 +19,10 @@ namespace style { struct PeerListItem; } // namespace style +namespace Api { +struct MessageMoneyRestriction; +} // namespace Api + namespace Data { class Thread; class Forum; @@ -93,13 +97,28 @@ private: }; -struct RecipientPremiumRequiredError { +struct RecipientMoneyRestrictionError { TextWithEntities text; }; -[[nodiscard]] RecipientPremiumRequiredError WritePremiumRequiredError( +[[nodiscard]] RecipientMoneyRestrictionError WriteMoneyRestrictionError( not_null user); +struct RestrictionBadgeCache { + int paletteVersion = 0; + int stars = 0; + QImage badge; +}; +void PaintRestrictionBadge( + Painter &p, + not_null st, + int stars, + RestrictionBadgeCache &cache, + int x, + int y, + int outerWidth, + int size); + class RecipientRow : public PeerListRow { public: explicit RecipientRow( @@ -112,30 +131,33 @@ public: [[nodiscard]] static bool ShowLockedError( not_null controller, not_null row, - Fn)> error); + Fn)> error); [[nodiscard]] History *maybeHistory() const { return _maybeHistory; } - [[nodiscard]] bool locked() const { - return _lockedSt != nullptr; - } - void setLocked(const style::PeerListItem *lockedSt) { - _lockedSt = lockedSt; - } - PaintRoundImageCallback generatePaintUserpicCallback( - bool forceRound) override; + void paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) override; void preloadUserpic() override; + [[nodiscard]] Api::MessageMoneyRestriction restriction() const; + void setRestriction(Api::MessageMoneyRestriction restriction); + private: + struct Restriction; + History *_maybeHistory = nullptr; - const style::PeerListItem *_lockedSt = nullptr; - bool _resolvePremiumRequired = false; + const style::PeerListItem *_maybeLockedSt = nullptr; + std::shared_ptr _restriction; }; -void TrackPremiumRequiredChanges( +void TrackMessageMoneyRestrictionsChanges( not_null controller, rpl::lifetime &lifetime); @@ -261,8 +283,8 @@ struct ChooseRecipientArgs { FnMut)> callback; Fn)> filter; - using PremiumRequiredError = RecipientPremiumRequiredError; - Fn)> premiumRequiredError; + using MoneyRestrictionError = RecipientMoneyRestrictionError; + Fn)> moneyRestrictionError; }; class ChooseRecipientBoxController @@ -290,8 +312,8 @@ private: const not_null _session; FnMut)> _callback; Fn)> _filter; - Fn)> _premiumRequiredError; + Fn)> _moneyRestrictionError; }; @@ -371,11 +393,3 @@ private: Fn)> _filter; }; - -void PaintPremiumRequiredLock( - Painter &p, - not_null st, - int x, - int y, - int outerWidth, - int size); diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index ab48b6b8d..4701cac1e 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -9,10 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "api/api_invite_links.h" +#include "api/api_premium.h" #include "boxes/peers/edit_participant_box.h" #include "boxes/peers/edit_peer_type_box.h" #include "boxes/peers/replace_boost_box.h" #include "boxes/max_invite_box.h" +#include "chat_helpers/message_field.h" #include "lang/lang_keys.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -22,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_peer_values.h" #include "history/history.h" +#include "history/history_item_helpers.h" #include "dialogs/dialogs_indexed_list.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/show_or_premium_box.h" @@ -52,16 +55,39 @@ constexpr auto kUserpicsLimit = 3; class ForbiddenRow final : public PeerListRow { public: - ForbiddenRow(not_null peer, bool locked); + ForbiddenRow( + not_null peer, + not_null lockSt, + bool locked); PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; + Api::MessageMoneyRestriction restriction() const; + void setRestriction(Api::MessageMoneyRestriction restriction); + + void preloadUserpic() override; + void paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) override; + + bool refreshLock(); + private: + struct Restriction { + Api::MessageMoneyRestriction value; + RestrictionBadgeCache cache; + }; + const bool _locked = false; + const not_null _lockSt; QImage _disabledFrame; InMemoryKey _userpicKey; int _paletteVersion = 0; + std::shared_ptr _restriction; }; @@ -81,6 +107,9 @@ public: [[nodiscard]] rpl::producer selectedValue() const { return _selected.value(); } + [[nodiscard]] rpl::producer starsToSend() const { + return _starsToSend.value(); + } void send( std::vector> list, @@ -89,10 +118,16 @@ public: private: void appendRow(not_null user); - [[nodiscard]] std::unique_ptr createRow( + [[nodiscard]] std::unique_ptr createRow( not_null user) const; [[nodiscard]] bool canInvite(not_null peer) const; + void send( + std::vector> list, + Ui::ShowPtr show, + Fn close, + Api::SendOptions options); + void setSimpleCover(); void setComplexCover(); @@ -101,8 +136,11 @@ private: const std::vector> &_users; const bool _can = false; rpl::variable _selected; + rpl::variable _starsToSend; bool _sending = false; + rpl::lifetime _paymentCheckLifetime; + }; base::flat_set> GetAlreadyInFromPeer(PeerData *peer) { @@ -256,11 +294,17 @@ Main::Session &InviteForbiddenController::session() const { return _peer->session(); } -ForbiddenRow::ForbiddenRow(not_null peer, bool locked) +ForbiddenRow::ForbiddenRow( + not_null peer, + not_null lockSt, + bool locked) : PeerListRow(peer) -, _locked(locked) { +, _locked(locked) +, _lockSt(lockSt) { if (_locked) { setCustomStatus(tr::lng_invite_status_disabled(tr::now)); + } else { + setRestriction(Api::ResolveMessageMoneyRestrictions(peer, nullptr)); } } @@ -339,6 +383,76 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback( }; } + +Api::MessageMoneyRestriction ForbiddenRow::restriction() const { + return _restriction + ? _restriction->value + : Api::MessageMoneyRestriction(); +} + +void ForbiddenRow::setRestriction(Api::MessageMoneyRestriction restriction) { + if (!restriction || !restriction.starsPerMessage) { + _restriction = nullptr; + return; + } else if (!_restriction) { + _restriction = std::make_unique(); + } + _restriction->value = restriction; +} + +void ForbiddenRow::paintUserpicOverlay( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int outerWidth) { + if (const auto &r = _restriction) { + PaintRestrictionBadge( + p, + _lockSt, + r->value.starsPerMessage, + r->cache, + x, + y, + outerWidth, + st.photoSize); + } +} + +bool ForbiddenRow::refreshLock() { + if (_locked) { + return false; + } else if (const auto user = peer()->asUser()) { + using Restriction = Api::MessageMoneyRestriction; + auto r = Api::ResolveMessageMoneyRestrictions(user, nullptr); + if (!r || !r.starsPerMessage) { + r = Restriction(); + } + if ((_restriction ? _restriction->value : Restriction()) != r) { + setRestriction(r); + return true; + } + } + return false; +} + +void ForbiddenRow::preloadUserpic() { + PeerListRow::preloadUserpic(); + + const auto peer = this->peer(); + const auto known = Api::ResolveMessageMoneyRestrictions( + peer, + nullptr).known; + if (known) { + return; + } else if (const auto user = peer->asUser()) { + const auto api = &user->session().api(); + api->premium().resolveMessageMoneyRestrictions(user); + } else if (const auto group = peer->asChannel()) { + group->updateFull(); + } +} + void InviteForbiddenController::setSimpleCover() { delegate()->peerListSetTitle( _can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant()); @@ -435,6 +549,30 @@ void InviteForbiddenController::setComplexCover() { } void InviteForbiddenController::prepare() { + session().api().premium().someMessageMoneyRestrictionsResolved( + ) | rpl::start_with_next([=] { + auto stars = 0; + const auto process = [&](not_null raw) { + const auto row = static_cast(raw.get()); + if (row->refreshLock()) { + delegate()->peerListUpdateRow(raw); + } + if (const auto r = row->restriction()) { + stars += r.starsPerMessage; + } + }; + auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count; ++i) { + process(delegate()->peerListRowAt(i)); + } + _starsToSend = stars; + + count = delegate()->peerListSearchRowsCount(); + for (auto i = 0; i != count; ++i) { + process(delegate()->peerListSearchRowAt(i)); + } + }, lifetime()); + if (session().premium() || (_forbidden.premiumAllowsInvite.empty() && _forbidden.premiumAllowsWrite.empty())) { @@ -464,6 +602,11 @@ void InviteForbiddenController::rowClicked(not_null row) { const auto checked = row->checked(); delegate()->peerListSetRowChecked(row, !checked); _selected = _selected.current() + (checked ? -1 : 1); + const auto r = static_cast(row.get())->restriction(); + if (r.starsPerMessage) { + _starsToSend = _starsToSend.current() + + (checked ? -r.starsPerMessage : r.starsPerMessage); + } } void InviteForbiddenController::appendRow(not_null user) { @@ -473,6 +616,9 @@ void InviteForbiddenController::appendRow(not_null user) { delegate()->peerListAppendRow(std::move(row)); if (canInvite(user)) { delegate()->peerListSetRowChecked(raw, true); + if (const auto r = raw->restriction()) { + _starsToSend = _starsToSend.current() + r.starsPerMessage; + } } } } @@ -481,7 +627,64 @@ void InviteForbiddenController::send( std::vector> list, Ui::ShowPtr show, Fn close) { - if (_sending || list.empty()) { + send(list, show, close, {}); +} + +void InviteForbiddenController::send( + std::vector> list, + Ui::ShowPtr show, + Fn close, + Api::SendOptions options) { + if (list.empty()) { + return; + } + _paymentCheckLifetime.destroy(); + + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + send(list, show, close, copy); + }; + const auto messagesCount = 1; + const auto alreadyApproved = options.starsApproved; + auto paid = std::vector>(); + auto waiting = base::flat_set>(); + auto totalStars = 0; + for (const auto &peer : list) { + const auto details = ComputePaymentDetails(peer, messagesCount); + if (!details) { + waiting.emplace(peer); + } else if (details->stars > 0) { + totalStars += details->stars; + paid.push_back(peer); + } + } + if (!waiting.empty()) { + session().changes().peerUpdates( + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + if (waiting.contains(update.peer)) { + withPaymentApproved(alreadyApproved); + } + }, _paymentCheckLifetime); + + if (!session().credits().loaded()) { + session().credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + withPaymentApproved(alreadyApproved); + }, _paymentCheckLifetime); + } + return; + } else if (totalStars > alreadyApproved) { + const auto sessionShow = Main::MakeSessionShow(show, &session()); + ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{ + .messages = messagesCount, + .stars = totalStars, + }, [=] { withPaymentApproved(totalStars); }); + return; + } else if (_sending) { return; } _sending = true; @@ -492,12 +695,18 @@ void InviteForbiddenController::send( if (link.isEmpty()) { return false; } + auto full = options; auto &api = _peer->session().api(); - auto options = Api::SendOptions(); for (const auto &to : list) { + auto copy = full; + copy.starsApproved = std::min( + to->starsPerMessageChecked(), + full.starsApproved); + full.starsApproved -= copy.starsApproved; + const auto history = to->owner().history(to); auto message = Api::MessageToSend( - Api::SendAction(history, options)); + Api::SendAction(history, copy)); message.textWithTags = { link }; message.action.clearDraft = false; api.sendMessage(std::move(message)); @@ -542,10 +751,11 @@ void InviteForbiddenController::send( } } -std::unique_ptr InviteForbiddenController::createRow( +std::unique_ptr InviteForbiddenController::createRow( not_null user) const { const auto locked = _can && !canInvite(user); - return std::make_unique(user, locked); + const auto lockSt = &computeListSt().item; + return std::make_unique(user, lockSt, locked); } } // namespace @@ -584,8 +794,8 @@ void AddParticipantsBoxController::subscribeToMigration() { } void AddParticipantsBoxController::rowClicked(not_null row) { - const auto premiumRequiredError = WritePremiumRequiredError; - if (RecipientRow::ShowLockedError(this, row, premiumRequiredError)) { + const auto moneyRestrictionError = WriteMoneyRestrictionError; + if (RecipientRow::ShowLockedError(this, row, moneyRestrictionError)) { return; } const auto &serverConfig = session().serverConfig(); @@ -614,7 +824,7 @@ void AddParticipantsBoxController::itemDeselectedHook( void AddParticipantsBoxController::prepareViewHook() { updateTitle(); - TrackPremiumRequiredChanges(this, lifetime()); + TrackMessageMoneyRestrictionsChanges(this, lifetime()); } int AddParticipantsBoxController::alreadyInCount() const { @@ -929,12 +1139,15 @@ bool ChatInviteForbidden( ) | rpl::start_with_next([=](bool has) { box->clearButtons(); if (has) { - box->addButton(tr::lng_via_link_send(), [=] { + const auto send = box->addButton(tr::lng_via_link_send(), [=] { weak->send( box->collectSelectedRows(), box->uiShow(), crl::guard(box, [=] { box->closeBox(); })); }); + send->setText(PaidSendButtonText( + weak->starsToSend(), + tr::lng_via_link_send())); } box->addButton(tr::lng_create_group_skip(), [=] { box->closeBox(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 73b3004f0..c457ce579 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_box.h" #include "boxes/stickers_box.h" #include "chat_helpers/compose/compose_show.h" -#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "core/ui_integration.h" // TextContext #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" #include "data/data_changes.h" @@ -165,7 +165,7 @@ private: const uint32 _level; const TextWithEntities _icon; - const Core::MarkedTextContext _context; + const Ui::Text::MarkedContext _context; Ui::Text::String _text; bool _minimal = false; @@ -466,7 +466,10 @@ LevelBadge::LevelBadge( st::settingsLevelBadgeLock, QMargins(0, st::settingsLevelBadgeLockSkip, 0, 0), false))) -, _context({ .session = session }) { +, _context(Core::TextContext({ + .session = session, + .repaint = [this] { update(); }, +})) { updateText(); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index b5cad4e65..75ca484b8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -219,6 +219,33 @@ void SaveSlowmodeSeconds( api->registerModifyRequest(key, requestId); } +void SaveStarsPerMessage( + not_null channel, + int starsPerMessage, + Fn done) { + const auto api = &channel->session().api(); + const auto key = Api::RequestKey("stars_per_message", channel->id); + + const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice( + channel->inputChannel, + MTP_long(starsPerMessage) + )).done([=](const MTPUpdates &result) { + api->clearModifyRequest(key); + api->applyUpdates(result); + channel->setStarsPerMessage(starsPerMessage); + done(); + }).fail([=](const MTP::Error &error) { + api->clearModifyRequest(key); + if (error.type() != u"CHAT_NOT_MODIFIED"_q) { + return; + } + channel->setStarsPerMessage(starsPerMessage); + done(); + }).send(); + + api->registerModifyRequest(key, requestId); +} + void SaveBoostsUnrestrict( not_null channel, int boostsUnrestrict, @@ -271,6 +298,7 @@ void ShowEditPermissions( channel, result.boostsUnrestrict, close); + SaveStarsPerMessage(channel, result.starsPerMessage, close); } }; auto done = [=](EditPeerPermissionsBoxResult result) { @@ -282,7 +310,9 @@ void ShowEditPermissions( const auto saveFor = peer->migrateToOrMe(); const auto chat = saveFor->asChat(); if (!chat - || (!result.slowmodeSeconds && !result.boostsUnrestrict)) { + || (!result.slowmodeSeconds + && !result.boostsUnrestrict + && !result.starsPerMessage)) { save(saveFor, result); return; } @@ -2689,3 +2719,9 @@ bool EditPeerInfoBox::Available(not_null peer) { return false; } } + +void ShowEditChatPermissions( + not_null navigation, + not_null peer) { + ShowEditPermissions(navigation, peer); +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h index 9844320cf..b8787afac 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h @@ -56,3 +56,7 @@ private: not_null _peer; }; + +void ShowEditChatPermissions( + not_null navigation, + not_null peer); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 2797b5394..70ad41acb 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "boxes/share_box.h" #include "core/application.h" -#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "core/ui_integration.h" // TextContext #include "data/components/credits.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -740,10 +740,10 @@ void Controller::setupAboveJoinedWidget() { { QString::number(current.subscription.credits) }, Ui::Text::WithEntities), kMarkupTextOptions, - Core::MarkedTextContext{ + Core::TextContext({ .session = &session(), - .customEmojiRepaint = [=] { widget->update(); }, - }); + .repaint = [=] { widget->update(); }, + })); auto &lifetime = widget->lifetime(); const auto rateValue = lifetime.make_state>( session().credits().rateValue(_peer)); @@ -994,10 +994,7 @@ void Controller::rowClicked(not_null row) { lt_cost, { QString::number(data.subscription.credits) }, Ui::Text::WithEntities), - Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [=] { subtitle1->update(); }, - }); + Core::TextContext({ .session = session })); const auto subtitle2 = box->addRow( object_ptr>( box, @@ -1484,8 +1481,12 @@ object_ptr ShareInviteLinkBox( ? tr::lng_group_invite_copied(tr::now) : copied); }; + auto countMessagesCallback = [=](const TextWithTags &comment) { + return 1; + }; auto submitCallback = [=]( std::vector> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions) { @@ -1503,6 +1504,8 @@ object_ptr ShareInviteLinkBox( result.size() > 1)); } return; + } else if (!checkPaid()) { + return; } *sending = true; @@ -1530,7 +1533,7 @@ object_ptr ShareInviteLinkBox( }; auto filterCallback = [](not_null thread) { if (const auto user = thread->peer()->asUser()) { - if (user->canSendIgnoreRequirePremium()) { + if (user->canSendIgnoreMoneyRestrictions()) { return true; } } @@ -1539,9 +1542,10 @@ object_ptr ShareInviteLinkBox( auto object = Box(ShareBox::Descriptor{ .session = session, .copyCallback = std::move(copyCallback), + .countMessagesCallback = std::move(countMessagesCallback), .submitCallback = std::move(submitCallback), .filterCallback = std::move(filterCallback), - .premiumRequiredError = SharePremiumRequiredError(), + .moneyRestrictionError = ShareMessageMoneyRestrictionError(), }); *box = Ui::MakeWeak(object.data()); return object; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 1b6996fad..c630f603b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/edit_privacy_box.h" #include "settings/settings_power_saving.h" #include "window/window_session_controller.h" #include "window/window_controller.h" @@ -891,11 +892,10 @@ void AddBoostsUnrestrictLabels( manager->registerInternalEmoji( st::boostsMessageIcon, st::boostsMessageIconPadding)); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = session, - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); for (auto i = 0; i != kBoostsUnrestrictValues; ++i) { const auto label = Ui::CreateChild( labels, @@ -942,9 +942,7 @@ rpl::producer AddBoostsUnrestrictSlider( const auto boostsUnrestrict = lifetime.make_state>( channel ? channel->boostsUnrestrict() : 0); - container->add( - object_ptr(container), - { 0, st::infoProfileSkip, 0, st::infoProfileSkip }); + Ui::AddSkip(container); auto enabled = boostsUnrestrict->value( ) | rpl::map(_1 > 0); @@ -1008,19 +1006,20 @@ rpl::producer AddBoostsUnrestrictWrapped( object_ptr>( container, object_ptr(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>( - container, - object_ptr(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(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 slowmodeSeconds; rpl::variable boostsUnrestrict; rpl::variable hasSendRestrictions; + rpl::variable starsPerMessage; }; + const auto state = inner->lifetime().make_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( + 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>( + inner, + object_ptr(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->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(); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h index ce45f4e68..6c4863b7b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h @@ -39,6 +39,7 @@ struct EditPeerPermissionsBoxResult final { ChatRestrictions rights; int slowmodeSeconds = 0; int boostsUnrestrict = 0; + int starsPerMessage = 0; }; void ShowEditPeerPermissionsBox( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp index 96ed8c949..5313a2ae3 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp @@ -363,12 +363,15 @@ object_ptr AddReactionsSelector( const auto customEmojiPaused = [controller = args.controller] { return controller->isGifPausedAtLeastFor(PauseReason::Layer); }; - auto factory = [=](QStringView data, Fn update) - -> std::unique_ptr { + auto context = Core::TextContext({ + .session = session, + }); + context.customEmojiFactory = [=]( + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr { 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( std::move(result), @@ -377,12 +380,10 @@ object_ptr AddReactionsSelector( using namespace Ui::Text; return std::make_unique(std::move(result)); }; - raw->setCustomTextContext([=](Fn 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) { diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index 204afadac..2a4e4f242 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -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 session) { } TextWithEntities CreditsEmojiSmall(not_null 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 SetButtonMarkedLabel( not_null button, rpl::producer text, - Fn update)> context, + Text::MarkedContext context, const style::FlatLabel &st, const style::color *textFg) { const auto buttonLabel = Ui::CreateChild( 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 SetButtonMarkedLabel( not_null session, const style::FlatLabel &st, const style::color *textFg) { - return SetButtonMarkedLabel(button, text, [=](Fn 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 session, std::shared_ptr data, Fn)> done) { diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h index cc84b379e..6dcaef1f8 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.h +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -41,7 +41,7 @@ void SendCreditsBox( not_null SetButtonMarkedLabel( not_null button, rpl::producer text, - Fn update)> context, + Text::MarkedContext context, const style::FlatLabel &st, const style::color *textFg = nullptr); @@ -52,7 +52,7 @@ not_null SetButtonMarkedLabel( const style::FlatLabel &st, const style::color *textFg = nullptr); -void SendStarGift( +void SendStarsForm( not_null session, std::shared_ptr data, Fn)> done); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index ad208b2db..d275a4fee 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -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 @@ -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()); } diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 9a0fea06d..92ba0e3c3 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -246,6 +246,7 @@ private: void addPreparedAsyncFile(Ui::PreparedFile &&file); void checkCharsLimitation(); + void refreshMessagesCount(); [[nodiscard]] Fn prepareSendMenuDetails( const SendFilesBoxDescriptor &descriptor); @@ -261,6 +262,7 @@ private: Ui::PreparedList _list; std::optional _removingIndex; + rpl::variable _messagesCount; SendFilesLimits _limits = {}; Fn _sendMenuDetails; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index bfd4dfe9c..72e99f608 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -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); - void refreshLockedRows(); + void refreshRestrictedRows(); [[nodiscard]] int displayedChatsCount() const; [[nodiscard]] not_null chatThread( @@ -137,7 +138,7 @@ private: void paintChat(Painter &p, not_null chat, int index); void updateChat(not_null peer); void updateChatName(not_null chat); - void initChatLocked(not_null chat); + void initChatRestriction(not_null chat); void repaintChat(not_null peer); int chatIndex(not_null 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>(); + auto waiting = base::flat_set>(); + 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) { - 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->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions()); } -void ShareBox::Inner::initChatLocked(not_null chat) { - if (_descriptor.premiumRequiredError) { +void ShareBox::Inner::initChatRestriction(not_null 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 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 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, + 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 show, not_null history, @@ -1510,12 +1616,14 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( const auto state = std::make_shared(); return [=]( std::vector> &&result, - TextWithTags &&comment, + Fn 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 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(schedule()), }; } @@ -1716,7 +1839,7 @@ void FastShareMessage( const auto requiresInline = item->requiresSendInlineRight(); auto filterCallback = [=](not_null 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::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> &&result, + Fn 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::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)> { - return WritePremiumRequiredError; +auto ShareMessageMoneyRestrictionError() +-> Fn)> { + return WriteMoneyRestrictionError; } diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index 779a60e0a..5cbfb794d 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -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 scheduleBox; }; [[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle(); @@ -87,20 +88,25 @@ void FastShareLink( const QString &url, ShareBoxStyleOverrides st = {}); -struct RecipientPremiumRequiredError; -[[nodiscard]] auto SharePremiumRequiredError() --> Fn)>; +struct RecipientMoneyRestrictionError; +[[nodiscard]] auto ShareMessageMoneyRestrictionError() +-> Fn)>; class ShareBox final : public Ui::BoxContent { public: using CopyCallback = Fn; + using CountMessagesCallback = Fn; using SubmitCallback = Fn>&&, + Fn checkPaid, TextWithTags&&, Api::SendOptions, Data::ForwardOptions)>; using FilterCallback = Fn)>; + [[nodiscard]] static auto DefaultForwardCountMessages( + not_null history, + MessageIdsList msgIds) -> CountMessagesCallback; [[nodiscard]] static SubmitCallback DefaultForwardCallback( std::shared_ptr show, not_null history, @@ -110,6 +116,7 @@ public: struct Descriptor { not_null session; CopyCallback copyCallback; + CountMessagesCallback countMessagesCallback; SubmitCallback submitCallback; FilterCallback filterCallback; object_ptr bottomWidget = { nullptr }; @@ -123,8 +130,9 @@ public: bool show = false; } forwardOptions; - using PremiumRequiredError = RecipientPremiumRequiredError; - Fn)> premiumRequiredError; + using MoneyRestrictionError = RecipientMoneyRestrictionError; + Fn)> 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 _copyLinkText; + rpl::variable _starsToSend; base::Timer _searchTimer; QString _peopleQuery; @@ -195,5 +205,6 @@ private: PeopleQueries _peopleQueries; Ui::Animations::Simple _scrollAnimation; + rpl::lifetime _submitLifetime; }; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index c99b64959..bc15e9059 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -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 &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 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, const GiftDetails &details, Fn 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 update) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = std::move(update), - }; - }; auto star = session->data().customEmojiManager().creditsEmoji(); const auto label = Ui::CreateChild( 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 details; + rpl::variable messageAllowed; std::shared_ptr media; bool submitting = false; }; @@ -1432,13 +1466,25 @@ void SendGiftBox( .descriptor = descriptor, .randomId = base::RandomValue(), }; + 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>( + container, + object_ptr(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( + 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 loading; + }; + const auto state = balance->lifetime().make_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(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 controller, not_null 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, 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 &&gifts) { + if (!gifts.empty()) { + auto &entry = Map[session]; + entry.starsGiftsReady = true; + if (entry.premiumGiftsReady) { + show(); + } + } + }, i->second.lifetime); } void AddUniqueGiftCover( diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index 39380bd3e..b79a417ee 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -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); }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 7ae977bfd..7c2bdfd4c 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -132,8 +132,12 @@ object_ptr 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> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions) { @@ -150,6 +154,8 @@ object_ptr ShareInviteLinkBox( MakeSendErrorBox(error, result.size() > 1)); } return; + } else if (!checkPaid()) { + return; } *sending = true; @@ -178,7 +184,7 @@ object_ptr ShareInviteLinkBox( }; auto filterCallback = [](not_null thread) { if (const auto user = thread->peer()->asUser()) { - if (user->canSendIgnoreRequirePremium()) { + if (user->canSendIgnoreMoneyRestrictions()) { return true; } } @@ -189,6 +195,7 @@ object_ptr ShareInviteLinkBox( auto result = Box(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 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; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 46e4aa04a..b5f24927d 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -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; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 64db33d13..209dab9f5 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -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); diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 421ed55d3..488bc2600 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -695,14 +695,15 @@ GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareSavedGif( } GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareInlineResult( - not_null result) { - auto it = _inlineLayouts.find(result); + std::shared_ptr 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 &r) { - return layoutPrepareInlineResult(r.get()); + const std::shared_ptr &r) { + return layoutPrepareInlineResult(r); }) | ranges::views::filter([](const LayoutItem *item) { return item != nullptr; }) | ranges::to>>; @@ -799,7 +800,7 @@ int GifsListWidget::validateExistingInlineRows(const InlineResults &results) { const auto until = _mosaic.validateExistingRows([&]( not_null item, int untilIndex) { - return item->getResult() != results[untilIndex].get(); + return item->getResult().get() != results[untilIndex].get(); }, results.size()); if (_mosaic.empty()) { diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index 249ba5d0c..355686a34 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -131,7 +131,7 @@ private: }; using InlineResult = InlineBots::Result; - using InlineResults = std::vector>; + using InlineResults = std::vector>; using LayoutItem = InlineBots::Layout::ItemBase; struct InlineCacheEntry { @@ -162,7 +162,8 @@ private: void clearInlineRows(bool resultsDeleted); LayoutItem *layoutPrepareSavedGif(not_null document); - LayoutItem *layoutPrepareInlineResult(not_null result); + LayoutItem *layoutPrepareInlineResult( + std::shared_ptr result); void deleteUnusedGifLayouts(); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 3dafbd289..a35267455 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -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 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 PaidSendButtonText( + rpl::producer stars, + rpl::producer 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); + }); +} diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 15e5f04b6..7d97d9332 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -19,6 +19,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace tr { +struct now_t; +} // namespace tr + namespace Main { class Session; class SessionShow; @@ -169,3 +173,8 @@ private: void SelectTextInFieldWithMargins( not_null field, const TextSelection &selection); + +[[nodiscard]] TextWithEntities PaidSendButtonText(tr::now_t, int stars); +[[nodiscard]] rpl::producer PaidSendButtonText( + rpl::producer stars, + rpl::producer fallback = nullptr); diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index 2d0d52fe0..ddb5b8a93 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -37,7 +37,7 @@ std::map 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" } }; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index ded39a2f8..9e290988f 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -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 close) { close(); - bot->session().local().markBotTrustedOpenGame(bot->id); + bot->session().local().markPeerTrustedOpenGame(bot->id); openGame(); }; controller->show(Ui::MakeConfirmBox({ diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 39a4c9701..fbb5fbfb0 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -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( + user, + Info::Section::Type::CommonGroups)); + } + } + return true; +} + bool ShowStarsExamples( Window::SessionController *controller, const Match &match, @@ -1529,6 +1571,14 @@ const std::vector &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, diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index bd96398d4..667ab43ae 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -76,6 +76,14 @@ const auto CommandByName = base::flat_map{ { 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 shortcut); void pruneListened(); @@ -293,7 +302,7 @@ void Manager::change( Command command, std::optional 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 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 shortcut) { if (shortcut) { _commandByObject.removeAll(shortcut.get()); diff --git a/Telegram/SourceFiles/core/stars_amount.h b/Telegram/SourceFiles/core/stars_amount.h index fc9c8df90..ca0e6fbe2 100644 --- a/Telegram/SourceFiles/core/stars_amount.h +++ b/Telegram/SourceFiles/core/stars_amount.h @@ -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; +} diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 28ddcb4e3..9dd503090 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -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( + simple(data, context), + loop); + }) + : Factory([simple]( + QStringView data, + const Context &context) { + return std::make_unique( + 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 &&callable) { Sandbox::Instance().postponeCall(std::move(callable)); } @@ -152,8 +187,8 @@ bool UiIntegration::screenIsLocked() { std::shared_ptr UiIntegration::createLinkHandler( const EntityLinkData &data, - const std::any &context) { - const auto my = std::any_cast(&context); + const Ui::Text::MarkedContext &context) { + const auto my = std::any_cast(&context.other); switch (data.type) { case EntityType::Url: return (!data.data.isEmpty() @@ -170,7 +205,7 @@ std::shared_ptr UiIntegration::createLinkHandler( return std::make_shared(data.data); case EntityType::Hashtag: - using HashtagMentionType = MarkedTextContext::HashtagMentionType; + using HashtagMentionType = TextContextDetails::HashtagMentionType; if (my && my->type == HashtagMentionType::Twitter) { return std::make_shared( (u"https://twitter.com/hashtag/"_q @@ -190,7 +225,7 @@ std::shared_ptr UiIntegration::createLinkHandler( return std::make_shared(data.data); case EntityType::Mention: - using HashtagMentionType = MarkedTextContext::HashtagMentionType; + using HashtagMentionType = TextContextDetails::HashtagMentionType; if (my && my->type == HashtagMentionType::Twitter) { return std::make_shared( u"https://twitter.com/"_q + data.data.mid(1), @@ -222,7 +257,9 @@ std::shared_ptr UiIntegration::createLinkHandler( case EntityType::Pre: return std::make_shared(data.text, data.type); case EntityType::Phone: - return std::make_shared(my->session, data.text); + return my->session + ? std::make_shared(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 UiIntegration::createCustomEmoji( - QStringView data, - const std::any &context) { - const auto my = std::any_cast(&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( - std::move(result), - my->customEmojiLoopLimit); - } else if (my->customEmojiLoopLimit) { - return std::make_unique( - std::move(result)); - } - return result; -} - -Fn UiIntegration::createSpoilerRepaint(const std::any &context) { - const auto my = std::any_cast(&context); - if (my) { - return my->customEmojiRepaint; - } - const auto common = std::any_cast(&context); - return common ? common->repaint : nullptr; -} - rpl::producer<> UiIntegration::forcePopupMenuHideRequests() { return Core::App().passcodeLockChanges() | rpl::to_empty; } diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index 330d7a84c..fcb9db5ee 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -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 customEmojiRepaint; +}; + +struct TextContextArgs { + not_null session; + TextContextDetails details; + Fn 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 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 createCustomEmoji( - QStringView data, - const std::any &context) override; - Fn createSpoilerRepaint(const std::any &context) override; QString phraseContextCopyText() override; QString phraseContextCopyEmail() override; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 68e59591d..3a8011d57 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -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; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index c1998a441..ca6f362ed 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -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())); }); } diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index cea4020d8..a3e57c0d2 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -37,10 +37,7 @@ void Credits::apply(const MTPDupdateStarsBalance &data) { rpl::producer Credits::rateValue( not_null ownedBotOrChannel) { - return rpl::single( - _session->appConfig().get( - u"stars_usd_withdraw_rate_x1000"_q, - 1200) / 1000.); + return rpl::single(_session->appConfig().starsWithdrawRate()); } void Credits::load(bool force) { diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index 4d039dc00..cecdf3606 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -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); diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index cff431720..f44ae72bb 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -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; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 79b9fe4e6..f21769328 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -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()) { diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 620cca9d1..115516073 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -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; @@ -150,6 +153,9 @@ private: ChannelLocation _location; Data::ChatBotCommands _botCommands; std::unique_ptr _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 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); diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 537663a36..29b9cb451 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -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( diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index adf692218..af37f4e58 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -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; + Fn()> 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; diff --git a/Telegram/SourceFiles/data/data_credits_earn.h b/Telegram/SourceFiles/data/data_credits_earn.h index 41c6f9a5a..e26e2bebc 100644 --- a/Telegram/SourceFiles/data/data_credits_earn.h +++ b/Telegram/SourceFiles/data/data_credits_earn.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index c04dd1ff8..19140fbaa 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -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(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(); } 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 &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(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 89d5e7d18..21aad4cde 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -173,10 +173,15 @@ inline constexpr bool is_flag_type(PeerBarSetting) { return true; }; using PeerBarSettings = base::flags; 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; diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 67e886d78..40f02ef10 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -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(); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index bb4eecaf8..b7412bcb0 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -542,14 +542,22 @@ not_null 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.is_stories_unavailable() @@ -564,14 +572,25 @@ not_null 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 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); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 3e5392173..14f272c6c 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -87,6 +87,8 @@ struct GiftUpdate { Convert, Transfer, Delete, + Pin, + Unpin, }; Data::SavedStarGiftId id; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index e88cae6d1..1b9a8ca06 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -132,6 +132,7 @@ struct SavedStarGift { TimeId date = 0; bool upgradable = false; bool anonymous = false; + bool pinned = false; bool hidden = false; bool mine = false; }; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index cf9f6870a..9f0562e1b 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 662c7b476..9c28fad86 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -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 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 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 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() diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index c7a200e33..515a33313 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -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; @@ -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; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 695cb06ee..eb34f30df 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -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 CustomEmojiManager::create( Ui::Text::CustomEmojiFactory CustomEmojiManager::factory( SizeTag tag, int sizeOverride) { - return [=](QStringView data, Fn 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 session) { return [owner = &session->data()]( - QStringView data, - Fn repaint) -> std::unique_ptr { + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr { 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( 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); }; } diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index 60fd75f06..b01f7561e 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -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); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 86c05781a..d9203debd 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -789,7 +789,3 @@ dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px); dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) { minWidth: 128px; } - -foldersMenu: Menu(menuWithIcons) { - itemPadding: margins(54px, 8px, 44px, 8px); -} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index a9871ded2..f43ff0077 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -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; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index c7e85c94e..6b105f094 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -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, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index c3430f185..1d67b22a2 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -584,10 +584,10 @@ void PaintRow( {}, true))).append(std::move(draftText)); } - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &thread->session(), - .customEmojiRepaint = customEmojiRepaint, - }; + .repaint = customEmojiRepaint, + }); cache.setMarkedText( st::dialogsTextStyle, std::move(draftText), diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index 0aaaa9cdc..eb9339b3e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -174,11 +174,11 @@ void MessageView::prepare( : nullptr; const auto hasImages = !preview.images.empty(); const auto history = item->history(); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history->session(), - .customEmojiRepaint = customEmojiRepaint, + .repaint = customEmojiRepaint, .customEmojiLoopLimit = kEmojiLoopCount, - }; + }); const auto senderTill = (preview.arrowInTextPosition > 0) ? preview.arrowInTextPosition : preview.imagesInTextPosition; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index cd312fd85..82fc64a7a 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -63,11 +63,11 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { && title.version == topic->titleVersion()) { continue; } - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &topic->session(), - .customEmojiRepaint = customEmojiRepaint, + .repaint = customEmojiRepaint, .customEmojiLoopLimit = kIconLoopCount, - }; + }); auto topicTitle = topic->titleWithIcon(); title.topicRootId = rootId; title.version = topic->titleVersion(); diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 8fd0f3e97..3811cbc86 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -546,9 +546,10 @@ SwresamplePointer MakeSwresamplePointer( } // Initialize audio resampler + AvErrorWrap error; #if DA_FFMPEG_NEW_CHANNEL_LAYOUT auto result = (SwrContext*)nullptr; - auto error = AvErrorWrap(swr_alloc_set_opts2( + error = AvErrorWrap(swr_alloc_set_opts2( &result, dstLayout, dstFormat, @@ -564,7 +565,7 @@ SwresamplePointer MakeSwresamplePointer( } #else // DA_FFMPEG_NEW_CHANNEL_LAYOUT auto result = swr_alloc_set_opts( - existing ? existing.get() : nullptr, + existing ? existing->get() : nullptr, dstLayout, dstFormat, dstRate, diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 1c6d68050..abf5bfbfb 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -191,7 +191,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTPint(), // quick_reply_shortcut_id MTP_long(data.veffect().value_or_empty()), MTPFactCheck(), - MTPint()); // report_delivery_until_date + MTPint(), // report_delivery_until_date + MTP_long(data.vpaid_message_stars().value_or_empty())); }); } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 07cd3f5ad..57fe064e5 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -142,6 +142,10 @@ void History::setHasPendingResizedItems() { void History::itemRemoved(not_null item) { if (item == _joinedMessage) { _joinedMessage = nullptr; + } else if (item == _newPeerNameChange) { + _newPeerNameChange = nullptr; + } else if (item == _newPeerPhotoChange) { + _newPeerPhotoChange = nullptr; } item->removeMainView(); if (_lastServerMessage == item) { @@ -465,6 +469,9 @@ not_null History::createItem( }); if (newMessage && result->out() && result->isRegular()) { session().topPeers().increment(peer, result->date()); + if (result->starsPaid()) { + session().credits().load(true); + } } return result; } @@ -1176,12 +1183,6 @@ void History::applyServiceChanges( } if (paid) { // Toast on a current active window. - const auto context = [=](not_null toast) { - return Core::MarkedTextContext{ - .session = &session(), - .customEmojiRepaint = [=] { toast->update(); }, - }; - }; Ui::Toast::Show({ .text = tr::lng_payments_success( tr::now, @@ -1192,7 +1193,9 @@ void History::applyServiceChanges( lt_title, Ui::Text::Bold(paid->title), Ui::Text::WithEntities), - .textContext = context, + .textContext = Core::TextContext({ + .session = &session(), + }), }); } } @@ -1240,6 +1243,8 @@ void History::mainViewRemoved( not_null block, not_null view) { Expects(_joinedMessage != view->data()); + Expects(_newPeerNameChange != view->data()); + Expects(_newPeerPhotoChange != view->data()); if (_firstUnreadView == view) { getNextFirstUnreadMessage(); @@ -3254,6 +3259,85 @@ HistoryItem *History::insertJoinedMessage() { return _joinedMessage; } +void History::checkNewPeerMessages() { + if (!loadedAtTop()) { + return; + } + const auto user = peer->asUser(); + if (!user) { + return; + } + const auto photo = user->photoChangeDate(); + const auto name = user->nameChangeDate(); + if (!photo && _newPeerPhotoChange) { + _newPeerPhotoChange->destroy(); + } + if (!name && _newPeerNameChange) { + _newPeerNameChange->destroy(); + } + if ((!photo || _newPeerPhotoChange) && (!name || _newPeerNameChange)) { + return; + } + + const auto when = [](TimeId date) { + const auto now = base::unixtime::now(); + const auto passed = now - date; + if (passed < 3600) { + return tr::lng_new_contact_updated_now(tr::now); + } else if (passed < 24 * 3600) { + return tr::lng_new_contact_updated_hours( + tr::now, + lt_count, + (passed / 3600)); + } else if (passed < 60 * 24 * 3600) { + return tr::lng_new_contact_updated_days( + tr::now, + lt_count, + (passed / (24 * 3600))); + } + return tr::lng_new_contact_updated_months( + tr::now, + lt_count, + (passed / (30 * 24 * 3600))); + }; + + auto firstDate = TimeId(); + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (item != _newPeerPhotoChange && item != _newPeerNameChange) { + firstDate = item->date(); + break; + } + } + if (firstDate) { + break; + } + } + if (!firstDate) { + firstDate = base::unixtime::serialize( + QDateTime(QDate(2013, 8, 1), QTime(0, 0))); + } + const auto add = [&](tr::phrase phrase, TimeId date) { + const auto result = makeMessage({ + .id = owner().nextLocalMessageId(), + .flags = MessageFlag::Local | MessageFlag::HideDisplayDate, + .date = (--firstDate), + }, PreparedServiceText{ TextWithEntities{ + phrase(tr::now, lt_when, when(date)), + } }); + insertMessageToBlocks(result); + return result; + }; + + if (photo && !_newPeerPhotoChange) { + _newPeerPhotoChange = add(tr::lng_new_contact_updated_photo, photo); + } + if (name && !_newPeerNameChange) { + _newPeerNameChange = add(tr::lng_new_contact_updated_name, name); + } +} + void History::insertMessageToBlocks(not_null item) { Expects(item->mainView() == nullptr); @@ -3307,6 +3391,8 @@ void History::checkLocalMessages() { && peer->asChannel()->inviter && goodDate(peer->asChannel()->inviteDate)) { insertJoinedMessage(); + } else { + checkNewPeerMessages(); } } @@ -3320,6 +3406,15 @@ void History::removeJoinedMessage() { } } +void History::removeNewPeerMessages() { + if (_newPeerNameChange) { + _newPeerNameChange->destroy(); + } + if (_newPeerPhotoChange) { + _newPeerPhotoChange->destroy(); + } +} + void History::reactionsEnabledChanged(bool enabled) { if (!enabled) { for (const auto &item : _items) { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index af7d86a4a..57b1203d0 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -82,6 +82,7 @@ public: [[nodiscard]] HistoryItem *joinedMessageInstance() const; void checkLocalMessages(); void removeJoinedMessage(); + void removeNewPeerMessages(); void reactionsEnabledChanged(bool enabled); @@ -545,6 +546,7 @@ private: HistoryItem *insertJoinedMessage(); void insertMessageToBlocks(not_null item); + void checkNewPeerMessages(); [[nodiscard]] Dialogs::BadgesState computeBadgesState() const; [[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder( @@ -563,6 +565,8 @@ private: Element *_unreadBarView = nullptr; Element *_firstUnreadView = nullptr; HistoryItem *_joinedMessage = nullptr; + HistoryItem *_newPeerNameChange = nullptr; + HistoryItem *_newPeerPhotoChange = nullptr; bool _loadedAtTop = false; bool _loadedAtBottom = true; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 4fe6bd08f..06d2a7ce1 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -3961,7 +3961,8 @@ auto HistoryInner::reactionButtonParameters( } void HistoryInner::mouseActionUpdate() { - if (hasPendingResizedItems() || !_mouseActive) { + if (hasPendingResizedItems() + || (!_mouseActive && !window()->isActiveWindow())) { return; } @@ -4407,6 +4408,12 @@ void HistoryInner::refreshAboutView(bool force) { _aboutView = std::make_unique( _history, _history->delegateMixin()->delegate()); + _aboutView->refreshRequests() | rpl::start_with_next([=] { + updateBotInfo(); + }, _aboutView->lifetime()); + _aboutView->sendIntroSticker() | rpl::start_to_stream( + _sendIntroSticker, + _aboutView->lifetime()); } }; if (const auto user = _peer->asUser()) { @@ -4415,15 +4422,17 @@ void HistoryInner::refreshAboutView(bool force) { if (!info->inited) { session().api().requestFullPeer(user); } - } else if (user->meRequiresPremiumToWrite() - && !user->session().premium() - && !historyHeight()) { + } else if (!user->isContact() + && !user->phoneCountryCode().isEmpty()) { refresh(); } else if (!historyHeight()) { - if (!user->isFullLoaded()) { - session().api().requestFullPeer(user); - } else { + if (user->starsPerMessage() > 0 + || (user->requiresPremiumToWrite() + && !user->session().premium()) + || user->isFullLoaded()) { refresh(); + } else { + session().api().requestFullPeer(user); } } } @@ -4840,6 +4849,11 @@ ClickContext HistoryInner::prepareClickContext( }; } +auto HistoryInner::sendIntroSticker() const +-> rpl::producer> { + return _sendIntroSticker.events(); +} + auto HistoryInner::DelegateMixin() -> std::unique_ptr { return std::make_unique(); diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index cec98e8b3..1a30e4202 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -221,6 +221,9 @@ public: Qt::MouseButton button, FullMsgId itemId) const; + [[nodiscard]] auto sendIntroSticker() const + -> rpl::producer>; + [[nodiscard]] static auto DelegateMixin() -> std::unique_ptr; @@ -466,6 +469,7 @@ private: std::unique_ptr _aboutView; std::unique_ptr _emptyPainter; std::unique_ptr _translateTracker; + rpl::event_stream> _sendIntroSticker; mutable History *_curHistory = nullptr; mutable int _curBlock = 0; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index cd253c6a8..c30243890 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -408,6 +408,7 @@ HistoryItem::HistoryItem( .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), .date = data.vdate().v, .shortcutId = data.vquick_reply_shortcut_id().value_or_empty(), + .starsPaid = int(data.vpaid_message_stars().value_or_empty()), .effectId = data.veffect().value_or_empty(), }) { _boostsApplied = data.vfrom_boosts_applied().value_or_empty(); @@ -785,6 +786,7 @@ HistoryItem::HistoryItem( : history->peer) , _flags(FinalizeMessageFlags(history, fields.flags)) , _date(fields.date) +, _starsPaid(fields.starsPaid) , _shortcutId(fields.shortcutId) , _effectId(fields.effectId) { Expects(!_shortcutId @@ -833,6 +835,10 @@ TimeId HistoryItem::date() const { return _date; } +int HistoryItem::starsPaid() const { + return _starsPaid; +} + bool HistoryItem::awaitingVideoProcessing() const { return (_flags & MessageFlag::EstimatedDate); } @@ -2315,6 +2321,10 @@ void HistoryItem::setRealId(MsgId newId) { if (const auto reply = Get()) { incrementReplyToTopCounter(); } + + if (out() && starsPaid()) { + _history->session().credits().load(true); + } } bool HistoryItem::canPin() const { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 4c4624d6a..b1513ba68 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -103,6 +103,7 @@ struct HistoryItemCommonFields { FullReplyTo replyTo; TimeId date = 0; BusinessShortcutId shortcutId = 0; + int starsPaid = 0; UserId viaBotId = 0; QString postAuthor; uint64 groupedId = 0; @@ -321,6 +322,9 @@ public: [[nodiscard]] bool hideEditedBadge() const { return (_flags & MessageFlag::HideEdited); } + [[nodiscard]] bool hideDisplayDate() const { + return (_flags & MessageFlag::HideDisplayDate); + } [[nodiscard]] bool isLocal() const { return _flags & MessageFlag::Local; } @@ -552,6 +556,7 @@ public: // content uses the color of the original sender. [[nodiscard]] PeerData *contentColorsFrom() const; [[nodiscard]] uint8 contentColorIndex() const; + [[nodiscard]] int starsPaid() const; [[nodiscard]] std::unique_ptr createView( not_null delegate, @@ -686,6 +691,7 @@ private: TimeId _date = 0; TimeId _ttlDestroyAt = 0; int _boostsApplied = 0; + int _starsPaid = 0; BusinessShortcutId _shortcutId = 0; MessageGroupId _groupId = MessageGroupId(); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 002e35e56..695b60bab 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -209,7 +209,9 @@ void HistoryMessageForwarded::create( const HistoryMessageVia *via, not_null item) const { auto phrase = TextWithEntities(); - auto context = Core::MarkedTextContext{}; + auto context = Core::TextContext({ + .session = &item->history()->session(), + }); const auto fromChannel = originalSender && originalSender->isChannel() && !originalSender->isMegagroup(); @@ -219,8 +221,7 @@ void HistoryMessageForwarded::create( : originalHiddenSenderInfo->name) }; if (const auto copy = originalSender) { - context.session = ©->owner().session(); - context.customEmojiRepaint = [=] { + context.repaint = [=] { // It is important to capture here originalSender by value, // not capture the HistoryMessageForwarded* and read the // originalSender field, because the components themselves @@ -229,7 +230,7 @@ void HistoryMessageForwarded::create( copy->owner().requestItemRepaint(item); }; phrase = Ui::Text::SingleCustomEmoji( - context.session->data().customEmojiManager().peerUserpicEmojiData( + copy->owner().customEmojiManager().peerUserpicEmojiData( copy, st::fwdTextUserpicPadding)); } @@ -777,10 +778,10 @@ ReplyKeyboard::ReplyKeyboard( _st->textStyle(), TextUtilities::SingleLine(textWithEntities), kMarkupTextOptions, - Core::MarkedTextContext{ + Core::TextContext({ .session = &item->history()->owner().session(), - .customEmojiRepaint = [=] { _st->repaint(item); }, - }); + .repaint = [=] { _st->repaint(item); }, + })); } else { button.text.setText( _st->textStyle(), diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index e59131c2e..4fa927cb0 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -38,10 +38,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "core/application.h" #include "core/click_handler_types.h" // ClickHandlerContext. +#include "settings/settings_credits_graphics.h" +#include "storage/storage_account.h" #include "ui/boxes/confirm_box.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" +#include "ui/widgets/checkbox.h" #include "ui/item_text_options.h" #include "lang/lang_keys.h" @@ -64,6 +67,33 @@ bool PeerCallKnown(not_null peer) { } // namespace +int ComputeSendingMessagesCount( + not_null history, + const SendingErrorRequest &request) { + auto result = 0; + if (request.text && !request.text->empty()) { + auto sending = TextWithEntities(); + auto left = TextWithEntities{ + request.text->text, + TextUtilities::ConvertTextTagsToEntities(request.text->tags) + }; + auto prepareFlags = Ui::ItemTextOptions( + history, + history->session().user()).flags; + TextUtilities::PrepareForSending(left, prepareFlags); + + while (TextUtilities::CutPart(sending, left, MaxMessageSize)) { + ++result; + } + if (!result) { + ++result; + } + } + return result + + (request.story ? 1 : 0) + + (request.forward ? int(request.forward->size()) : 0); +} + Data::SendError GetErrorForSending( not_null peer, SendingErrorRequest request) { @@ -98,9 +128,9 @@ Data::SendError GetErrorForSending( } } if (peer->slowmodeApplied()) { - const auto count = (hasText ? 1 : 0) - + (request.story ? 1 : 0) - + (request.forward ? int(request.forward->size()) : 0); + const auto count = request.messagesCount + ? request.messagesCount + : ComputeSendingMessagesCount(thread->owningHistory(), request); if (const auto history = peer->owner().historyLoaded(peer)) { if (!request.ignoreSlowmodeCountdown && (history->latestSendingMessage() != nullptr) @@ -138,7 +168,6 @@ Data::SendError GetErrorForSending( Ui::FormatDurationWordsSlowmode(left)); } } - return {}; } @@ -160,6 +189,34 @@ Data::SendErrorWithThread GetErrorForSending( } return {}; } + +std::optional ComputePaymentDetails( + not_null peer, + int messagesCount) { + if (const auto user = peer->asUser()) { + if (user->hasStarsPerMessage() + && !user->messageMoneyRestrictionsKnown()) { + user->updateFull(); + return {}; + } + } else if (const auto channel = peer->asChannel()) { + if (!channel->isFullLoaded()) { + channel->updateFull(); + return {}; + } + } + if (!peer->session().credits().loaded()) { + peer->session().credits().load(); + return {}; + } else if (const auto perMessage = peer->starsPerMessageChecked()) { + return SendPaymentDetails{ + .messages = messagesCount, + .stars = messagesCount * perMessage, + }; + } + return SendPaymentDetails(); +} + object_ptr MakeSendErrorBox( const Data::SendErrorWithThread &error, bool withTitle) { @@ -192,6 +249,210 @@ object_ptr MakeSendErrorBox( }); } +void ShowSendPaidConfirm( + not_null navigation, + not_null peer, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles) { + return ShowSendPaidConfirm( + navigation->uiShow(), + peer, + details, + confirmed, + styles); +} + +void ShowSendPaidConfirm( + std::shared_ptr show, + not_null peer, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles) { + ShowSendPaidConfirm( + std::move(show), + std::vector>{ peer }, + details, + confirmed, + styles); +} + +void ShowSendPaidConfirm( + std::shared_ptr show, + const std::vector> &peers, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles) { + Expects(!peers.empty()); + + const auto singlePeer = (peers.size() > 1) + ? (PeerData*)nullptr + : peers.front().get(); + const auto singlePeerId = singlePeer ? singlePeer->id : PeerId(); + const auto check = [=] { + const auto required = details.stars; + if (!required) { + return; + } + const auto done = [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Success + || result == Settings::SmallBalanceResult::Already) { + confirmed(); + } + }; + Settings::MaybeRequestBalanceIncrease( + show, + required, + Settings::SmallBalanceForMessage{ .recipientId = singlePeerId }, + done); + }; + auto usersOnly = true; + for (const auto &peer : peers) { + if (!peer->isUser()) { + usersOnly = false; + break; + } + } + const auto singlePeerStars = singlePeer + ? singlePeer->starsPerMessageChecked() + : 0; + if (singlePeer) { + const auto session = &singlePeer->session(); + const auto trusted = session->local().isPeerTrustedPayForMessage( + singlePeerId, + singlePeerStars); + if (trusted) { + check(); + return; + } + } + const auto messages = details.messages; + const auto stars = details.stars; + show->showBox(Box([=](not_null box) { + const auto trust = std::make_shared>(); + const auto proceed = [=](Fn close) { + if (singlePeer && (*trust)->checked()) { + const auto session = &singlePeer->session(); + session->local().markPeerTrustedPayForMessage( + singlePeerId, + singlePeerStars); + } + check(); + close(); + }; + Ui::ConfirmBox(box, { + .text = (singlePeer + ? tr::lng_payment_confirm_text( + tr::now, + lt_count, + stars / messages, + lt_name, + Ui::Text::Bold(singlePeer->shortName()), + Ui::Text::RichLangValue) + : (usersOnly + ? tr::lng_payment_confirm_users + : tr::lng_payment_confirm_chats)( + tr::now, + lt_count, + int(peers.size()), + Ui::Text::RichLangValue)).append(' ').append( + tr::lng_payment_confirm_sure( + tr::now, + lt_count, + messages, + lt_amount, + tr::lng_payment_confirm_amount( + tr::now, + lt_count, + stars, + Ui::Text::RichLangValue), + Ui::Text::RichLangValue)), + .confirmed = proceed, + .confirmText = tr::lng_payment_confirm_button( + lt_count, + rpl::single(messages * 1.)), + .labelStyle = styles.label, + .title = tr::lng_payment_confirm_title(), + }); + if (singlePeer) { + const auto skip = st::defaultCheckbox.margin.top(); + *trust = box->addRow( + object_ptr( + box, + tr::lng_payment_confirm_dont_ask(tr::now), + false, + (styles.checkbox + ? *styles.checkbox + : st::defaultCheckbox)), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + } + })); +} + +bool SendPaymentHelper::check( + not_null navigation, + not_null peer, + int messagesCount, + int starsApproved, + Fn resend, + PaidConfirmStyles styles) { + return check( + navigation->uiShow(), + peer, + messagesCount, + starsApproved, + std::move(resend), + styles); +} + +bool SendPaymentHelper::check( + std::shared_ptr show, + not_null peer, + int messagesCount, + int starsApproved, + Fn resend, + PaidConfirmStyles styles) { + clear(); + + const auto details = ComputePaymentDetails(peer, messagesCount); + if (!details) { + _resend = [=] { resend(starsApproved); }; + + if (!peer->session().credits().loaded()) { + peer->session().credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + if (const auto callback = base::take(_resend)) { + callback(); + } + }, _lifetime); + } + + peer->session().changes().peerUpdates( + peer, + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=] { + if (const auto callback = base::take(_resend)) { + callback(); + } + }, _lifetime); + + return false; + } else if (const auto stars = details->stars; stars > starsApproved) { + ShowSendPaidConfirm(show, peer, *details, [=] { + resend(stars); + }, styles); + return false; + } + return true; +} + +void SendPaymentHelper::clear() { + _lifetime.destroy(); + _resend = nullptr; +} + void RequestDependentMessageItem( not_null item, PeerId peerId, diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 4160753bc..dfe2c737c 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -11,11 +11,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace style { +struct FlatLabel; +struct Checkbox; +} // namespace style + namespace Api { struct SendOptions; struct SendAction; } // namespace Api +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { class Story; class Thread; @@ -25,12 +34,17 @@ struct SendErrorWithThread; namespace Main { class Session; +class SessionShow; } // namespace Main namespace Ui { class BoxContent; } // namespace Ui +namespace Window { +class SessionNavigation; +} // namespace Window + struct PreparedServiceText { TextWithEntities text; std::vector links; @@ -114,8 +128,12 @@ struct SendingErrorRequest { const HistoryItemsList *forward = nullptr; const Data::Story *story = nullptr; const TextWithTags *text = nullptr; + int messagesCount = 0; bool ignoreSlowmodeCountdown = false; }; +[[nodiscard]] int ComputeSendingMessagesCount( + not_null history, + const SendingErrorRequest &request); [[nodiscard]] Data::SendError GetErrorForSending( not_null peer, SendingErrorRequest request); @@ -123,6 +141,62 @@ struct SendingErrorRequest { not_null thread, SendingErrorRequest request); +struct SendPaymentDetails { + int messages = 0; + int stars = 0; +}; +[[nodiscard]] std::optional ComputePaymentDetails( + not_null peer, + int messagesCount); + +struct PaidConfirmStyles { + const style::FlatLabel *label = nullptr; + const style::Checkbox *checkbox = nullptr; +}; +void ShowSendPaidConfirm( + not_null navigation, + not_null peer, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles = {}); +void ShowSendPaidConfirm( + std::shared_ptr show, + not_null peer, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles = {}); +void ShowSendPaidConfirm( + std::shared_ptr show, + const std::vector> &peers, + SendPaymentDetails details, + Fn confirmed, + PaidConfirmStyles styles = {}); + +class SendPaymentHelper final { +public: + [[nodiscard]] bool check( + not_null navigation, + not_null peer, + int messagesCount, + int starsApproved, + Fn resend, + PaidConfirmStyles styles = {}); + [[nodiscard]] bool check( + std::shared_ptr show, + not_null peer, + int messagesCount, + int starsApproved, + Fn resend, + PaidConfirmStyles styles = {}); + + void clear(); + +private: + Fn _resend; + rpl::lifetime _lifetime; + +}; + [[nodiscard]] Data::SendErrorWithThread GetErrorForSending( const std::vector> &threads, SendingErrorRequest request); diff --git a/Telegram/SourceFiles/history/history_view_top_toast.cpp b/Telegram/SourceFiles/history/history_view_top_toast.cpp index 7736f971d..a68061b9f 100644 --- a/Telegram/SourceFiles/history/history_view_top_toast.cpp +++ b/Telegram/SourceFiles/history/history_view_top_toast.cpp @@ -31,16 +31,10 @@ void InfoTooltip::show( not_null session, const TextWithEntities &text, Fn hiddenCallback) { - const auto context = [=](not_null toast) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [=] { toast->update(); }, - }; - }; hide(anim::type::normal); _topToast = Ui::Toast::Show(parent, Ui::Toast::Config{ .text = text, - .textContext = context, + .textContext = Core::TextContext({ .session = session }), .st = &st::historyInfoToast, .attach = RectPart::Top, .duration = CountToastDuration(text), diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4aa1f69b2..d869fcff6 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_unread_things.h" #include "ui/boxes/confirm_box.h" #include "boxes/delete_messages_box.h" +#include "boxes/send_credits_box.h" #include "boxes/send_files_box.h" #include "boxes/share_box.h" #include "boxes/edit_caption_box.h" @@ -129,6 +130,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_config.h" #include "lang/lang_keys.h" #include "settings/business/settings_quick_replies.h" +#include "settings/settings_credits_graphics.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/file_upload.h" @@ -146,6 +148,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" #include "ui/chat/continuous_scroll.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/popup_menu.h" #include "ui/item_text_options.h" @@ -521,6 +524,11 @@ HistoryWidget::HistoryWidget( moveFieldControls(); }, lifetime()); + _send->widthValue() | rpl::skip(1) | rpl::start_with_next([=] { + updateFieldSize(); + moveFieldControls(); + }, _send->lifetime()); + _keyboard->sendCommandRequests( ) | rpl::start_with_next([=](Bot::SendCommandRequest r) { sendBotCommand(r); @@ -568,6 +576,7 @@ HistoryWidget::HistoryWidget( session().attachWebView().attachBotsUpdates(), session().changes().peerUpdates( Data::PeerUpdate::Flag::Rights + | Data::PeerUpdate::Flag::StarsPerMessage ) | rpl::filter([=](const Data::PeerUpdate &update) { return update.peer == _peer; }) | rpl::to_empty @@ -841,7 +850,7 @@ HistoryWidget::HistoryWidget( ) | rpl::start_with_next([=](UserData::Flags::Change change) { if (change.diff & UserData::Flag::Premium) { if (const auto user = _peer ? _peer->asUser() : nullptr) { - if (user->meRequiresPremiumToWrite()) { + if (user->requiresPremiumToWrite()) { handlePeerUpdate(); } } @@ -865,6 +874,7 @@ HistoryWidget::HistoryWidget( | PeerUpdateFlag::MessagesTTL | PeerUpdateFlag::ChatThemeEmoji | PeerUpdateFlag::FullInfo + | PeerUpdateFlag::StarsPerMessage ) | rpl::filter([=](const Data::PeerUpdate &update) { return (update.peer.get() == _peer); }) | rpl::map([](const Data::PeerUpdate &update) { @@ -872,6 +882,7 @@ HistoryWidget::HistoryWidget( }) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) { if (flags & PeerUpdateFlag::Rights) { updateFieldPlaceholder(); + updateSendButtonType(); _preview->checkNow(false); const auto was = (_sendAs != nullptr); @@ -899,6 +910,10 @@ HistoryWidget::HistoryWidget( return; } } + if (flags & PeerUpdateFlag::StarsPerMessage) { + updateFieldPlaceholder(); + updateSendButtonType(); + } if (flags & PeerUpdateFlag::BotStartToken) { updateControlsVisibility(); updateControlsGeometry(); @@ -937,7 +952,9 @@ HistoryWidget::HistoryWidget( } if (flags & PeerUpdateFlag::FullInfo) { fullInfoUpdated(); - if (const auto channel = _peer->asChannel()) { + if (_peer->starsPerMessageChecked()) { + session().credits().load(); + } else if (const auto channel = _peer->asChannel()) { if (channel->allowedReactions().paidEnabled) { session().credits().load(); } @@ -1170,19 +1187,8 @@ void HistoryWidget::initVoiceRecordBar() { }, lifetime()); _voiceRecordBar->sendVoiceRequests( - ) | rpl::start_with_next([=](const auto &data) { - if (!canWriteMessage() || data.bytes.isEmpty() || !_history) { - return; - } - - auto action = prepareSendAction(data.options); - session().api().sendVoiceMessage( - data.bytes, - data.waveform, - data.duration, - data.video, - action); - _voiceRecordBar->clearListenState(); + ) | rpl::start_with_next([=](const VoiceToSend &data) { + sendVoice(data); }, lifetime()); _voiceRecordBar->cancelRequests( @@ -1765,6 +1771,9 @@ void HistoryWidget::orderWidgets() { if (_contactStatus) { _contactStatus->bar().raise(); } + if (_paysStatus) { + _paysStatus->bar().raise(); + } if (_translateBar) { _translateBar->raise(); } @@ -2465,6 +2474,7 @@ void HistoryWidget::showHistory( setHistory(nullptr); _list = nullptr; _peer = nullptr; + _sendPayment.clear(); _topicsRequested.clear(); _canSendMessages = false; _canSendTexts = false; @@ -2494,6 +2504,7 @@ void HistoryWidget::showHistory( _showAtMsgId = showAtMsgId; _showAtMsgParams = params; _historyInited = false; + _paysStatus = nullptr; _contactStatus = nullptr; _businessBotStatus = nullptr; @@ -2514,6 +2525,14 @@ void HistoryWidget::showHistory( refreshGiftToChannelShown(); if (const auto user = _peer->asUser()) { + _paysStatus = std::make_unique( + controller(), + this, + user); + _paysStatus->bar().heightValue( + ) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _paysStatus->bar().lifetime()); _businessBotStatus = std::make_unique( controller(), this, @@ -2591,6 +2610,12 @@ void HistoryWidget::showHistory( _scroll->hide(); _list = _scroll->setOwnedWidget( object_ptr(this, _scroll, controller(), _history)); + _list->sendIntroSticker( + ) | rpl::start_with_next([=](not_null sticker) { + sendExistingDocument( + sticker, + Api::MessageToSend(prepareSendAction({}))); + }, _list->lifetime()); _list->show(); if (const auto channel = _peer->asChannel()) { @@ -3107,16 +3132,13 @@ bool HistoryWidget::contentOverlapped(const QRect &globalRect) { } bool HistoryWidget::canWriteMessage() const { - if (!_history || !_canSendMessages) { - return false; - } - if (isBlocked() || isJoinChannel() || isMuteUnmute() || isBotStart()) { - return false; - } - if (isSearching()) { - return false; - } - return true; + return _history + && _canSendMessages + && !isBlocked() + && !isJoinChannel() + && !isMuteUnmute() + && !isBotStart() + && !isSearching(); } void HistoryWidget::updateControlsVisibility() { @@ -3165,6 +3187,9 @@ void HistoryWidget::updateControlsVisibility() { if (_requestsBar) { _requestsBar->show(); } + if (_paysStatus) { + _paysStatus->show(); + } if (_contactStatus) { _contactStatus->show(); } @@ -4258,7 +4283,7 @@ void HistoryWidget::checkReplyReturns() { } void HistoryWidget::cancelInlineBot() { - auto &textWithTags = _field->getTextWithTags(); + const auto &textWithTags = _field->getTextWithTags(); if (textWithTags.text.size() > _inlineBotUsername.size() + 2) { setFieldText( { '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, @@ -4418,6 +4443,9 @@ void HistoryWidget::hideChildWidgets() { if (_chooseTheme) { _chooseTheme->hide(); } + if (_paysStatus) { + _paysStatus->hide(); + } if (_contactStatus) { _contactStatus->hide(); } @@ -4453,6 +4481,34 @@ Api::SendAction HistoryWidget::prepareSendAction( return result; } +void HistoryWidget::sendVoice(const VoiceToSend &data) { + if (!canWriteMessage() || data.bytes.isEmpty() || !_history) { + return; + } + + const auto withPaymentApproved = [=](int approved) { + auto copy = data; + copy.options.starsApproved = approved; + sendVoice(copy); + }; + const auto checked = checkSendPayment( + 1 + int(_forwardPanel->items().size()), + data.options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + + auto action = prepareSendAction(data.options); + session().api().sendVoiceMessage( + data.bytes, + data.waveform, + data.duration, + data.video, + action); + _voiceRecordBar->clearListenState(); +} + void HistoryWidget::send(Api::SendOptions options) { const auto settings = &AyuSettings::getInstance(); if (AyuSettings::isUseScheduledMessages() && !options.scheduled) { @@ -4472,9 +4528,7 @@ void HistoryWidget::send(Api::SendOptions options) { return; } else if (!options.scheduled && showSlowmodeError()) { return; - } - - if (_voiceRecordBar->isListenState()) { + } else if (_voiceRecordBar->isListenState()) { _voiceRecordBar->requestToSendWithOptions(options); return; } @@ -4488,9 +4542,16 @@ void HistoryWidget::send(Api::SendOptions options) { message.webPage = _preview->draft(); const auto ignoreSlowmodeCountdown = (options.scheduled != 0); + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + send(copy); + }; if (showSendMessageError( message.textWithTags, - ignoreSlowmodeCountdown)) { + ignoreSlowmodeCountdown, + withPaymentApproved, + options.starsApproved)) { return; } @@ -4548,6 +4609,8 @@ void HistoryWidget::sendScheduled(Api::SendOptions initialOptions) { SendMenu::Details HistoryWidget::sendMenuDetails() const { const auto type = !_peer ? SendMenu::Type::Disabled + : _peer->starsPerMessageChecked() + ? SendMenu::Type::SilentOnly : _peer->isSelf() ? SendMenu::Type::Reminder : HistoryView::CanScheduleUntilOnline(_peer) @@ -4831,6 +4894,19 @@ FullMsgId HistoryWidget::cornerButtonsCurrentId() { : FullMsgId(); } +bool HistoryWidget::checkSendPayment( + int messagesCount, + int starsApproved, + Fn withPaymentApproved) { + return _peer + && _sendPayment.check( + controller(), + _peer, + messagesCount, + starsApproved, + std::move(withPaymentApproved)); +} + void HistoryWidget::checkSuggestToGigagroup() { const auto group = _peer ? _peer->asMegagroup() : nullptr; if (!group || !group->owner().suggestToGigagroup(group)) { @@ -4996,13 +5072,32 @@ void HistoryWidget::mouseReleaseEvent(QMouseEvent *e) { } void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { -// replyTo != 0 from ReplyKeyboardMarkup, == 0 from command links + sendBotCommand(request, {}); +} + +void HistoryWidget::sendBotCommand( + const Bot::SendCommandRequest &request, + Api::SendOptions options) { + // replyTo != 0 from ReplyKeyboardMarkup, == 0 from command links if (_peer != request.peer.get()) { return; } else if (showSlowmodeError()) { return; } + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendBotCommand(request, copy); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + const auto forMsgId = _keyboard->forMsgId(); const auto lastKeyboardUsed = (forMsgId == request.replyTo.messageId) && (forMsgId == FullMsgId(_peer->id, _history->lastKeyboardId)); @@ -5012,7 +5107,7 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { ? request.command : Bot::WrapCommandInChat(_peer, request.command, request.context); - auto message = Api::MessageToSend(prepareSendAction({})); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = { toSend, TextWithTags::Tags() }; message.action.replyTo = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) @@ -5248,19 +5343,30 @@ void HistoryWidget::updateSendButtonType() { using Type = Ui::SendButton::Type; const auto type = computeSendButtonType(); - _send->setType(type); - // This logic is duplicated in RepliesWidget. const auto disabledBySlowmode = _peer && _peer->slowmodeApplied() && (_history->latestSendingMessage() != nullptr); - const auto delay = [&] { return (type != Type::Cancel && type != Type::Save && _peer) ? _peer->slowmodeSecondsLeft() : 0; }(); - _send->setSlowmodeDelay(delay); + const auto perMessage = _peer ? _peer->starsPerMessageChecked() : 0; + const auto messages = !_peer + ? 0 + : _voiceRecordBar->isListenState() + ? 1 + : ComputeSendingMessagesCount(_history, { + .forward = &_forwardPanel->items(), + .text = &_field->getTextWithTags(), + }); + const auto stars = perMessage ? (perMessage * messages) : 0; + _send->setState({ + .type = (delay > 0) ? Type::Slowmode : type, + .slowmodeDelay = delay, + .starsToSend = stars, + }); _send->setDisabled(disabledBySlowmode && (type == Type::Send || type == Type::Record @@ -5861,6 +5967,9 @@ void HistoryWidget::fieldFocused() { } void HistoryWidget::updateFieldPlaceholder() { + _voiceRecordBar->setPauseInsteadSend(_history + && _history->peer->starsPerMessageChecked() > 0); + if (!_editMsgId && _inlineBot && !_inlineLookingUpBot) { _field->setPlaceholder( rpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)), @@ -5869,14 +5978,21 @@ void HistoryWidget::updateFieldPlaceholder() { } _field->setPlaceholder([&]() -> rpl::producer { + const auto peer = _history ? _history->peer.get() : nullptr; if (_editMsgId) { return tr::lng_edit_message_text(); - } else if (!_history) { + } else if (!peer) { return tr::lng_message_ph(); } else if ((_kbShown || _keyboard->forceReply()) && !_keyboard->placeholder().isEmpty()) { return rpl::single(_keyboard->placeholder()); - } else if (const auto channel = _history->peer->asChannel()) { + } else if (const auto stars = peer->starsPerMessageChecked()) { + return tr::lng_message_paid_ph( + lt_amount, + tr::lng_prize_credits_amount( + lt_count, + rpl::single(stars * 1.))); + } else if (const auto channel = peer->asChannel()) { const auto topic = resolveReplyToTopic(); const auto topicRootId = topic ? topic->rootId() @@ -5993,24 +6109,31 @@ Data::ForumTopic *HistoryWidget::resolveReplyToTopic() { bool HistoryWidget::showSendMessageError( const TextWithTags &textWithTags, - bool ignoreSlowmodeCountdown) { + bool ignoreSlowmodeCountdown, + Fn withPaymentApproved, + int starsApproved) { if (!_canSendMessages) { return false; } const auto topicRootId = resolveReplyToTopicRootId(); - const auto error = GetErrorForSending( - _peer, - { - .topicRootId = topicRootId, - .forward = &_forwardPanel->items(), - .text = &textWithTags, - .ignoreSlowmodeCountdown = ignoreSlowmodeCountdown, - }); - if (!error) { - return false; + auto request = SendingErrorRequest{ + .topicRootId = topicRootId, + .forward = &_forwardPanel->items(), + .text = &textWithTags, + .ignoreSlowmodeCountdown = ignoreSlowmodeCountdown, + }; + request.messagesCount = ComputeSendingMessagesCount(_history, request); + const auto error = GetErrorForSending(_peer, request); + if (error) { + Data::ShowSendErrorToast(controller(), _peer, error); + return true; } - Data::ShowSendErrorToast(controller(), _peer, error); - return true; + + return withPaymentApproved + && !checkSendPayment( + request.messagesCount, + starsApproved, + withPaymentApproved); } bool HistoryWidget::confirmSendingFiles(const QStringList &files) { @@ -6107,27 +6230,52 @@ void HistoryWidget::sendingFilesConfirmed( if (showSendingFilesError(list, compress)) { return; } + auto groups = DivideByGroups( std::move(list), way, _peer->slowmodeApplied()); + auto bundle = PrepareFilesBundle( + std::move(groups), + way, + std::move(caption), + ctrlShiftEnter); + sendingFilesConfirmed(std::move(bundle), options); +} + +void HistoryWidget::sendingFilesConfirmed( + std::shared_ptr bundle, + Api::SendOptions options) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendingFilesConfirmed(bundle, copy); + }; + const auto checked = checkSendPayment( + bundle->totalCount, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + + const auto compress = bundle->way.sendImagesAsPhotos(); const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if ((groups.size() != 1 || !groups.front().sentWithCaption()) - && !caption.text.isEmpty()) { + if (bundle->sendComment) { auto message = Api::MessageToSend(action); - message.textWithTags = base::take(caption); + message.textWithTags = base::take(bundle->caption); session().api().sendMessage(std::move(message)); } - for (auto &group : groups) { + for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; session().api().sendFiles( std::move(group.list), type, - base::take(caption), + base::take(bundle->caption), album, action); } @@ -6332,8 +6480,13 @@ void HistoryWidget::updateControlsGeometry() { _translateBar->move(0, translateTop); _translateBar->resizeToWidth(width()); } - const auto contactStatusTop = translateTop + const auto paysStatusTop = translateTop + (_translateBar ? _translateBar->height() : 0); + if (_paysStatus) { + _paysStatus->bar().move(0, paysStatusTop); + } + const auto contactStatusTop = paysStatusTop + + (_paysStatus ? _paysStatus->bar().height() : 0); if (_contactStatus) { _contactStatus->bar().move(0, contactStatusTop); } @@ -6584,6 +6737,9 @@ void HistoryWidget::updateHistoryGeometry( if (_requestsBar) { newScrollHeight -= _requestsBar->height(); } + if (_paysStatus) { + newScrollHeight -= _paysStatus->bar().height(); + } if (_contactStatus) { newScrollHeight -= _contactStatus->bar().height(); } @@ -7009,6 +7165,7 @@ void HistoryWidget::botCallbackSent(not_null item) { int HistoryWidget::computeMaxFieldHeight() const { const auto available = height() - _topBar->height() + - (_paysStatus ? _paysStatus->bar().height() : 0) - (_contactStatus ? _contactStatus->bar().height() : 0) - (_businessBotStatus ? _businessBotStatus->bar().height() : 0) - (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0) @@ -7371,10 +7528,21 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { return; } else if (showSlowmodeError()) { return; + } else if (const auto error = result.result->getErrorOnSend(_history)) { + Data::ShowSendErrorToast(controller(), _peer, error); + return; } - if (const auto error = result.result->getErrorOnSend(_history)) { - Data::ShowSendErrorToast(controller(), _peer, error); + const auto withPaymentApproved = [=](int approved) { + auto copy = result; + copy.options.starsApproved = approved; + sendInlineResult(copy); + }; + const auto checked = checkSendPayment( + 1, + result.options.starsApproved, + withPaymentApproved); + if (!checked) { return; } @@ -7385,7 +7553,7 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { action.generateLocal = true; session().api().sendInlineResult( result.bot, - result.result, + result.result.get(), action, result.messageSendingFrom.localId); @@ -8026,6 +8194,18 @@ bool HistoryWidget::sendExistingDocument( || ShowSendPremiumError(controller(), document)) { return false; } + const auto withPaymentApproved = [=](int approved) { + auto copy = messageToSend; + copy.action.options.starsApproved = approved; + sendExistingDocument(document, std::move(copy), localId); + }; + const auto checked = checkSendPayment( + 1, + messageToSend.action.options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } Api::SendExistingDocument( std::move(messageToSend), @@ -8063,6 +8243,19 @@ bool HistoryWidget::sendExistingPhoto( return false; } + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendExistingPhoto(photo, copy); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } + Api::SendExistingPhoto( Api::MessageToSend(prepareSendAction(options)), photo); @@ -8840,10 +9033,10 @@ void HistoryWidget::messageDataReceived( } void HistoryWidget::updateReplyEditText(not_null item) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &session(), - .customEmojiRepaint = [=] { updateField(); }, - }; + .repaint = [=] { updateField(); }, + }); _replyEditMsgText.setMarkedText( st::defaultTextStyle, ((_editMsgId || _replyTo.quote.empty()) @@ -8929,11 +9122,10 @@ void HistoryWidget::updateReplyToName() { } else if (!_replyEditMsg && (_replyTo || !_kbReplyTo)) { return; } - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_history->session(), - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo; const auto replyToQuote = _replyTo && !_replyTo.quote.empty(); _replyToName.setMarkedText( diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 9321f19c4..8a25d1833 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_media_edit_manager.h" #include "history/view/history_view_corner_buttons.h" #include "history/history_drag_area.h" +#include "history/history_item_helpers.h" #include "history/history_view_highlight_manager.h" #include "history/history_view_top_toast.h" #include "history/history.h" @@ -69,6 +70,7 @@ class PinnedBar; class GroupCallBar; class RequestsBar; struct PreparedList; +struct PreparedBundle; class SendFilesWay; class SendAsButton; class SpoilerAnimation; @@ -98,6 +100,7 @@ namespace HistoryView { class StickerToast; class PaidReactionToast; class TopBarWidget; +class PaysStatus; class ContactStatus; class BusinessBotStatus; class Element; @@ -115,6 +118,7 @@ class TTLButton; class WebpageProcessor; class CharactersLimitLabel; class PhotoEditSpoilerManager; +struct VoiceToSend; } // namespace HistoryView::Controls class BotKeyboard; @@ -319,6 +323,7 @@ protected: private: using TabbedPanel = ChatHelpers::TabbedPanel; using TabbedSelector = ChatHelpers::TabbedSelector; + using VoiceToSend = HistoryView::Controls::VoiceToSend; enum ScrollChangeType { ScrollChangeNone, @@ -357,6 +362,11 @@ private: bool cornerButtonsUnreadMayBeShown() override; bool cornerButtonsHas(HistoryView::CornerButtonType type) override; + [[nodiscard]] bool checkSendPayment( + int messagesCount, + int starsApproved, + Fn withPaymentApproved); + void checkSuggestToGigagroup(); void processReply(); void setReplyFieldsFromProcessing(); @@ -402,6 +412,7 @@ private: [[nodiscard]] Api::SendAction prepareSendAction( Api::SendOptions options) const; + void sendVoice(const VoiceToSend &data); void send(Api::SendOptions options); void sendWithModifiers(Qt::KeyboardModifiers modifiers); void sendScheduled(Api::SendOptions initialOptions); @@ -470,7 +481,9 @@ private: std::optional compress) const; bool showSendMessageError( const TextWithTags &textWithTags, - bool ignoreSlowmodeCountdown); + bool ignoreSlowmodeCountdown, + Fn withPaymentApproved = nullptr, + int starsApproved = 0); void sendingFilesConfirmed( Ui::PreparedList &&list, @@ -478,6 +491,13 @@ private: TextWithTags &&caption, Api::SendOptions options, bool ctrlShiftEnter); + void sendingFilesConfirmed( + std::shared_ptr bundle, + Api::SendOptions options); + + void sendBotCommand( + const Bot::SendCommandRequest &request, + Api::SendOptions options); void uploadFile(const QByteArray &fileContent, SendMediaType type); void itemRemoved(not_null item); @@ -771,6 +791,7 @@ private: Webrtc::RecordAvailability _recordAvailability = {}; + std::unique_ptr _paysStatus; std::unique_ptr _contactStatus; std::unique_ptr _businessBotStatus; @@ -868,6 +889,8 @@ private: int _topDelta = 0; + SendPaymentHelper _sendPayment; + rpl::event_stream<> _cancelRequests; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index ec7edd59c..c97e367a9 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -428,10 +428,10 @@ void FieldHeader::init() { void FieldHeader::updateShownMessageText() { Expects(_shownMessage != nullptr); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_data->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, - }; + .repaint = [=] { customEmojiRepaint(); }, + }); const auto reply = replyingToMessage(); _shownMessageText.setMarkedText( st::messageTextStyle, @@ -464,11 +464,10 @@ void FieldHeader::setShownMessage(HistoryItem *item) { tr::lng_edit_message(tr::now), Ui::NameTextOptions()); } else if (item) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_history->session(), - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); const auto replyTo = _replyTo.current(); const auto quote = replyTo && !replyTo.quote.empty(); _shownMessageName.setMarkedText( @@ -1714,6 +1713,9 @@ void ComposeControls::initFieldAutocomplete() { } void ComposeControls::updateFieldPlaceholder() { + _voiceRecordBar->setPauseInsteadSend(_history + && _history->peer->starsPerMessageChecked() > 0); + if (!isEditingMessage() && _isInlineBot) { _field->setPlaceholder( rpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)), @@ -1722,13 +1724,20 @@ void ComposeControls::updateFieldPlaceholder() { } _field->setPlaceholder([&] { + const auto peer = _history ? _history->peer.get() : nullptr; if (_fieldCustomPlaceholder) { return rpl::duplicate(_fieldCustomPlaceholder); } else if (isEditingMessage()) { return tr::lng_edit_message_text(); - } else if (!_history) { + } else if (!peer) { return tr::lng_message_ph(); - } else if (const auto channel = _history->peer->asChannel()) { + } else if (const auto stars = peer->starsPerMessageChecked()) { + return tr::lng_message_paid_ph( + lt_amount, + tr::lng_prize_credits_amount( + lt_count, + rpl::single(stars * 1.))); + } else if (const auto channel = peer->asChannel()) { if (channel->isBroadcast()) { return session().data().notifySettings().silentPosts(channel) ? tr::lng_broadcast_silent_ph() @@ -2157,6 +2166,10 @@ void ComposeControls::initSendButton() { _recordAvailability = value; updateSendButtonType(); }, _send->lifetime()); + + _send->widthValue() | rpl::skip(1) | rpl::start_with_next([=] { + updateControlsGeometry(_wrap->size()); + }, _send->lifetime()); } void ComposeControls::initSendAsButton(not_null peer) { @@ -2543,14 +2556,18 @@ SendMenu::Details ComposeControls::sendButtonMenuDetails() const { void ComposeControls::updateSendButtonType() { using Type = Ui::SendButton::Type; const auto type = computeSendButtonType(); - _send->setType(type); - const auto delay = [&] { return (type != Type::Cancel && type != Type::Save) ? _slowmodeSecondsLeft.current() : 0; }(); - _send->setSlowmodeDelay(delay); + const auto peer = _history ? _history->peer.get() : nullptr; + const auto stars = peer ? peer->starsPerMessageChecked() : 0; + _send->setState({ + .type = type, + .slowmodeDelay = delay, + .starsToSend = stars, + }); _send->setDisabled(_sendDisabledBySlowmode.current() && (type == Type::Send || type == Type::Record @@ -3120,6 +3137,7 @@ void ComposeControls::initWebpageProcess() { | Data::PeerUpdate::Flag::Notifications | Data::PeerUpdate::Flag::MessagesTTL | Data::PeerUpdate::Flag::FullInfo + | Data::PeerUpdate::Flag::StarsPerMessage ) | rpl::filter([peer = _history->peer](const Data::PeerUpdate &update) { return (update.peer.get() == peer); }) | rpl::map([](const Data::PeerUpdate &update) { @@ -3135,6 +3153,9 @@ void ComposeControls::initWebpageProcess() { if (flags & Data::PeerUpdate::Flag::MessagesTTL) { updateMessagesTTLShown(); } + if (flags & Data::PeerUpdate::Flag::StarsPerMessage) { + updateFieldPlaceholder(); + } if (flags & Data::PeerUpdate::Flag::FullInfo) { if (updateBotCommandShown()) { updateControlsVisibility(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 2d03f6dfa..549d25233 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -1199,12 +1199,15 @@ struct AuthorSelector { _peer->owner().history(_peer), &computeListSt().item)); delegate()->peerListRefreshRows(); - TrackPremiumRequiredChanges(this, _lifetime); + TrackMessageMoneyRestrictionsChanges(this, _lifetime); } void loadMoreRows() override { } void rowClicked(not_null row) override { - if (RecipientRow::ShowLockedError(this, row, WritePremiumRequiredError)) { + if (RecipientRow::ShowLockedError( + this, + row, + WriteMoneyRestrictionError)) { return; } else if (const auto onstack = _click) { onstack(); @@ -1298,7 +1301,7 @@ void ShowReplyToChatBox( .callback = [=](Chosen thread) { _singleChosen.fire_copy(thread); }, - .premiumRequiredError = WritePremiumRequiredError, + .moneyRestrictionError = WriteMoneyRestrictionError, }) { _authorRow = AuthorRowSelector( session, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index 022b124ab..7502a93c8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -190,10 +190,10 @@ void ForwardPanel::updateTexts() { } } _from.setText(st::msgNameStyle, from, Ui::NameTextOptions()); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_to->session(), - .customEmojiRepaint = _repaint, - }; + .repaint = _repaint, + }); _text.setMarkedText( st::defaultTextStyle, text, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 4ddc39203..677fee209 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -529,6 +529,7 @@ public: ListenWrap( not_null parent, const style::RecordBar &st, + std::shared_ptr send, not_null session, not_null data, const style::font &font); @@ -556,6 +557,7 @@ private: const not_null _parent; const style::RecordBar &_st; + const std::shared_ptr _send; const not_null _session; const not_null _document; const std::unique_ptr _voiceData; @@ -590,11 +592,13 @@ private: ListenWrap::ListenWrap( not_null parent, const style::RecordBar &st, + std::shared_ptr send, not_null session, not_null data, const style::font &font) : _parent(parent) , _st(st) +, _send(send) , _session(session) , _document(DummyDocument(&session->data())) , _voiceData(ProcessCaptureResult(data->waveform)) @@ -619,14 +623,18 @@ void ListenWrap::init() { }) | rpl::distinct_until_changed(); _delete->showOn(std::move(deleteShow)); - _parent->sizeValue( - ) | rpl::start_with_next([=](QSize size) { + rpl::combine( + _parent->sizeValue(), + _send->widthValue() + ) | rpl::start_with_next([=](QSize size, int send) { _waveformBgRect = QRect({ 0, 0 }, size) .marginsRemoved(st::historyRecordWaveformBgMargins); { - const auto m = _st.remove.width + _waveformBgRect.height() / 2; + const auto skip = _waveformBgRect.height() / 2; + const auto left = _st.remove.width + skip; + const auto right = send + skip; _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved( - style::margins(m, 0, m, 0)); + style::margins(left, 0, right, 0)); } { const auto &play = _playPauseSt.playOuter; @@ -656,7 +664,7 @@ void ListenWrap::init() { const auto deleteIconLeft = remove.iconPosition.x(); const auto bgRectRight = anim::interpolate( deleteIconLeft, - remove.width, + _send->width(), _isShowAnimation ? progress : 1.); const auto bgRectLeft = anim::interpolate( _parent->width() - deleteIconLeft - _waveformBgRect.height(), @@ -1765,6 +1773,10 @@ void VoiceRecordBar::setTTLFilter(FilterCallback &&callback) { _hasTTLFilter = std::move(callback); } +void VoiceRecordBar::setPauseInsteadSend(bool pauseInsteadSend) { + _pauseInsteadSend = pauseInsteadSend; +} + void VoiceRecordBar::initLockGeometry() { const auto parent = static_cast(parentWidget()); rpl::merge( @@ -1915,6 +1927,11 @@ void VoiceRecordBar::recordUpdated(quint16 level, int samples) { void VoiceRecordBar::stop(bool send) { if (isHidden() && !send) { return; + } else if (send && _pauseInsteadSend) { + _fullRecord = true; + stopRecording(StopType::Listen); + _lockShowing = false; + return; } const auto ttlBeforeHide = peekTTLState(); auto disappearanceCallback = [=] { @@ -1978,6 +1995,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _listen = std::make_unique( this, _st, + _send, &_show->session(), &_data, _cancelFont); @@ -2011,6 +2029,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _listen = std::make_unique( this, _st, + _send, &_show->session(), &_data, _cancelFont); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index 027366a59..c1a9ff4a7 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -100,6 +100,7 @@ public: void setStartRecordingFilter(FilterCallback &&callback); void setTTLFilter(FilterCallback &&callback); + void setPauseInsteadSend(bool pauseInsteadSend); [[nodiscard]] bool isRecording() const; [[nodiscard]] bool isRecordingLocked() const; @@ -193,6 +194,7 @@ private: FilterCallback _hasTTLFilter; bool _warningShown = false; + bool _pauseInsteadSend = false; rpl::variable _recording = false; rpl::variable _inField = false; diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 452f595ac..d2ad1b735 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -14,14 +14,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "chat_helpers/stickers_lottie.h" #include "core/click_handler_types.h" +#include "core/ui_integration.h" +#include "countries/countries_instance.h" #include "data/business/data_business_common.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" +#include "history/view/history_view_group_call_bar.h" #include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_service_box.h" #include "history/view/media/history_view_sticker_player_abstract.h" #include "history/view/media/history_view_sticker.h" +#include "history/view/media/history_view_unique_gift.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "history/history_item.h" @@ -30,21 +35,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_chat_intro.h" +#include "settings/settings_credits.h" // BuyStarsHandler #include "settings/settings_premium.h" #include "ui/chat/chat_style.h" +#include "ui/text/custom_emoji_instance.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" +#include "ui/dynamic_image.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" // GroupCallUserpics +#include "styles/style_credits.h" namespace HistoryView { namespace { -class PremiumRequiredBox final : public ServiceBoxContent { +constexpr auto kLabelOpacity = 0.85; +constexpr auto kMaxCommonChatsUserpics = 3; + +class EmptyChatLockedBox final + : public ServiceBoxContent + , public base::has_weak_ptr { public: - explicit PremiumRequiredBox(not_null parent); - ~PremiumRequiredBox(); + enum class Type { + PremiumRequired, + StarsCharged, + }; + + EmptyChatLockedBox(not_null parent, Type type); + ~EmptyChatLockedBox(); int width() override; int top() override; @@ -74,14 +94,113 @@ public: private: const not_null _parent; + Settings::BuyStarsHandler _buyStars; + rpl::variable _buyStarsLoading; + Type _type = {}; }; +class UserpicsList final : public Ui::DynamicImage { +public: + UserpicsList( + std::vector> peers, + const style::GroupCallUserpics &st, + int countOverride = 0); + + [[nodiscard]] int width() const; + + std::shared_ptr clone() override; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + struct Subscribed { + explicit Subscribed(Fn callback) + : callback(std::move(callback)) { + } + + std::vector list; + bool someNotLoaded = false; + Fn callback; + int paletteVersion = 0; + }; + + const std::vector> _peers; + const style::GroupCallUserpics &_st; + const int _countOverride = 0; + + QImage _frame; + std::unique_ptr _subscribed; + +}; + +UserpicsList::UserpicsList( + std::vector> peers, + const style::GroupCallUserpics &st, + int countOverride) +: _peers(std::move(peers)) +, _st(st) +, _countOverride(countOverride) { +} + +std::shared_ptr UserpicsList::clone() { + return std::make_shared(_peers, _st); +} + +QImage UserpicsList::image(int size) { + Expects(_subscribed != nullptr); + + const auto regenerate = [&] { + const auto version = style::PaletteVersion(); + if (_frame.isNull() || _subscribed->paletteVersion != version) { + _subscribed->paletteVersion = version; + return true; + } + for (auto &entry : _subscribed->list) { + const auto peer = entry.peer; + auto &view = entry.view; + const auto wasView = view.cloud.get(); + if (peer->userpicUniqueKey(view) != entry.uniqueKey + || view.cloud.get() != wasView) { + return true; + } + } + return false; + }(); + if (regenerate) { + const auto max = std::max(_countOverride, int(_peers.size())); + GenerateUserpicsInRow(_frame, _subscribed->list, _st, max); + } + return _frame; +} + +void UserpicsList::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + _subscribed = std::make_unique(std::move(callback)); + for (const auto peer : _peers) { + _subscribed->list.push_back({ .peer = peer }); + } +} + +int UserpicsList::width() const { + const auto count = std::max(_countOverride, int(_peers.size())); + if (!count) { + return 0; + } + const auto shifted = count - 1; + return _st.size + (shifted * (_st.size - _st.shift)); +} + auto GenerateChatIntro( not_null parent, Element *replacing, const Data::ChatIntro &data, - Fn)> helloChosen) + Fn)> helloChosen, + Fn)> sendIntroSticker) -> Fn, Fn)>)> { @@ -125,9 +244,7 @@ auto GenerateChatIntro( } } const auto send = [=] { - Api::SendExistingDocument(Api::MessageToSend( - Api::SendAction(parent->history()) - ), sticker); + sendIntroSticker(sticker); }; return StickerInBubblePart::Data{ .sticker = sticker, @@ -144,54 +261,195 @@ auto GenerateChatIntro( }; } -PremiumRequiredBox::PremiumRequiredBox(not_null parent) -: _parent(parent) { +auto GenerateNewPeerInfo( + not_null parent, + Element *replacing, + not_null user, + std::vector> commonGroups) +-> Fn, + Fn)>)> { + return [=]( + not_null media, + Fn)> push) { + const auto normalFg = [](const PaintContext &context) { + return context.st->msgServiceFg()->c; + }; + const auto fadedFg = [](const PaintContext &context) { + auto result = context.st->msgServiceFg()->c; + result.setAlphaF(result.alphaF() * kLabelOpacity); + return result; + }; + push(std::make_unique( + Ui::Text::Bold(user->name()), + st::newPeerTitleMargin)); + push(std::make_unique( + tr::lng_new_contact_not_contact(tr::now, Ui::Text::WithEntities), + st::newPeerSubtitleMargin, + fadedFg)); + + auto entries = std::vector(); + const auto country = user->phoneCountryCode(); + if (!country.isEmpty()) { + const auto &countries = Countries::Instance(); + const auto name = countries.countryNameByISO2(country); + const auto flag = countries.flagEmojiByISO2(country); + entries.push_back({ + tr::lng_new_contact_phone_number(tr::now), + Ui::Text::Bold(flag + QChar(0xA0) + name), + }); + } + const auto month = user->registrationMonth(); + const auto year = user->registrationYear(); + if (month && year) { + entries.push_back({ + tr::lng_new_contact_registration(tr::now), + Ui::Text::Bold(langMonthOfYearFull(month, year)), + }); + } + + const auto context = Core::TextContext({ + .session = &parent->history()->session(), + .repaint = [parent] { parent->repaint(); }, + }); + const auto kUserpicsPrefix = u"userpics-list/"_q; + if (const auto count = user->commonChatsCount()) { + const auto url = u"internal:common_groups/"_q + + QString::number(user->id.value); + auto ids = QStringList(); + const auto userpics = std::min(count, kMaxCommonChatsUserpics); + for (auto i = 0; i != userpics; ++i) { + ids.push_back(QString::number(i < commonGroups.size() + ? commonGroups[i]->id.value + : 0)); + } + auto userpicsData = kUserpicsPrefix + ids.join(','); + entries.push_back({ + tr::lng_new_contact_common_groups(tr::now), + Ui::Text::Wrapped( + tr::lng_new_contact_groups( + tr::now, + lt_count, + count, + lt_emoji, + Ui::Text::SingleCustomEmoji(userpicsData), + lt_arrow, + Ui::Text::IconEmoji(&st::textMoreIconEmoji), + Ui::Text::Bold), + EntityType::CustomUrl, + url), + }); + } + + auto copy = context; + copy.customEmojiFactory = [=, old = copy.customEmojiFactory]( + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr { + if (!data.startsWith(kUserpicsPrefix)) { + return old(data, context); + } + const auto ids = data.mid(kUserpicsPrefix.size()).split(','); + auto peers = std::vector>(); + for (const auto &id : ids) { + if (const auto peerId = PeerId(id.toULongLong())) { + peers.push_back(user->owner().peer(peerId)); + } + } + auto image = std::make_shared( + std::move(peers), + st::newPeerUserpics, + ids.size()); + const auto size = image->width(); + return std::make_unique( + data.toString(), + std::move(image), + context.repaint, + st::newPeerUserpicsPadding, + size); + }; + push(std::make_unique( + std::move(entries), + st::newPeerSubtitleMargin, + fadedFg, + normalFg, + copy)); + + const auto details = user->botVerifyDetails(); + const auto text = details + ? Data::SingleCustomEmoji( + details->iconId + ).append(' ').append(details->description) + : Ui::Text::IconEmoji( + &st::newPeerNonOfficial + ).append(' ').append(tr::lng_new_contact_not_official(tr::now)); + push(std::make_unique( + text, + st::newPeerSubtitleMargin, + fadedFg, + st::defaultTextStyle, + base::flat_map(), + context)); + }; } -PremiumRequiredBox::~PremiumRequiredBox() = default; +EmptyChatLockedBox::EmptyChatLockedBox(not_null parent, Type type) +: _parent(parent) +, _type(type) { +} -int PremiumRequiredBox::width() { +EmptyChatLockedBox::~EmptyChatLockedBox() = default; + +int EmptyChatLockedBox::width() { return st::premiumRequiredWidth; } -int PremiumRequiredBox::top() { +int EmptyChatLockedBox::top() { return st::msgServiceGiftBoxButtonMargins.top(); } -QSize PremiumRequiredBox::size() { +QSize EmptyChatLockedBox::size() { return { st::msgServicePhotoWidth, st::msgServicePhotoWidth }; } -TextWithEntities PremiumRequiredBox::title() { +TextWithEntities EmptyChatLockedBox::title() { return {}; } -int PremiumRequiredBox::buttonSkip() { +int EmptyChatLockedBox::buttonSkip() { return st::storyMentionButtonSkip; } -rpl::producer PremiumRequiredBox::button() { - return tr::lng_send_non_premium_go(); +rpl::producer EmptyChatLockedBox::button() { + return (_type == Type::PremiumRequired) + ? tr::lng_send_non_premium_go() + : tr::lng_send_charges_stars_go(); } -bool PremiumRequiredBox::buttonMinistars() { +bool EmptyChatLockedBox::buttonMinistars() { return true; } -TextWithEntities PremiumRequiredBox::subtitle() { +TextWithEntities EmptyChatLockedBox::subtitle() { return _parent->data()->notificationText(); } -ClickHandlerPtr PremiumRequiredBox::createViewLink() { - return std::make_shared([=](ClickContext context) { +ClickHandlerPtr EmptyChatLockedBox::createViewLink() { + _buyStarsLoading = _buyStars.loadingValue(); + const auto handler = [=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - Settings::ShowPremium(controller, u"require_premium"_q); + if (_type == Type::PremiumRequired) { + Settings::ShowPremium(controller, u"require_premium"_q); + } else if (!_buyStarsLoading.current()) { + _buyStars.handler(controller->uiShow())(); + } } - }); + }; + return std::make_shared(crl::guard(this, handler)); } -void PremiumRequiredBox::draw( +void EmptyChatLockedBox::draw( Painter &p, const PaintContext &context, const QRect &geometry) { @@ -201,20 +459,20 @@ void PremiumRequiredBox::draw( st::premiumRequiredIcon.paintInCenter(p, geometry); } -void PremiumRequiredBox::stickerClearLoopPlayed() { +void EmptyChatLockedBox::stickerClearLoopPlayed() { } -std::unique_ptr PremiumRequiredBox::stickerTakePlayer( +std::unique_ptr EmptyChatLockedBox::stickerTakePlayer( not_null data, const Lottie::ColorReplacements *replacements) { return nullptr; } -bool PremiumRequiredBox::hasHeavyPart() { +bool EmptyChatLockedBox::hasHeavyPart() { return false; } -void PremiumRequiredBox::unloadHeavyPart() { +void EmptyChatLockedBox::unloadHeavyPart() { } } // namespace @@ -256,14 +514,27 @@ bool AboutView::refresh() { const auto user = _history->peer->asUser(); const auto info = user ? user->botInfo.get() : nullptr; if (!info) { - if (user && !user->isSelf() && _history->isDisplayedEmpty()) { + if (user + && !user->isContact() + && !user->phoneCountryCode().isEmpty()) { + if (_item && !_commonGroupsStale) { + return false; + } + loadCommonGroups(); + setItem(makeNewPeerInfo(user), nullptr); + return true; + } else if (user && !user->isSelf() && _history->isDisplayedEmpty()) { if (_item) { return false; - } else if (user->meRequiresPremiumToWrite() + } else if (user->requiresPremiumToWrite() && !user->session().premium()) { setItem(makePremiumRequired(), nullptr); } else if (user->isBlocked()) { setItem(makeBlocked(), nullptr); + } else if (user->businessDetails().intro) { + makeIntro(user); + } else if (const auto stars = user->starsPerMessageChecked()) { + setItem(makeStarsPerMessage(stars), nullptr); } else { makeIntro(user); } @@ -326,9 +597,17 @@ void AboutView::make(Data::ChatIntro data, bool preview) { } } }; + const auto sendIntroSticker = [=](not_null sticker) { + _sendIntroSticker.fire_copy(sticker); + }; owned->overrideMedia(std::make_unique( owned.get(), - GenerateChatIntro(owned.get(), _item.get(), data, helloChosen), + GenerateChatIntro( + owned.get(), + _item.get(), + data, + helloChosen, + sendIntroSticker), HistoryView::MediaGenericDescriptor{ .maxWidth = st::chatIntroWidth, .serviceLink = std::make_shared(handler), @@ -341,6 +620,18 @@ void AboutView::make(Data::ChatIntro data, bool preview) { setItem(std::move(owned), data.sticker); } +rpl::producer> AboutView::sendIntroSticker() const { + return _sendIntroSticker.events(); +} + +rpl::producer<> AboutView::refreshRequests() const { + return _refreshRequests.events(); +} + +rpl::lifetime &AboutView::lifetime() { + return _lifetime; +} + void AboutView::toggleStickerRegistered(bool registered) { if (const auto item = _item ? _item->data().get() : nullptr) { if (_sticker) { @@ -357,6 +648,75 @@ void AboutView::toggleStickerRegistered(bool registered) { } } +void AboutView::loadCommonGroups() { + if (_commonGroupsRequested) { + return; + } + _commonGroupsRequested = true; + + const auto user = _history->peer->asUser(); + if (!user) { + return; + } + + struct Cached { + std::vector> list; + }; + struct Session { + base::flat_map, Cached> data; + }; + static auto Map = base::flat_map, Session>(); + const auto session = &_history->session(); + auto i = Map.find(session); + if (i == end(Map)) { + i = Map.emplace(session).first; + session->lifetime().add([session] { + Map.remove(session); + }); + } + auto &cached = i->second.data[user]; + + const auto count = user->commonChatsCount(); + if (!count) { + cached = {}; + return; + } else while (cached.list.size() > count) { + cached.list.pop_back(); + } + _commonGroups = cached.list; + const auto requestId = _history->session().api().request( + MTPmessages_GetCommonChats( + user->inputUser, + MTP_long(0), + MTP_int(kMaxCommonChatsUserpics)) + ).done([=](const MTPmessages_Chats &result) { + const auto chats = result.match([](const auto &data) { + return &data.vchats().v; + }); + auto &owner = user->session().data(); + auto list = std::vector>(); + list.reserve(chats->size()); + for (const auto &chat : *chats) { + if (const auto peer = owner.processChat(chat)) { + list.push_back(peer); + if (list.size() == kMaxCommonChatsUserpics) { + break; + } + } + } + if (_commonGroups != list) { + Map[session].data[user].list = list; + _commonGroups = std::move(list); + _commonGroupsStale = true; + _refreshRequests.fire({}); + } + }).send(); + + _lifetime.add([=] { + _history->session().api().request(requestId).cancel(); + }); +} + void AboutView::setHelloChosen(not_null sticker) { _helloChosen = sticker; toggleStickerRegistered(false); @@ -371,6 +731,29 @@ void AboutView::setItem(AdminLog::OwnedItem item, DocumentData *sticker) { toggleStickerRegistered(true); } +AdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null user) { + _commonGroupsStale = false; + + const auto text = user->name(); + const auto item = _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, PreparedServiceText{ { text }}); + + auto owned = AdminLog::OwnedItem(_delegate, item); + owned->overrideMedia(std::make_unique( + owned.get(), + GenerateNewPeerInfo(owned.get(), _item.get(), user, _commonGroups), + HistoryView::MediaGenericDescriptor{ + .service = true, + .hideServiceText = true, + })); + return owned; +} + AdminLog::OwnedItem AboutView::makeAboutVerifyCodes() { return makeAboutSimple( tr::lng_verification_codes_about(tr::now, Ui::Text::RichLangValue)); @@ -422,7 +805,35 @@ AdminLog::OwnedItem AboutView::makePremiumRequired() { auto result = AdminLog::OwnedItem(_delegate, item); result->overrideMedia(std::make_unique( result.get(), - std::make_unique(result.get()))); + std::make_unique( + result.get(), + EmptyChatLockedBox::Type::PremiumRequired))); + return result; +} + +AdminLog::OwnedItem AboutView::makeStarsPerMessage(int stars) { + const auto item = _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, PreparedServiceText{ tr::lng_send_charges_stars_text( + tr::now, + lt_user, + Ui::Text::Bold(_history->peer->shortName()), + lt_amount, + Ui::Text::IconEmoji( + &st::starIconEmoji + ).append(Ui::Text::Bold(Lang::FormatCountDecimal(stars))), + Ui::Text::RichLangValue), + }); + auto result = AdminLog::OwnedItem(_delegate, item); + result->overrideMedia(std::make_unique( + result.get(), + std::make_unique( + result.get(), + EmptyChatLockedBox::Type::StarsCharged))); return result; } diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h index 3e78c5c62..b2c8d3cbb 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.h +++ b/Telegram/SourceFiles/history/view/history_view_about_view.h @@ -30,6 +30,11 @@ public: void make(Data::ChatIntro data, bool preview = false); + [[nodiscard]] auto sendIntroSticker() const + -> rpl::producer>; + [[nodiscard]] rpl::producer<> refreshRequests() const; + [[nodiscard]] rpl::lifetime &lifetime(); + int top = 0; int height = 0; @@ -41,19 +46,33 @@ private: DocumentData *document = nullptr, PhotoData *photo = nullptr); [[nodiscard]] AdminLog::OwnedItem makePremiumRequired(); + [[nodiscard]] AdminLog::OwnedItem makeStarsPerMessage(int stars); + [[nodiscard]] AdminLog::OwnedItem makeNewPeerInfo( + not_null user); [[nodiscard]] AdminLog::OwnedItem makeBlocked(); void makeIntro(not_null user); void setItem(AdminLog::OwnedItem item, DocumentData *sticker); void setHelloChosen(not_null sticker); void toggleStickerRegistered(bool registered); + void loadCommonGroups(); + const not_null _history; const not_null _delegate; AdminLog::OwnedItem _item; + DocumentData *_helloChosen = nullptr; DocumentData *_sticker = nullptr; int _version = 0; + rpl::event_stream> _sendIntroSticker; + + bool _commonGroupsStale = false; + bool _commonGroupsRequested = false; + std::vector> _commonGroups; + rpl::event_stream<> _refreshRequests; + rpl::lifetime _lifetime; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index ce15289b6..2fc9d92bd 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" +#include "history/view/media/history_view_media.h" #include "history/view/history_view_message.h" #include "history/view/history_view_cursor_state.h" #include "chat_helpers/emoji_interactions.h" @@ -28,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_message_reactions.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_credits.h" #include "styles/style_dialogs.h" // AyuGram includes @@ -123,7 +125,7 @@ TextState BottomInfo::textState( } const auto textWidth = _authorEditedDate.maxWidth(); auto withTicksWidth = textWidth; - if (!AyuFeatures::MessageShot::isTakingShot() && _data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) { + if (!AyuFeatures::MessageShot::isTakingShot() && (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending))) { withTicksWidth += st::historySendStateSpace; } if (!_views.isEmpty()) { @@ -226,7 +228,7 @@ void BottomInfo::paint( auto right = position.x() + width(); const auto firstLineBottom = position.y() + st::msgDateFont->height; - if (!AyuFeatures::MessageShot::isTakingShot() && _data.flags & Data::Flag::OutLayout) { + if (!AyuFeatures::MessageShot::isTakingShot() && (_data.flags & Data::Flag::OutLayout)) { const auto &icon = (_data.flags & Data::Flag::Sending) ? (inverted ? st->historySendingInvertedIcon() @@ -447,9 +449,14 @@ void BottomInfo::layoutDateText() { : name.isEmpty() ? (deleted + date) : (deleted + name + afterAuthor); - _authorEditedDate.setText( + auto marked = TextWithEntities{ full }; + if (const auto count = _data.stars) { + marked.append(Ui::Text::IconEmoji(&st::starIconEmoji)); + marked.append(Lang::FormatCountToShort(count).string); + } + _authorEditedDate.setMarkedText( st::msgDateTextStyle, - full, + marked, Ui::NameTextOptions()); } else { TextWithEntities deleted; @@ -564,7 +571,7 @@ QSize BottomInfo::countOptimalSize() { return { st::historyShortcutStateSpace, st::msgDateFont->height }; } auto width = 0; - if (!AyuFeatures::MessageShot::isTakingShot() && _data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) { + if (!AyuFeatures::MessageShot::isTakingShot() && (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending))) { width += st::historySendStateSpace; } width += _authorEditedDate.maxWidth(); @@ -687,6 +694,17 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { if (item->isSending() || item->hasFailed()) { result.flags |= Flag::Sending; } + if (item->out() && !item->history()->peer->isUser()) { + const auto media = message->media(); + const auto mine = PaidInformation{ + .messages = 1, + .stars = item->starsPaid(), + }; + auto info = media ? media->paidInformation().value_or(mine) : mine; + if (const auto total = info.stars) { + result.stars = total; + } + } const auto forwarded = item->Get(); if (forwarded && forwarded->imported) { result.flags |= Flag::Imported; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index f7ea3e089..526275579 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -51,6 +51,7 @@ public: QDateTime date; QString author; EffectId effectId = 0; + int stars = 0; std::optional views; std::optional replies; std::optional forwardsCount; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index a25ebeed0..71e711ad0 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/boxes/confirm_box.h" #include "ui/layers/generic_box.h" +#include "chat_helpers/message_field.h" // PaidSendButtonText #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/business/data_business_chatbots.h" @@ -162,7 +163,7 @@ public: void showState( State state, TextWithEntities status, - Fn customEmojiRepaint)> context); + Ui::Text::MarkedContext context); [[nodiscard]] rpl::producer<> unarchiveClicks() const; [[nodiscard]] rpl::producer<> addClicks() const; @@ -271,7 +272,7 @@ ContactStatus::Bar::Bar( void ContactStatus::Bar::showState( State state, TextWithEntities status, - Fn customEmojiRepaint)> context) { + Ui::Text::MarkedContext context) { using Type = State::Type; const auto type = state.type; _add->setVisible(type == Type::AddOrBlock || type == Type::Add); @@ -293,6 +294,7 @@ void ContactStatus::Bar::showState( _emojiStatusShadow->setVisible( has && (type == Type::AddOrBlock || type == Type::UnarchiveOrBlock)); if (has) { + context.repaint = [=] { emojiStatusRepaint(); }; _emojiStatusInfo->entity()->setMarkedText( tr::lng_new_contact_about_status( tr::now, @@ -302,7 +304,7 @@ void ContactStatus::Bar::showState( Ui::Text::Link( tr::lng_new_contact_about_status_link(tr::now)), Ui::Text::WithEntities), - context([=] { emojiStatusRepaint(); })); + context); _emojiStatusInfo->entity()->overrideLinkClickHandler([=] { _emojiStatusClicks.fire({}); }); @@ -595,9 +597,9 @@ auto ContactStatus::PeerState(not_null peer) return { .type = Type::RequestChatInfo, .requestChatName = peer->requestChatTitle(), + .requestDate = peer->requestChatDate(), .requestChatIsBroadcast = !!(settings.value & PeerBarSetting::RequestChatIsBroadcast), - .requestDate = peer->requestChatDate(), }; } else if (settings.value & PeerBarSetting::AutoArchived) { return { Type::UnarchiveOrBlock }; @@ -627,12 +629,7 @@ void ContactStatus::setupState(not_null peer, bool showInForum) { peer->session().api().requestPeerSettings(peer); } - _context = [=](Fn customEmojiRepaint) { - return Core::MarkedTextContext{ - .session = &peer->session(), - .customEmojiRepaint = customEmojiRepaint, - }; - }; + _context = Core::TextContext({ .session = &peer->session() }); _inner->showState({}, {}, _context); const auto channel = peer->asChannel(); rpl::combine( @@ -1131,4 +1128,165 @@ void TopicReopenBar::setupHandler() { }); } +class PaysStatus::Bar final : public Ui::RpWidget { +public: + Bar(QWidget *parent, not_null peer); + + void showState(State state); + + [[nodiscard]] rpl::producer<> removeClicks() const; + +private: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + + not_null _peer; + object_ptr _label; + object_ptr _remove; + rpl::event_stream<> _removeClicks; + +}; + +PaysStatus::Bar::Bar(QWidget *parent, not_null peer) +: RpWidget(parent) +, _peer(peer) +, _label(this, st::paysStatusLabel) +, _remove(this, tr::lng_payment_bar_button(tr::now)) { + _label->setAttribute(Qt::WA_TransparentForMouseEvents); +} + +void PaysStatus::Bar::showState(State state) { + _label->setMarkedText(tr::lng_payment_bar_text( + tr::now, + lt_name, + TextWithEntities{ _peer->shortName() }, + lt_cost, + PaidSendButtonText(tr::now, state.perMessage), + Ui::Text::WithEntities)); + resizeToWidth(width()); +} + +rpl::producer<> PaysStatus::Bar::removeClicks() const { + return _remove->clicks() | rpl::to_empty; +} + +void PaysStatus::Bar::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(e->rect(), st::historyContactStatusButton.bgColor); +} + +int PaysStatus::Bar::resizeGetHeight(int newWidth) { + const auto skip = st::defaultPeerListItem.photoPosition.y(); + _label->resizeToWidth(newWidth - skip); + _label->moveToLeft(skip, skip, newWidth); + _remove->move( + (newWidth - _remove->width()) / 2, + skip + _label->height() + skip); + return _remove->y() + _remove->height() + skip; +} + +PaysStatus::PaysStatus( + not_null window, + not_null parent, + not_null user) +: _controller(window) +, _user(user) +, _inner(Ui::CreateChild(parent.get(), user)) +, _bar(parent, object_ptr::fromRaw(_inner)) { + setupState(); + setupHandlers(); +} + +void PaysStatus::setupState() { + _user->session().api().requestPeerSettings(_user); + + _user->session().changes().peerFlagsValue( + _user, + Data::PeerUpdate::Flag::PaysPerMessage + ) | rpl::start_with_next([=] { + _state = State{ _user->paysPerMessage() }; + if (_state.perMessage > 0) { + _inner->showState(_state); + _bar.toggleContent(true); + } else { + _bar.toggleContent(false); + } + }, _bar.lifetime()); +} + +void PaysStatus::setupHandlers() { + _inner->removeClicks( + ) | rpl::start_with_next([=] { + const auto user = _user; + const auto exception = [=](bool refund) { + using Flag = MTPaccount_AddNoPaidMessagesException::Flag; + const auto api = &user->session().api(); + api->request(MTPaccount_AddNoPaidMessagesException( + MTP_flags(refund ? Flag::f_refund_charged : Flag()), + user->inputUser + )).done([=] { + user->clearPaysPerMessage(); + }).send(); + }; + _controller->show(Box([=](not_null box) { + const auto refund = std::make_shared>(); + Ui::ConfirmBox(box, { + .text = tr::lng_payment_refund_text( + tr::now, + lt_name, + Ui::Text::Bold(user->shortName()), + Ui::Text::WithEntities), + .confirmed = [=](Fn close) { + exception(*refund && (*refund)->checked()); + close(); + }, + .confirmText = tr::lng_payment_refund_confirm(tr::now), + .title = tr::lng_payment_refund_title(tr::now), + }); + const auto paid = box->lifetime().make_state< + rpl::variable + >(); + *paid = _paidAlready.value(); + paid->value() | rpl::start_with_next([=](int already) { + if (!already) { + delete base::take(*refund); + } else if (!*refund) { + const auto skip = st::defaultCheckbox.margin.top(); + *refund = box->addRow( + object_ptr( + box, + tr::lng_payment_refund_also( + lt_count, + paid->value() | tr::to_count()), + false, + st::defaultCheckbox), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + } + }, box->lifetime()); + + user->session().api().request(MTPaccount_GetPaidMessagesRevenue( + user->inputUser + )).done(crl::guard(_inner, [=]( + const MTPaccount_PaidMessagesRevenue &result) { + _paidAlready = result.data().vstars_amount().v; + })).send(); + })); + }, _bar.lifetime()); +} + +void PaysStatus::show() { + if (!_shown) { + _shown = true; + if (_state.perMessage > 0) { + _inner->showState(_state); + _bar.toggleContent(true); + } + } + _bar.show(); +} + +void PaysStatus::hide() { + _bar.hide(); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.h b/Telegram/SourceFiles/history/view/history_view_contact_status.h index aafc23a6a..0c540a36c 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.h +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.h @@ -95,9 +95,10 @@ private: RequestChatInfo, }; Type type = Type::None; + int starsPerMessage = 0; QString requestChatName; - bool requestChatIsBroadcast = false; TimeId requestDate = 0; + bool requestChatIsBroadcast = false; }; void setupState(not_null peer, bool showInForum); @@ -116,7 +117,7 @@ private: const not_null _controller; State _state; TextWithEntities _status; - Fn customEmojiRepaint)> _context; + Ui::Text::MarkedContext _context; QPointer _inner; SlidingBar _bar; bool _hiddenByForum = false; @@ -181,4 +182,38 @@ private: }; +class PaysStatus final { +public: + PaysStatus( + not_null controller, + not_null parent, + not_null user); + + void show(); + void hide(); + + [[nodiscard]] SlidingBar &bar() { + return _bar; + } + +private: + class Bar; + + struct State { + int perMessage = 0; + }; + + void setupState(); + void setupHandlers(); + + const not_null _controller; + const not_null _user; + rpl::variable _paidAlready; + State _state; + QPointer _inner; + SlidingBar _bar; + bool _shown = false; + +}; + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index bd446022a..b682ffe1c 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -590,6 +590,8 @@ bool AddRescheduleAction( const auto peer = firstItem->history()->peer; const auto sendMenuType = !peer ? SendMenu::Type::Disabled + : peer->starsPerMessageChecked() + ? SendMenu::Type::SilentOnly : peer->isSelf() ? SendMenu::Type::Reminder : HistoryView::CanScheduleUntilOnline(peer) diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 4a4405465..dcfb00b50 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -391,6 +391,9 @@ QString DateTooltipText(not_null view) { if (item->isScheduled() && item->isSilent()) { dateText += '\n' + QChar(0xD83D) + QChar(0xDD15); } + if (const auto stars = item->out() ? item->starsPaid() : 0) { + dateText += '\n' + tr::lng_you_paid_stars(tr::now, lt_count, stars); + } if (!item->isLocal()) { dateText += '\n'; dateText += "ID: "; @@ -488,12 +491,15 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } -void ServicePreMessage::init(TextWithEntities string) { +void ServicePreMessage::init(PreparedServiceText string) { text = Ui::Text::String( st::serviceTextStyle, - string, + string.text, kMarkupTextOptions, st::msgMinWidth); + for (auto i = 0; i != int(string.links.size()); ++i) { + text.setLink(i + 1, string.links[i]); + } } int ServicePreMessage::resizeToWidth(int newWidth, bool chatWide) { @@ -563,6 +569,27 @@ void ServicePreMessage::paint( p.translate(0, -top); } +ClickHandlerPtr ServicePreMessage::textState( + QPoint point, + const StateRequest &request, + QRect g) const { + const auto top = g.top() - height - st::msgMargin.top(); + const auto rect = QRect(0, top, width, height) + - st::msgServiceMargin; + const auto trect = rect - st::msgServicePadding; + if (trect.contains(point)) { + auto textRequest = request.forText(); + textRequest.align = style::al_center; + return text.getState( + point - trect.topLeft(), + trect.width(), + textRequest).link; + } + return {}; +} + + + void FakeBotAboutTop::init() { if (!text.isEmpty()) { return; @@ -1164,10 +1191,10 @@ void Element::validateText() { void Element::setTextWithLinks( const TextWithEntities &text, const std::vector &links) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history()->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, - }; + .repaint = [=] { customEmojiRepaint(); }, + }); if (_flags & Flag::ServiceMessage) { const auto &options = Ui::ItemTextServiceOptions(); _text.setMarkedText(st::serviceTextStyle, text, options, context); @@ -1424,6 +1451,9 @@ bool Element::countIsTopicRootReply() const { void Element::setDisplayDate(bool displayDate) { const auto item = data(); + if (item->hideDisplayDate()) { + displayDate = false; + } if (displayDate && !Has()) { AddComponents(DateBadge::Bit()); Get()->init( @@ -1435,8 +1465,8 @@ void Element::setDisplayDate(bool displayDate) { } } -void Element::setServicePreMessage(TextWithEntities text) { - if (!text.empty()) { +void Element::setServicePreMessage(PreparedServiceText text) { + if (!text.text.empty()) { AddComponents(ServicePreMessage::Bit()); const auto service = Get(); service->init(std::move(text)); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index abb1b3ada..de00c0f48 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -16,6 +16,7 @@ class History; class HistoryBlock; class HistoryItem; struct HistoryMessageReply; +struct PreparedServiceText; namespace Data { struct Reaction; @@ -260,7 +261,7 @@ struct DateBadge : public RuntimeComponent { // displaying some text in layout of a service message above the message. struct ServicePreMessage : public RuntimeComponent { - void init(TextWithEntities string); + void init(PreparedServiceText string); int resizeToWidth(int newWidth, bool chatWide); @@ -269,6 +270,10 @@ struct ServicePreMessage const PaintContext &context, QRect g, bool chatWide) const; + [[nodiscard]] ClickHandlerPtr textState( + QPoint point, + const StateRequest &request, + QRect g) const; Ui::Text::String text; int width = 0; @@ -403,7 +408,7 @@ public: // For blocks context this should be called only from recountDisplayDate(). void setDisplayDate(bool displayDate); - void setServicePreMessage(TextWithEntities text); + void setServicePreMessage(PreparedServiceText text); bool computeIsAttachToPrevious(not_null previous); diff --git a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp index ef7b2674f..49a310aa2 100644 --- a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp +++ b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp @@ -61,7 +61,8 @@ PeerId GenerateUser(not_null history, const QString &name) { MTPPeerColor(), // color MTPPeerColor(), // profile_color MTPint(), // bot_active_users - MTPlong())); // bot_verification_icon + MTPlong(), // bot_verification_icon + MTPlong())); // send_paid_messages_stars return peerId; } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 1e3c0fe70..18740f501 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2964,6 +2964,7 @@ void ListWidget::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + _mouseActive = true; mouseActionStart(e->globalPos(), e->button()); } @@ -3171,6 +3172,7 @@ void ListWidget::mouseMoveEvent(QMouseEvent *e) { mouseReleaseEvent(e); } if (reallyMoved) { + _mouseActive = true; lastGlobalPosition = e->globalPos(); if (!buttonsPressed || (_scrollDateLink @@ -3201,6 +3203,7 @@ rpl::producer ListWidget::touchMaybeSelectingValue() const { } void ListWidget::enterEventHook(QEnterEvent *e) { + _mouseActive = true; mouseActionUpdate(QCursor::pos()); return TWidget::enterEventHook(e); } @@ -3221,6 +3224,7 @@ void ListWidget::leaveEventHook(QEvent *e) { _cursor = style::cur_default; setCursor(_cursor); } + _mouseActive = false; return TWidget::leaveEventHook(e); } @@ -3653,6 +3657,10 @@ int ListWidget::SelectionViewOffset( void ListWidget::mouseActionUpdate() { + if (!_mouseActive && !window()->isActiveWindow()) { + return; + } + auto mousePosition = mapFromGlobal(_mousePosition); auto point = QPoint( std::clamp(mousePosition.x(), 0, width()), diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index a21cea06e..71db29cd5 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -816,6 +816,7 @@ private: uint16 _mouseTextSymbol = 0; bool _pressWasInactive = false; bool _overSenderUserpic = false; + bool _mouseActive = false; bool _selectEnabled = false; HistoryItem *_selectedTextItem = nullptr; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index ebd36aef6..b6683b8e3 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -436,6 +436,7 @@ Message::Message( _rightAction->second->link = ReportSponsoredClickHandler(data); } } + initPaidInformation(); } Message::~Message() { @@ -446,6 +447,54 @@ Message::~Message() { } } +void Message::initPaidInformation() { + const auto item = data(); + if (!item->history()->peer->isUser()) { + return; + } + const auto media = this->media(); + const auto mine = PaidInformation{ + .messages = 1, + .stars = item->starsPaid(), + }; + auto info = media ? media->paidInformation().value_or(mine) : mine; + if (!info) { + return; + } + const auto action = [&] { + return (info.messages == 1) + ? tr::lng_action_paid_message_one( + tr::now, + Ui::Text::WithEntities) + : tr::lng_action_paid_message_some( + tr::now, + lt_count, + info.messages, + Ui::Text::WithEntities); + }; + auto text = PreparedServiceText{ + .text = item->out() + ? tr::lng_action_paid_message_sent( + tr::now, + lt_count, + info.stars, + lt_action, + action(), + Ui::Text::WithEntities) + : tr::lng_action_paid_message_got( + tr::now, + lt_count, + info.stars, + lt_name, + Ui::Text::Link(item->from()->shortName(), 1), + Ui::Text::WithEntities), + }; + if (!item->out()) { + text.links.push_back(item ->from()->createOpenLink()); + } + setServicePreMessage(std::move(text)); +} + void Message::refreshRightBadge() { const auto item = data(); const auto drawChannelBadge = [&] @@ -528,11 +577,10 @@ void Message::refreshRightBadge() { if (badge.empty()) { _rightBadge.clear(); } else { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &item->history()->session(), - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); _rightBadge.setMarkedText( st::defaultTextStyle, badge, @@ -1028,11 +1076,11 @@ void Message::refreshTopicButton() { _topicButton->link = MakeTopicButtonLink(topic, jumpToId); if (_topicButton->nameVersion != topic->titleVersion()) { _topicButton->nameVersion = topic->titleVersion(); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history()->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, + .repaint = [=] { customEmojiRepaint(); }, .customEmojiLoopLimit = 1, - }; + }); _topicButton->name.setMarkedText( st::fwdTextStyle, topic->titleWithIcon(), @@ -2493,6 +2541,13 @@ TextState Message::textState( return result; } + if (const auto service = Get()) { + result.link = service->textState(point, request, g); + if (result.link) { + return result; + } + } + const auto bubble = drawBubble(); const auto reactionsInBubble = _reactions && embedReactionsInBubble(); const auto mediaDisplayed = media && media->isDisplayed(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index e3541e10f..1885dbab1 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -178,6 +178,7 @@ private: bool updateBottomInfo(); + void initPaidInformation(); void initLogEntryOriginal(); void initPsa(); void fromNameUpdated(int width) const; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index eebbb1848..9564b94c5 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -29,10 +29,10 @@ namespace { Fn repaint) { return Ui::MessageBarContent{ .text = item->inReplyText(), - .context = Core::MarkedTextContext{ + .context = Core::TextContext({ .session = &item->history()->session(), - .customEmojiRepaint = std::move(repaint), - }, + .repaint = std::move(repaint), + }), }; } diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 8b69b95c1..9f4ca309d 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -741,8 +741,8 @@ void RepliesWidget::setupComposeControls() { }, lifetime()); _composeControls->sendVoiceRequests( - ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { - sendVoice(std::move(data)); + ) | rpl::start_with_next([=](const ComposeControls::VoiceToSend &data) { + sendVoice(data); }, lifetime()); _composeControls->sendCommandRequests( @@ -1078,25 +1078,59 @@ void RepliesWidget::sendingFilesConfirmed( std::move(list), way, _history->peer->slowmodeApplied()); - const auto type = way.sendImagesAsPhotos() - ? SendMediaType::Photo - : SendMediaType::File; + auto bundle = PrepareFilesBundle( + std::move(groups), + way, + std::move(caption), + ctrlShiftEnter); + sendingFilesConfirmed(std::move(bundle), options); +} + +bool RepliesWidget::checkSendPayment( + int messagesCount, + int starsApproved, + Fn withPaymentApproved) { + return _sendPayment.check( + controller(), + _history->peer, + messagesCount, + starsApproved, + std::move(withPaymentApproved)); +} + +void RepliesWidget::sendingFilesConfirmed( + std::shared_ptr bundle, + Api::SendOptions options) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendingFilesConfirmed(bundle, copy); + }; + const auto checked = checkSendPayment( + bundle->totalCount, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if ((groups.size() != 1 || !groups.front().sentWithCaption()) - && !caption.text.isEmpty()) { + if (bundle->sendComment) { auto message = Api::MessageToSend(action); - message.textWithTags = base::take(caption); + message.textWithTags = base::take(bundle->caption); session().api().sendMessage(std::move(message)); } - for (auto &group : groups) { + for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; session().api().sendFiles( std::move(group.list), type, - base::take(caption), + base::take(bundle->caption), album, action); } @@ -1235,7 +1269,20 @@ void RepliesWidget::send() { send({}); } -void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) { +void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) { + const auto withPaymentApproved = [=](int approved) { + auto copy = data; + copy.options.starsApproved = approved; + sendVoice(copy); + }; + const auto checked = checkSendPayment( + 1, + data.options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + auto action = prepareSendAction(data.options); session().api().sendVoiceMessage( data.bytes, @@ -1262,19 +1309,32 @@ void RepliesWidget::send(Api::SendOptions options) { message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); message.webPage = _composeControls->webPageDraft(); - const auto error = GetErrorForSending( - _history->peer, - { - .topicRootId = _topic ? _topic->rootId() : MsgId(0), - .forward = &_composeControls->forwardItems(), - .text = &message.textWithTags, - .ignoreSlowmodeCountdown = (options.scheduled != 0), - }); + auto request = SendingErrorRequest{ + .topicRootId = _topic ? _topic->rootId() : MsgId(0), + .forward = &_composeControls->forwardItems(), + .text = &message.textWithTags, + .ignoreSlowmodeCountdown = (options.scheduled != 0), + }; + request.messagesCount = ComputeSendingMessagesCount(_history, request); + const auto error = GetErrorForSending(_history->peer, request); if (error) { Data::ShowSendErrorToast(controller(), _history->peer, error); return; } - + if (!options.scheduled) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + send(copy); + }; + const auto checked = checkSendPayment( + request.messagesCount, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + } session().api().sendMessage(std::move(message)); _composeControls->clear(); @@ -1428,6 +1488,18 @@ bool RepliesWidget::sendExistingDocument( || ShowSendPremiumError(controller(), document)) { return false; } + const auto withPaymentApproved = [=](int approved) { + auto copy = messageToSend; + copy.action.options.starsApproved = approved; + sendExistingDocument(document, std::move(copy), localId); + }; + const auto checked = checkSendPayment( + 1, + messageToSend.action.options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } Api::SendExistingDocument( std::move(messageToSend), @@ -1456,6 +1528,19 @@ bool RepliesWidget::sendExistingPhoto( return false; } + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendExistingPhoto(photo, copy); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } + Api::SendExistingPhoto( Api::MessageToSend(prepareSendAction(options)), photo); @@ -1466,13 +1551,13 @@ bool RepliesWidget::sendExistingPhoto( } void RepliesWidget::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot) { if (const auto error = result->getErrorOnSend(_history)) { Data::ShowSendErrorToast(controller(), _history->peer, error); return; } - sendInlineResult(result, bot, {}, std::nullopt); + sendInlineResult(std::move(result), bot, {}, std::nullopt); //const auto callback = [=](Api::SendOptions options) { // sendInlineResult(result, bot, options); //}; @@ -1482,13 +1567,30 @@ void RepliesWidget::sendInlineResult( } void RepliesWidget::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options, std::optional localMessageId) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendInlineResult(result, bot, copy, localMessageId); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + auto action = prepareSendAction(options); action.generateLocal = true; - session().api().sendInlineResult(bot, result, action, localMessageId); + session().api().sendInlineResult( + bot, + result.get(), + action, + localMessageId); _composeControls->clear(); //_saveDraftText = true; @@ -1511,7 +1613,9 @@ void RepliesWidget::sendInlineResult( SendMenu::Details RepliesWidget::sendMenuDetails() const { using Type = SendMenu::Type; - const auto type = _topic ? Type::Scheduled : Type::SilentOnly; + const auto type = (_topic && !_history->peer->starsPerMessageChecked()) + ? Type::Scheduled + : Type::SilentOnly; return SendMenu::Details{ .type = type }; } @@ -2628,11 +2732,31 @@ bool RepliesWidget::listIsGoodForAroundPosition( void RepliesWidget::listSendBotCommand( const QString &command, const FullMsgId &context) { + sendBotCommandWithOptions(command, context, {}); +} + +void RepliesWidget::sendBotCommandWithOptions( + const QString &command, + const FullMsgId &context, + Api::SendOptions options) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendBotCommandWithOptions(command, context, copy); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + const auto text = Bot::WrapCommandInChat( _history->peer, command, context); - auto message = Api::MessageToSend(prepareSendAction({})); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = { text }; session().api().sendMessage(std::move(message)); finishSending(); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 94f9dfba3..5d7758b7f 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/section_memento.h" #include "history/view/history_view_corner_buttons.h" #include "history/view/history_view_list_widget.h" +#include "history/history_item_helpers.h" #include "history/history_view_swipe_data.h" #include "data/data_messages.h" #include "base/timer.h" @@ -38,6 +39,7 @@ class PlainShadow; class FlatButton; class PinnedBar; struct PreparedList; +struct PreparedBundle; class SendFilesWay; } // namespace Ui @@ -207,6 +209,11 @@ private: void checkActivation() override; void doSetInnerFocus() override; + [[nodiscard]] bool checkSendPayment( + int messagesCount, + int starsApproved, + Fn withPaymentApproved); + void onScroll(); void updateInnerVisibleArea(); void updateControlsGeometry(); @@ -251,7 +258,7 @@ private: Api::SendOptions options) const; void send(); void send(Api::SendOptions options); - void sendVoice(Controls::VoiceToSend &&data); + void sendVoice(const Controls::VoiceToSend &data); void edit( not_null item, Api::SendOptions options, @@ -308,6 +315,14 @@ private: TextWithTags &&caption, Api::SendOptions options, bool ctrlShiftEnter); + void sendingFilesConfirmed( + std::shared_ptr bundle, + Api::SendOptions options); + + void sendBotCommandWithOptions( + const QString &command, + const FullMsgId &context, + Api::SendOptions options); bool sendExistingDocument( not_null document, @@ -318,10 +333,10 @@ private: not_null photo, Api::SendOptions options); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options, std::optional localMessageId); @@ -348,6 +363,7 @@ private: std::unique_ptr _composeControls; std::unique_ptr _composeSearch; std::unique_ptr _joinGroup; + std::unique_ptr _payForMessage; std::unique_ptr _topicReopenBar; std::unique_ptr _emptyPainter; bool _skipScrollEvent = false; @@ -379,6 +395,8 @@ private: HistoryView::ChatPaintGestureHorizontalData _gestureHorizontal; + SendPaymentHelper _sendPayment; + int _lastScrollTop = 0; int _topicReopenBarHeight = 0; int _scrollTopDelta = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 4e7f6c48b..107fb0480 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -248,10 +248,10 @@ void Reply::update( }).text : TextWithEntities(); const auto repaint = [=] { item->customEmojiRepaint(); }; - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &view->history()->session(), - .customEmojiRepaint = repaint, - }; + .repaint = repaint, + }); _text.setMarkedText( st::defaultTextStyle, text, @@ -467,11 +467,10 @@ void Reply::updateName( if (!viaBotUsername.isEmpty()) { nameFull.append(u" @"_q).append(viaBotUsername); } - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history->session(), - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); _name.setMarkedText( st::fwdTextStyle, nameFull, diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp index 9ab0af794..1dc8a8b64 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp @@ -62,7 +62,8 @@ bool CanScheduleUntilOnline(not_null peer) { if (const auto user = peer->asUser()) { return !user->isSelf() && !user->isBot() - && !user->lastseen().isHidden(); + && !user->lastseen().isHidden() + && !user->starsPerMessageChecked(); } return false; } diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 39510e857..84283bced 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -917,7 +917,7 @@ bool ScheduledWidget::sendExistingPhoto( } void ScheduledWidget::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot) { if (const auto error = result->getErrorOnSend(_history)) { Data::ShowSendErrorToast(controller(), _history->peer, error); @@ -931,12 +931,16 @@ void ScheduledWidget::sendInlineResult( } void ScheduledWidget::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options) { auto action = prepareSendAction(options); action.generateLocal = true; - session().api().sendInlineResult(bot, result, action, std::nullopt); + session().api().sendInlineResult( + bot, + result.get(), + action, + std::nullopt); _composeControls->clear(); //_saveDraftText = true; diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index a2e3a5438..bd79ded77 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -274,10 +274,10 @@ private: not_null photo, Api::SendOptions options); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options); diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp index a2dd5d2e9..371ec0a45 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp @@ -38,10 +38,10 @@ Game::Game( , _title(st::msgMinWidth - _st.padding.left() - _st.padding.right()) , _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) { if (!consumed.text.isEmpty()) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history()->session(), - .customEmojiRepaint = [=] { _parent->customEmojiRepaint(); }, - }; + .repaint = [=] { _parent->customEmojiRepaint(); }, + }); _description.setMarkedText( st::webPageDescriptionStyle, consumed, @@ -503,10 +503,10 @@ void Game::parentTextUpdated() { if (const auto media = _parent->data()->media()) { const auto consumed = media->consumedMessageText(); if (!consumed.text.isEmpty()) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history()->session(), - .customEmojiRepaint = [=] { _parent->customEmojiRepaint(); }, - }; + .repaint = [=] { _parent->customEmojiRepaint(); }, + }); _description.setMarkedText( st::webPageDescriptionStyle, consumed, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index d41c93feb..1d3554972 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -244,10 +244,11 @@ void Media::drawPurchasedTag( const auto session = &item->history()->session(); auto text = Ui::Text::Colorized(Ui::CreditsEmojiSmall(session)); text.append(Lang::FormatCountDecimal(amount)); - purchased->text.setMarkedText(st::defaultTextStyle, text, kMarkupTextOptions, Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [] {}, - }); + purchased->text.setMarkedText( + st::defaultTextStyle, + text, + kMarkupTextOptions, + Core::TextContext({ .session = session })); } const auto st = context.st; @@ -413,10 +414,7 @@ void Media::drawSpoilerTag( price, Ui::Text::WithEntities), kMarkupTextOptions, - Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [] {}, - }); + Core::TextContext({ .session = session })); } const auto width = iconSkip + text.maxWidth(); const auto inner = QRect(0, 0, width, text.minHeight()); @@ -541,10 +539,10 @@ Ui::Text::String Media::createCaption(not_null item) const { - st::msgPadding.left() - st::msgPadding.right(); auto result = Ui::Text::String(minResizeWidth); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &history()->session(), - .customEmojiRepaint = [=] { _parent->customEmojiRepaint(); }, - }; + .repaint = [=] { _parent->customEmojiRepaint(); }, + }); result.setMarkedText( st::messageTextStyle, item->translatedTextWithLocalEntities(), diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 93c877b66..844f0465c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -78,6 +78,15 @@ enum class MediaInBubbleState : uchar { TimeId duration, const QString &base); +struct PaidInformation { + int messages = 0; + int stars = 0; + + explicit operator bool() const { + return stars != 0; + } +}; + class Media : public Object, public base::has_weak_ptr { public: explicit Media(not_null parent) : _parent(parent) { @@ -121,6 +130,10 @@ public: [[nodiscard]] virtual bool allowsFastShare() const { return false; } + [[nodiscard]] virtual auto paidInformation() const + -> std::optional { + return {}; + } virtual void refreshParentId(not_null realParent) { } virtual void drawHighlight( diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index f2c970e40..3b70d6282 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -236,7 +236,7 @@ MediaGenericTextPart::MediaGenericTextPart( QMargins margins, const style::TextStyle &st, const base::flat_map &links, - const std::any &context) + const Ui::Text::MarkedContext &context) : _text(st::msgMinWidth) , _margins(margins) { _text.setMarkedText( diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index 437c665a4..0fd011914 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -141,7 +141,7 @@ public: QMargins margins, const style::TextStyle &st = st::defaultTextStyle, const base::flat_map &links = {}, - const std::any &context = {}); + const Ui::Text::MarkedContext &context = {}); void draw( Painter &p, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index 3bbec1bc8..4072d8a00 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -871,6 +871,15 @@ QPoint GroupedMedia::resolveCustomInfoRightBottom() const { return QPoint(width() - skipx, height() - skipy); } +std::optional GroupedMedia::paidInformation() const { + auto result = PaidInformation(); + for (const auto &part : _parts) { + ++result.messages; + result.stars += part.item->starsPaid(); + } + return result; +} + bool GroupedMedia::enforceBubbleWidth() const { return _mode == Mode::Grid; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h index 459eb4b1c..24499a776 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h @@ -93,6 +93,7 @@ public: bool allowsFastShare() const override { return true; } + std::optional paidInformation() const override; bool customHighlight() const override { return true; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 72b1df9ba..bc9386a1d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_poll.h" -#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "core/ui_integration.h" // TextContext #include "lang/lang_keys.h" #include "history/history.h" #include "history/history_item.h" @@ -158,7 +158,7 @@ struct Poll::Answer { void fillData( not_null poll, const PollAnswer &original, - Core::MarkedTextContext context); + Ui::Text::MarkedContext context); Ui::Text::String text; QByteArray option; @@ -206,7 +206,7 @@ Poll::Answer::Answer() : text(st::msgMinWidth / 2) { void Poll::Answer::fillData( not_null poll, const PollAnswer &original, - Core::MarkedTextContext context) { + Ui::Text::MarkedContext context) { chosen = original.chosen; correct = poll->quiz() ? original.correct : chosen; if (!text.isEmpty() && text.toTextWithEntities() == original.text) { @@ -396,11 +396,11 @@ void Poll::updateTexts() { st::historyPollQuestionStyle, _poll->question, options, - Core::MarkedTextContext{ + Core::TextContext({ .session = &_poll->session(), - .customEmojiRepaint = [=] { repaint(); }, + .repaint = [=] { repaint(); }, .customEmojiLoopLimit = 2, - }); + })); } if (_flags != _poll->flags() || _subtitle.isEmpty()) { using Flag = PollData::Flag; @@ -525,11 +525,11 @@ void Poll::updateRecentVoters() { } void Poll::updateAnswers() { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_poll->session(), - .customEmojiRepaint = [=] { repaint(); }, + .repaint = [=] { repaint(); }, .customEmojiLoopLimit = 2, - }; + }); const auto changed = !ranges::equal( _answers, _poll->answers, diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index 08444c490..3cf6d7b77 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -47,10 +47,10 @@ ServiceBox::ServiceBox( _content->title(), kMarkupTextOptions, _maxWidth, - Core::MarkedTextContext{ + Core::TextContext({ .session = &parent->history()->session(), - .customEmojiRepaint = [parent] { parent->customEmojiRepaint(); }, - }) + .repaint = [parent] { parent->customEmojiRepaint(); }, + })) , _subtitle( st::premiumPreviewAbout.style, Ui::Text::Filtered( @@ -65,10 +65,10 @@ ServiceBox::ServiceBox( }), kMarkupTextOptions, _maxWidth, - Core::MarkedTextContext{ + Core::TextContext({ .session = &parent->history()->session(), - .customEmojiRepaint = [parent] { parent->customEmojiRepaint(); }, - }) + .repaint = [parent] { parent->customEmojiRepaint(); }, + })) , _size( _content->width(), (st::msgServiceGiftBoxTopSkip diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker_player.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker_player.cpp index 64f6a883b..da9587057 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker_player.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker_player.cpp @@ -21,6 +21,10 @@ LottiePlayer::LottiePlayer(std::unique_ptr lottie) } void LottiePlayer::setRepaintCallback(Fn callback) { + if (!callback) { + _repaintLifetime.destroy(); + return; + } _repaintLifetime = _lottie->updates( ) | rpl::start_with_next([=](Lottie::Update update) { v::match(update.data, [&](const Lottie::Information &) { @@ -82,7 +86,9 @@ void WebmPlayer::clipCallback(ClipNotification notification) { case ClipNotification::Repaint: break; } - _repaintCallback(); + if (const auto onstack = _repaintCallback) { + onstack(); + } } void WebmPlayer::setRepaintCallback(Fn callback) { @@ -133,7 +139,9 @@ StaticStickerPlayer::StaticStickerPlayer( } void StaticStickerPlayer::setRepaintCallback(Fn callback) { - callback(); + if (callback) { + callback(); + } } bool StaticStickerPlayer::ready() { diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp index 27ca9c545..abda42b78 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -39,60 +39,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { -class TextPartColored final : public MediaGenericTextPart { -public: - TextPartColored( - TextWithEntities text, - QMargins margins, - QColor color, - const style::TextStyle &st = st::defaultTextStyle, - const base::flat_map &links = {}, - const std::any &context = {}); - -private: - void setupPen( - Painter &p, - not_null owner, - const PaintContext &context) const override; - - QColor _color; - -}; - -class AttributeTable final : public MediaGenericPart { -public: - struct Entry { - QString label; - QString value; - }; - - AttributeTable( - std::vector entries, - QMargins margins, - QColor labelColor); - - void draw( - Painter &p, - not_null owner, - const PaintContext &context, - int outerWidth) const override; - - QSize countOptimalSize() override; - QSize countCurrentSize(int newWidth) override; - -private: - struct Part { - Ui::Text::String label; - Ui::Text::String value; - }; - - std::vector _parts; - QMargins _margins; - QColor _labelColor; - int _valueLeft = 0; - -}; - class ButtonPart final : public MediaGenericPart { public: ButtonPart( @@ -270,116 +216,7 @@ QSize ButtonPart::countCurrentSize(int newWidth) { return optimalSize(); } -TextPartColored::TextPartColored( - TextWithEntities text, - QMargins margins, - QColor color, - const style::TextStyle &st, - const base::flat_map &links, - const std::any &context) -: MediaGenericTextPart(text, margins, st, links, context) -, _color(color) { -} - -void TextPartColored::setupPen( - Painter &p, - not_null owner, - const PaintContext &context) const { - p.setPen(_color); -} - -AttributeTable::AttributeTable( - std::vector entries, - QMargins margins, - QColor labelColor) -: _margins(margins) -, _labelColor(labelColor) { - for (const auto &entry : entries) { - _parts.emplace_back(); - auto &part = _parts.back(); - part.label.setText(st::defaultTextStyle, entry.label); - part.value.setMarkedText( - st::defaultTextStyle, - Ui::Text::Bold(entry.value)); - } -} - -void AttributeTable::draw( - Painter &p, - not_null owner, - const PaintContext &context, - int outerWidth) const { - const auto labelRight = _valueLeft - st::chatUniqueTableSkip; - const auto palette = &context.st->serviceTextPalette(); - auto top = _margins.top(); - const auto paint = [&]( - const Ui::Text::String &text, - int left, - int availableWidth, - style::align align) { - text.draw(p, { - .position = { left, top }, - .outerWidth = outerWidth, - .availableWidth = availableWidth, - .align = align, - .palette = palette, - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = context.now, - .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionLines = 1, - }); - }; - const auto forLabel = labelRight - _margins.left(); - const auto forValue = width() - _valueLeft - _margins.right(); - const auto white = QColor(255, 255, 255); - for (const auto &part : _parts) { - p.setPen(_labelColor); - paint(part.label, _margins.left(), forLabel, style::al_topright); - p.setPen(white); - paint(part.value, _valueLeft, forValue, style::al_topleft); - top += st::normalFont->height + st::chatUniqueRowSkip; - } -} - -QSize AttributeTable::countOptimalSize() { - auto maxLabel = 0; - auto maxValue = 0; - for (const auto &part : _parts) { - maxLabel = std::max(maxLabel, part.label.maxWidth()); - maxValue = std::max(maxValue, part.value.maxWidth()); - } - const auto skip = st::chatUniqueTableSkip; - const auto row = st::normalFont->height + st::chatUniqueRowSkip; - const auto height = int(_parts.size()) * row - st::chatUniqueRowSkip; - return { - _margins.left() + maxLabel + skip + maxValue + _margins.right(), - _margins.top() + height + _margins.bottom(), - }; -} - -QSize AttributeTable::countCurrentSize(int newWidth) { - const auto skip = st::chatUniqueTableSkip; - const auto width = newWidth - _margins.left() - _margins.right() - skip; - auto maxLabel = 0; - auto maxValue = 0; - for (const auto &part : _parts) { - maxLabel = std::max(maxLabel, part.label.maxWidth()); - maxValue = std::max(maxValue, part.value.maxWidth()); - } - if (width <= 0 || !maxLabel) { - _valueLeft = _margins.left(); - } else if (!maxValue) { - _valueLeft = newWidth - _margins.right(); - } else { - _valueLeft = _margins.left() - + int((int64(maxLabel) * width) / (maxLabel + maxValue)) - + skip; - } - return { newWidth, minHeight() }; -} - -}; // namespace +} // namespace auto GenerateUniqueGiftMedia( not_null parent, @@ -402,7 +239,7 @@ auto GenerateUniqueGiftMedia( push(std::make_unique( std::move(text), margins, - color, + [color](const auto&) { return color; }, st)); }; @@ -447,15 +284,19 @@ auto GenerateUniqueGiftMedia( gift->backdrop.textColor, st::chatUniqueTextPadding); + const auto name = [](const Data::UniqueGiftAttribute &value) { + return Ui::Text::Bold(value.name); + }; auto attributes = std::vector{ - { tr::lng_gift_unique_model(tr::now), gift->model.name }, - { tr::lng_gift_unique_backdrop(tr::now), gift->backdrop.name }, - { tr::lng_gift_unique_symbol(tr::now), gift->pattern.name }, + { tr::lng_gift_unique_model(tr::now), name(gift->model) }, + { tr::lng_gift_unique_backdrop(tr::now), name(gift->backdrop) }, + { tr::lng_gift_unique_symbol(tr::now), name(gift->pattern) }, }; push(std::make_unique( std::move(attributes), st::chatUniqueTextPadding, - gift->backdrop.textColor)); + [c = gift->backdrop.textColor](const auto&) { return c; }, + [](const auto&) { return QColor(255, 255, 255); })); auto link = OpenStarGiftLink(parent->data()); push(std::make_unique( @@ -594,4 +435,137 @@ std::unique_ptr MakeGenericButtonPart( return std::make_unique(text, margins, repaint, link, bg); } +TextPartColored::TextPartColored( + TextWithEntities text, + QMargins margins, + Fn color, + const style::TextStyle &st, + const base::flat_map &links, + const Ui::Text::MarkedContext &context) +: MediaGenericTextPart(text, margins, st, links, context) +, _color(std::move(color)) { +} + +void TextPartColored::setupPen( + Painter &p, + not_null owner, + const PaintContext &context) const { + p.setPen(_color(context)); +} + +AttributeTable::AttributeTable( + std::vector entries, + QMargins margins, + Fn labelColor, + Fn valueColor, + const Ui::Text::MarkedContext &context) +: _margins(margins) +, _labelColor(std::move(labelColor)) +, _valueColor(std::move(valueColor)) { + for (const auto &entry : entries) { + _parts.emplace_back(); + auto &part = _parts.back(); + part.label.setText(st::defaultTextStyle, entry.label); + part.value.setMarkedText( + st::defaultTextStyle, + entry.value, + kMarkupTextOptions, + context); + } +} + +void AttributeTable::draw( + Painter &p, + not_null owner, + const PaintContext &context, + int outerWidth) const { + const auto labelRight = _valueLeft - st::chatUniqueTableSkip; + const auto palette = &context.st->serviceTextPalette(); + auto top = _margins.top(); + const auto paint = [&]( + const Ui::Text::String &text, + int left, + int availableWidth, + style::align align) { + text.draw(p, { + .position = { left, top }, + .outerWidth = outerWidth, + .availableWidth = availableWidth, + .align = align, + .palette = palette, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .elisionLines = 1, + }); + }; + const auto forLabel = labelRight - _margins.left(); + const auto forValue = width() - _valueLeft - _margins.right(); + for (const auto &part : _parts) { + p.setPen(_labelColor(context)); + paint(part.label, _margins.left(), forLabel, style::al_topright); + p.setPen(_valueColor(context)); + paint(part.value, _valueLeft, forValue, style::al_topleft); + top += st::normalFont->height + st::chatUniqueRowSkip; + } +} + +TextState AttributeTable::textState( + QPoint point, + StateRequest request, + int outerWidth) const { + auto top = _margins.top(); + for (const auto &part : _parts) { + const auto height = st::normalFont->height + st::chatUniqueRowSkip; + if (point.y() >= top && point.y() < top + height) { + point -= QPoint((outerWidth - width()) / 2 + _valueLeft, top); + auto result = TextState(); + auto forText = request.forText(); + forText.align = style::al_topleft; + result.link = part.value.getState(point, width(), forText).link; + return result; + } + top += height; + } + return {}; +} + +QSize AttributeTable::countOptimalSize() { + auto maxLabel = 0; + auto maxValue = 0; + for (const auto &part : _parts) { + maxLabel = std::max(maxLabel, part.label.maxWidth()); + maxValue = std::max(maxValue, part.value.maxWidth()); + } + const auto skip = st::chatUniqueTableSkip; + const auto row = st::normalFont->height + st::chatUniqueRowSkip; + const auto height = int(_parts.size()) * row - st::chatUniqueRowSkip; + return { + _margins.left() + maxLabel + skip + maxValue + _margins.right(), + _margins.top() + height + _margins.bottom(), + }; +} + +QSize AttributeTable::countCurrentSize(int newWidth) { + const auto skip = st::chatUniqueTableSkip; + const auto width = newWidth - _margins.left() - _margins.right() - skip; + auto maxLabel = 0; + auto maxValue = 0; + for (const auto &part : _parts) { + maxLabel = std::max(maxLabel, part.label.maxWidth()); + maxValue = std::max(maxValue, part.value.maxWidth()); + } + if (width <= 0 || !maxLabel) { + _valueLeft = _margins.left(); + } else if (!maxValue) { + _valueLeft = newWidth - _margins.right(); + } else { + _valueLeft = _margins.left() + + int((int64(maxLabel) * width) / (maxLabel + maxValue)) + + skip; + } + return { newWidth, minHeight() }; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h index 635190af1..4816b1f68 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "history/view/media/history_view_media_generic.h" + class Painter; namespace Data { @@ -55,4 +57,66 @@ class MediaGenericPart; ClickHandlerPtr link, QColor bg = QColor(0, 0, 0, 0)); + +class TextPartColored : public MediaGenericTextPart { +public: + TextPartColored( + TextWithEntities text, + QMargins margins, + Fn color, + const style::TextStyle &st = st::defaultTextStyle, + const base::flat_map &links = {}, + const Ui::Text::MarkedContext &context = {}); + +private: + void setupPen( + Painter &p, + not_null owner, + const PaintContext &context) const override; + + Fn _color; + +}; + +class AttributeTable final : public MediaGenericPart { +public: + struct Entry { + QString label; + TextWithEntities value; + }; + + AttributeTable( + std::vector entries, + QMargins margins, + Fn labelColor, + Fn valueColor, + const Ui::Text::MarkedContext &context = {}); + + void draw( + Painter &p, + not_null owner, + const PaintContext &context, + int outerWidth) const override; + TextState textState( + QPoint point, + StateRequest request, + int outerWidth) const override; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + +private: + struct Part { + Ui::Text::String label; + Ui::Text::String value; + }; + + std::vector _parts; + QMargins _margins; + Fn _labelColor; + Fn _valueColor; + int _valueLeft = 0; + +}; + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index e34d492fa..a129612cb 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -383,11 +383,10 @@ QSize WebPage::countOptimalSize() { // Detect _openButtonWidth before counting paddings. _openButton = Ui::Text::String(); if (HasButton(_data)) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_data->session(), - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); _openButton.setMarkedText( st::semiboldTextStyle, PageToPhrase(_data), @@ -543,16 +542,18 @@ QSize WebPage::countOptimalSize() { _description = Ui::Text::String(st::minPhotoSize - rect::m::sum::h(padding)); } - using MarkedTextContext = Core::MarkedTextContext; - auto context = MarkedTextContext{ + using Type = Core::TextContextDetails::HashtagMentionType; + auto context = Core::TextContext({ .session = &history()->session(), - .customEmojiRepaint = [=] { _parent->customEmojiRepaint(); }, - }; - if (_data->siteName == u"Twitter"_q) { - context.type = MarkedTextContext::HashtagMentionType::Twitter; - } else if (_data->siteName == u"Instagram"_q) { - context.type = MarkedTextContext::HashtagMentionType::Instagram; - } + .details = { + .type = ((_data->siteName == u"Twitter"_q) + ? Type::Twitter + : (_data->siteName == u"Instagram"_q) + ? Type::Instagram + : Type::Telegram), + }, + .repaint = [=] { _parent->customEmojiRepaint(); }, + }); _description.setMarkedText( st::webPageDescriptionStyle, text, diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp index 209e73e3c..90c6d52fa 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp @@ -154,7 +154,7 @@ Row::Row( : PeerListRow(peer, id) , _custom(reactionEntityData.isEmpty() ? nullptr - : factory(reactionEntityData, [=] { repaint(this); })) + : factory(reactionEntityData, { .repaint = [=] { repaint(this); } })) , _paused(std::move(paused)) { } diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp index f3b23804f..df06ba49f 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp @@ -58,7 +58,7 @@ not_null CreateTab( ? nullptr : factory( Data::ReactionEntityData(reaction), - [=] { result->update(); }); + { .repaint = [=] { result->update(); } }); result->paintRequest( ) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp index 049b84e09..a088bc958 100644 --- a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp @@ -600,12 +600,6 @@ object_ptr JoinStarRefBox( if (const auto average = program.revenuePerUser) { const auto layout = box->verticalLayout(); const auto session = &initialRecipient->session(); - const auto makeContext = [session](Fn update) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = std::move(update), - }; - }; auto text = Ui::Text::Colorized(Ui::CreditsEmoji(session)); text.append(Lang::FormatStarsAmountRounded(average)); layout->add( @@ -618,7 +612,7 @@ object_ptr JoinStarRefBox( Ui::Text::WithEntities), st::starrefRevenueText, st::defaultPopupMenu, - makeContext), + Core::TextContext({ .session = session })), st::boxRowPadding); Ui::AddSkip(layout, st::defaultVerticalListSkip); } @@ -916,7 +910,7 @@ std::unique_ptr MakePeerBubbleButton( userpic->moveToLeft(left, 0, outer.width()); if (right) { right->moveToLeft( - left + *width - padding.right() - right->width(), + left + *width - padding.right() - rwidth, padding.top(), outer.width()); } diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp index 2a4abeb28..a5a73614e 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp @@ -1402,7 +1402,7 @@ void CreateGiveawayBox( auto invoice = [&] { if (isPrepaidCredits) { return Payments::InvoicePremiumGiftCode{ - .creditsAmount = prepaid->credits, + .giveawayCredits = prepaid->credits, .randomId = prepaid->id, .users = prepaid->quantity, }; @@ -1412,7 +1412,7 @@ void CreateGiveawayBox( return Payments::InvoicePremiumGiftCode{ .currency = option.currency, .storeProduct = option.storeProduct, - .creditsAmount = option.credits, + .giveawayCredits = option.credits, .randomId = UniqueIdFromCreditsOption(option, peer), .amount = option.amount, .users = state->sliderValue.current(), diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index 3bf228c9b..1558fb259 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -221,6 +221,10 @@ darkGiftShare: icon {{ "menu/share", groupCallMembersFg }}; darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }}; darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }}; darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }}; +darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }}; +darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }}; +darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }}; +darkGiftUnpin: icon {{ "menu/unpin", groupCallMembersFg }}; darkGiftPalette: TextPalette(defaultTextPalette) { linkFg: mediaviewTextLinkFg; monoFg: groupCallMembersFg; diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp index e556b7c58..4b319c310 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_color_box.h" // AddLevelBadge. #include "chat_helpers/stickers_emoji_pack.h" #include "core/application.h" +#include "core/ui_integration.h" // TextContext. #include "data/components/credits.h" #include "data/data_channel.h" #include "data/data_premium_limits.h" @@ -24,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" +#include "dialogs/ui/chat_search_empty.h" #include "history/view/controls/history_view_webpage_processor.h" #include "info/bot/starref/info_bot_starref_join_widget.h" #include "info/bot/starref/info_bot_starref_setup_widget.h" @@ -297,9 +299,7 @@ void InnerWidget::load() { _showFinished.events( ) | rpl::take(1) | rpl::start_with_next([=] { - state->api.request( - ) | rpl::start_with_error_done(fail, [=] { - _state.currencyEarn = state->api.data(); + const auto nextRequests = [=] { state->apiCreditsHistory.request({}, [=]( const Data::CreditsStatusSlice &data) { _state.creditsStatusSlice = data; @@ -322,19 +322,52 @@ void InnerWidget::load() { state->apiPremiumBotLifetime.destroy(); }, state->apiPremiumBotLifetime); }); + }; + const auto isMegagroup = _peer->isMegagroup(); + state->api.request( + ) | rpl::start_with_error_done([=](const QString &error) { + if (isMegagroup) { + _state.currencyEarn = {}; + if (error == u"BROADCAST_REQUIRED"_q) { + _state.canViewCurrencyMegagroupEarn = false; + } + nextRequests(); + } else { + show->showToast(error); + } + }, [=] { + _state.currencyEarn = state->api.data(); + nextRequests(); }, state->apiLifetime); }, lifetime()); } void InnerWidget::fill() { const auto container = this; + if (!_state.currencyEarn && !_state.creditsEarn) { + const auto empty = container->add(object_ptr( + container, + Dialogs::SearchEmptyIcon::NoResults, + tr::lng_search_tab_no_results(Ui::Text::Bold))); + empty->setMinimalHeight(st::changePhoneIconSize); + empty->animate(); + return; + } const auto bot = (peerIsUser(_peer->id) && _peer->asUser()->botInfo) ? _peer->asUser() : nullptr; const auto channel = _peer->asChannel(); - const auto canViewCurrencyEarn = channel - ? (channel->flags() & ChannelDataFlag::CanViewRevenue) - : true; + const auto canViewCurrencyEarn = [&] { + if (!channel) { + return true; + } else if (!(channel->flags() & ChannelDataFlag::CanViewRevenue)) { + return false; + } else if (channel->isMegagroup()) { + return _state.canViewCurrencyMegagroupEarn; + } else { + return true; + } + }(); const auto &data = canViewCurrencyEarn ? _state.currencyEarn : Data::EarnStatistics(); @@ -370,12 +403,6 @@ void InnerWidget::fill() { const auto session = &_peer->session(); const auto withdrawalEnabled = WithdrawalEnabled(session); - const auto makeContext = [=](not_null l) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [=] { l->update(); }, - }; - }; const auto addEmojiToMajor = [=]( not_null label, rpl::producer value, @@ -401,7 +428,7 @@ void InnerWidget::fill() { ) | rpl::start_with_next([=](EarnInt v) { label->setMarkedText( base::duplicate(prepended).append(icon).append(MajorPart(v)), - makeContext(label)); + Core::TextContext({ .session = session })); }, label->lifetime()); }; @@ -413,11 +440,7 @@ void InnerWidget::fill() { st::channelEarnCurrencyLearnMargins, false)); - const auto arrow = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); + const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); const auto addAboutWithLearn = [&](const tr::phrase &text) { auto label = Ui::CreateLabelWithCustomEmoji( container, @@ -431,7 +454,7 @@ void InnerWidget::fill() { return Ui::Text::Link(std::move(text), 1); }), Ui::Text::RichLangValue), - { .session = session }, + Core::TextContext({ .session = session }), st::boxDividerLabel); label->setLink(1, std::make_shared([=] { _show->showBox(Box([=](not_null box) { @@ -548,7 +571,7 @@ void InnerWidget::fill() { Ui::Text::Link(bigCurrencyIcon, 1)), Ui::Text::RichLangValue ), - { .session = session }, + Core::TextContext({ .session = session }), st::boxTitle)))->entity(); const auto diamonds = l->lifetime().make_state(0); l->setLink(1, std::make_shared([=] { @@ -578,7 +601,7 @@ void InnerWidget::fill() { }), Ui::Text::RichLangValue ), - { .session = session }, + Core::TextContext({ .session = session }), st::channelEarnLearnDescription)); label->resizeToWidth(box->width() - rect::m::sum::h(st::boxRowPadding)); @@ -613,9 +636,11 @@ void InnerWidget::fill() { st::defaultBoxDividerLabelPadding, RectPart::Top | RectPart::Bottom)); }; - addAboutWithLearn(bot - ? tr::lng_channel_earn_about_bot - : tr::lng_channel_earn_about); + if (canViewCurrencyEarn) { + addAboutWithLearn(bot + ? tr::lng_channel_earn_about_bot + : tr::lng_channel_earn_about); + } { using Type = Statistic::ChartViewType; Ui::AddSkip(container); @@ -977,7 +1002,8 @@ void InnerWidget::fill() { const auto sectionIndex = container->lifetime().make_state(0); const auto rebuildLists = [=]( const Memento::SavedState &data, - not_null listsContainer) { + not_null listsContainer, + not_null historyDividerContainer) { const auto hasCurrencyTab = !data.currencyEarn.firstHistorySlice.list.empty(); const auto hasCreditsTab = !data.creditsStatusSlice.list.empty(); @@ -1361,9 +1387,9 @@ void InnerWidget::fill() { true); } if (hasCurrencyTab || hasCreditsTab) { - Ui::AddSkip(listsContainer); - Ui::AddDivider(listsContainer); - Ui::AddSkip(listsContainer); + Ui::AddSkip(historyDividerContainer); + Ui::AddDivider(historyDividerContainer); + Ui::AddSkip(historyDividerContainer); } listsContainer->resizeToWidth(width()); @@ -1371,18 +1397,20 @@ void InnerWidget::fill() { const auto historyContainer = container->add( object_ptr(container)); + const auto historyDividerContainer = container->add( + object_ptr(container)); rpl::single(rpl::empty) | rpl::then( _stateUpdated.events() ) | rpl::start_with_next([=] { const auto listsContainer = historyContainer->add( object_ptr(container)); - rebuildLists(_state, listsContainer); + rebuildLists(_state, listsContainer, historyDividerContainer); while (historyContainer->count() > 1) { delete historyContainer->widgetAt(0); } }, historyContainer->lifetime()); - if (channel) { + if (channel && !channel->isMegagroup()) { //constexpr auto kMaxCPM = 50; // Debug. const auto requiredLevel = Data::LevelLimits(session) .channelRestrictSponsoredLevelMin(); @@ -1439,6 +1467,10 @@ void InnerWidget::fill() { Ui::AddSkip(container); Ui::AddDividerText(container, tr::lng_channel_earn_off_about()); + } else { + while (historyDividerContainer->count() > 1) { + delete historyDividerContainer->widgetAt(0); + } } Ui::AddSkip(container); @@ -1481,4 +1513,3 @@ not_null InnerWidget::peer() const { } } // namespace Info::ChannelEarn - diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h index becd834c6..031b404d9 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h @@ -34,6 +34,7 @@ public: Data::CreditsEarnStatistics creditsEarn; Data::CreditsStatusSlice creditsStatusSlice; PeerId premiumBotId = PeerId(0); + bool canViewCurrencyMegagroupEarn = true; }; void setState(SavedState states); diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index 798d2180c..803e7c433 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -85,13 +85,11 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { unsubscribe(); v::match(descriptor, [&](const GiftTypePremium &data) { const auto months = data.months; - const auto years = (months % 12) ? 0 : months / 12; _text = Ui::Text::String(st::giftBoxGiftHeight / 4); _text.setMarkedText( st::defaultTextStyle, - Ui::Text::Bold(years - ? tr::lng_years(tr::now, lt_count, years) - : tr::lng_months(tr::now, lt_count, months) + Ui::Text::Bold( + tr::lng_months(tr::now, lt_count, months) ).append('\n').append( tr::lng_gift_premium_label(tr::now) )); @@ -101,6 +99,18 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { data.cost, data.currency, true)); + if (const auto stars = data.stars) { + const auto starsText = Lang::FormatCountDecimal(stars); + _byStars.setMarkedText( + st::giftBoxByStarsStyle, + tr::lng_gift_premium_by_stars( + tr::now, + lt_amount, + _delegate->ministar().append(' ' + starsText), + Ui::Text::WithEntities), + kMarkupTextOptions, + _delegate->textContext()); + } _userpic = nullptr; if (!_stars) { _stars.emplace(this, true, starsType); @@ -129,7 +139,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { (unique ? tr::lng_gift_price_unique(tr::now, Ui::Text::WithEntities) : _delegate->star().append( - ' ' + QString::number(data.info.stars))), + ' ' + Lang::FormatCountDecimal(data.info.stars))), kMarkupTextOptions, _delegate->textContext()); if (!_stars) { @@ -170,7 +180,9 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { QSize(buttonw, buttonh) ).marginsAdded(st::giftBoxButtonPadding); const auto skipy = _delegate->buttonSize().height() - - st::giftBoxButtonBottom + - (_byStars.isEmpty() + ? st::giftBoxButtonBottom + : st::giftBoxButtonBottomByStars) - inner.height(); const auto skipx = (width() - inner.width()) / 2; const auto outer = (width() - 2 * skipx); @@ -238,6 +250,12 @@ void GiftButton::resizeEvent(QResizeEvent *e) { } } +void GiftButton::contextMenuEvent(QContextMenuEvent *e) { + _contextMenuRequests.fire_copy((e->reason() == QContextMenuEvent::Mouse) + ? e->globalPos() + : QCursor::pos()); +} + void GiftButton::cacheUniqueBackground( not_null unique, int width, @@ -355,7 +373,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { ? st::giftBoxSmallStickerTop : _text.isEmpty() ? st::giftBoxStickerStarTop - : st::giftBoxStickerTop), + : _byStars.isEmpty() + ? st::giftBoxStickerTop + : st::giftBoxStickerTopByStars), size.width(), size.height()), frame); @@ -367,7 +387,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { ? st::giftBoxSmallStickerTop : _text.isEmpty() ? st::giftBoxStickerStarTop - : st::giftBoxStickerTop)); + : _byStars.isEmpty() + ? st::giftBoxStickerTop + : st::giftBoxStickerTopByStars)); _delegate->hiddenMark()->paint( p, frame, @@ -441,6 +463,24 @@ void GiftButton::paintEvent(QPaintEvent *e) { position.y() - rubberOut, cached); } + + v::match(_descriptor, [](const GiftTypePremium &) { + }, [&](const GiftTypeStars &data) { + if (unique && data.pinned) { + auto hq = PainterHighQualityEnabler(p); + const auto &icon = st::giftBoxPinIcon; + const auto skip = st::giftBoxUserpicSkip; + const auto add = (st::giftBoxUserpicSize - icon.width()) / 2; + p.setPen(Qt::NoPen); + p.setBrush(unique->backdrop.patternColor); + const auto rect = QRect( + QPoint(_extend.left() + skip, _extend.top() + skip), + QSize(icon.width() + 2 * add, icon.height() + 2 * add)); + p.drawEllipse(rect); + icon.paintInCenter(p, rect); + } + }); + if (!_button.isEmpty()) { p.setBrush(unique ? QBrush(QColor(255, 255, 255, .2 * 255)) @@ -473,8 +513,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { if (!_text.isEmpty()) { p.setPen(st::windowFg); _text.draw(p, { - .position = (position - + QPoint(0, st::giftBoxPremiumTextTop)), + .position = (position + QPoint(0, _byStars.isEmpty() + ? st::giftBoxPremiumTextTop + : st::giftBoxPremiumTextTopByStars)), .availableWidth = singlew, .align = style::al_top, }); @@ -492,6 +533,17 @@ void GiftButton::paintEvent(QPaintEvent *e) { + QPoint(padding.left(), padding.top())), .availableWidth = _price.maxWidth(), }); + + if (!_byStars.isEmpty()) { + p.setPen(st::creditsFg); + _byStars.draw(p, { + .position = QPoint( + position.x(), + _button.y() + _button.height() + st::giftBoxByStarsSkip), + .availableWidth = singlew, + .align = style::al_top, + }); + } } } @@ -515,11 +567,14 @@ TextWithEntities Delegate::star() { return owner->customEmojiManager().creditsEmoji(); } -std::any Delegate::textContext() { - return Core::MarkedTextContext{ - .session = &_window->session(), - .customEmojiRepaint = [] {}, - }; +TextWithEntities Delegate::ministar() { + const auto owner = &_window->session().data(); + const auto top = st::giftBoxByStarsStarTop; + return owner->customEmojiManager().ministarEmoji({ 0, top, 0, 0 }); +} + +Ui::Text::MarkedContext Delegate::textContext() { + return Core::TextContext({ .session = &_window->session() }); } QSize Delegate::buttonSize() { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index bba226aaa..0d93a8bf2 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -44,6 +44,7 @@ namespace Info::PeerGifts { struct GiftTypePremium { int64 cost = 0; QString currency; + int stars = 0; int months = 0; int discountPercent = 0; @@ -55,7 +56,9 @@ struct GiftTypePremium { struct GiftTypeStars { Data::StarGift info; PeerData *from = nullptr; + TimeId date = 0; bool userpic = false; + bool pinned = false; bool hidden = false; bool mine = false; @@ -101,7 +104,8 @@ enum class GiftButtonMode { class GiftButtonDelegate { public: [[nodiscard]] virtual TextWithEntities star() = 0; - [[nodiscard]] virtual std::any textContext() = 0; + [[nodiscard]] virtual TextWithEntities ministar() = 0; + [[nodiscard]] virtual Ui::Text::MarkedContext textContext() = 0; [[nodiscard]] virtual QSize buttonSize() = 0; [[nodiscard]] virtual QMargins buttonExtend() = 0; [[nodiscard]] virtual auto buttonPatternEmoji( @@ -124,9 +128,14 @@ public: void setDescriptor(const GiftDescriptor &descriptor, Mode mode); void setGeometry(QRect inner, QMargins extend); + [[nodiscard]] rpl::producer contextMenuRequests() const { + return _contextMenuRequests.events(); + } + private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; void cacheUniqueBackground( not_null unique, @@ -139,10 +148,12 @@ private: void unsubscribe(); const not_null _delegate; + rpl::event_stream _contextMenuRequests; QImage _hiddenBgCache; GiftDescriptor _descriptor; Ui::Text::String _text; Ui::Text::String _price; + Ui::Text::String _byStars; std::shared_ptr _userpic; QImage _uniqueBackgroundCache; std::unique_ptr _uniquePatternEmoji; @@ -169,7 +180,8 @@ public: ~Delegate(); TextWithEntities star() override; - std::any textContext() override; + TextWithEntities ministar() override; + Ui::Text::MarkedContext textContext() override; QSize buttonSize() override; QMargins buttonExtend() override; auto buttonPatternEmoji( diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 9656bd08a..6bbde5e05 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "apiwrap.h" #include "data/data_channel.h" +#include "data/data_credits.h" #include "data/data_session.h" #include "data/data_user.h" #include "info/peer_gifts/info_peer_gifts_common.h" @@ -20,10 +21,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/box_content_divider.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/wrap/slide_wrap.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "mtproto/sender.h" #include "window/window_session_controller.h" @@ -48,7 +51,9 @@ constexpr auto kPerPage = 50; .from = ((gift.anonymous || !gift.fromId) ? nullptr : to->owner().peer(gift.fromId).get()), + .date = gift.date, .userpic = !gift.info.unique, + .pinned = gift.pinned, .hidden = gift.hidden, .mine = to->isSelf(), }; @@ -99,8 +104,12 @@ private: void refreshButtons(); void validateButtons(); void showGift(int index); + void showMenuFor(not_null button, QPoint point); void refreshAbout(); + void markPinned(std::vector::iterator i); + void markUnpinned(std::vector::iterator i); + int resizeGetHeight(int width) override; const not_null _window; @@ -131,6 +140,8 @@ private: int _visibleFrom = 0; int _visibleTill = 0; + base::unique_qptr _menu; + }; InnerWidget::InnerWidget( @@ -188,6 +199,8 @@ void InnerWidget::subscribeToUpdates() { } else if (update.action == Action::Save || update.action == Action::Unsave) { i->gift.hidden = (update.action == Action::Unsave); + + const auto unpin = i->gift.hidden && i->gift.pinned; v::match(i->descriptor, [](GiftTypePremium &) { }, [&](GiftTypeStars &data) { data.hidden = i->gift.hidden; @@ -198,6 +211,16 @@ void InnerWidget::subscribeToUpdates() { view.manageId = {}; } } + if (unpin) { + markUnpinned(i); + } + } else if (update.action == Action::Pin + || update.action == Action::Unpin) { + if (update.action == Action::Pin) { + markPinned(i); + } else { + markUnpinned(i); + } } else { return; } @@ -205,6 +228,65 @@ void InnerWidget::subscribeToUpdates() { }, lifetime()); } +void InnerWidget::markPinned(std::vector::iterator i) { + const auto index = int(i - begin(_entries)); + + i->gift.pinned = true; + v::match(i->descriptor, [](const GiftTypePremium &) { + }, [&](GiftTypeStars &data) { + data.pinned = true; + }); + if (index) { + std::rotate(begin(_entries), i, i + 1); + } + auto unpin = end(_entries); + const auto session = &_window->session(); + const auto limit = session->appConfig().pinnedGiftsLimit(); + if (limit < _entries.size()) { + const auto j = begin(_entries) + limit; + if (j->gift.pinned) { + unpin = j; + } + } + for (auto &view : _views) { + if (view.index <= index) { + view.index = -1; + view.manageId = {}; + } + } + if (unpin != end(_entries)) { + markUnpinned(unpin); + } +} + +void InnerWidget::markUnpinned(std::vector::iterator i) { + const auto index = int(i - begin(_entries)); + + i->gift.pinned = false; + v::match(i->descriptor, [](const GiftTypePremium &) { + }, [&](GiftTypeStars &data) { + data.pinned = false; + }); + auto after = index + 1; + for (auto j = i + 1; j != end(_entries); ++j) { + if (!j->gift.pinned && j->gift.date <= i->gift.date) { + break; + } + ++after; + } + if (after == _entries.size()) { + _entries.erase(i); + } else if (after > index + 1) { + std::rotate(i, i + 1, begin(_entries) + after); + } + for (auto &view : _views) { + if (view.index >= index) { + view.index = -1; + view.manageId = {}; + } + } +} + void InnerWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { @@ -355,7 +437,12 @@ void InnerWidget::validateButtons() { views.push_back(base::take(*unused)); } else { auto button = std::make_unique(this, &_delegate); - button->show(); + const auto raw = button.get(); + raw->contextMenuRequests( + ) | rpl::start_with_next([=](QPoint point) { + showMenuFor(raw, point); + }, raw->lifetime()); + raw->show(); views.push_back({ .button = std::move(button) }); } } @@ -386,6 +473,58 @@ void InnerWidget::validateButtons() { std::swap(_views, views); } +void InnerWidget::showMenuFor(not_null button, QPoint point) { + if (_menu) { + return; + } + const auto index = [&] { + for (const auto &view : _views) { + if (view.button.get() == button) { + return view.index; + } + } + return -1; + }(); + if (index < 0) { + return; + } + + auto entry = ::Settings::SavedStarGiftEntry( + _peer, + _entries[index].gift); + auto pinnedIds = std::vector(); + for (const auto &entry : _entries) { + if (entry.gift.pinned) { + pinnedIds.push_back(entry.gift.manageId); + } else { + break; + } + } + entry.pinnedSavedGifts = [pinnedIds] { + auto result = std::vector(); + result.reserve(pinnedIds.size()); + for (const auto &id : pinnedIds) { + result.push_back({ + .bareMsgId = uint64(id.userMessageId().bare), + .bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0, + .giftChannelSavedId = id.chatSavedId(), + .stargift = true, + }); + } + return result; + }; + _menu = base::make_unique_q(this, st::popupMenuWithIcons); + ::Settings::FillSavedStarGiftMenu( + _controller->uiShow(), + _menu.get(), + entry, + ::Settings::SavedStarGiftMenuType::List); + if (_menu->empty()) { + return; + } + _menu->popup(point); +} + void InnerWidget::showGift(int index) { Expects(index >= 0 && index < _entries.size()); @@ -630,7 +769,7 @@ void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { }); }, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck); - if (_inner->peer()->canManageGifts() && _inner->peer()->isChannel()) { + if (_inner->peer()->canManageGifts()) { addAction({ .isSeparator = true }); addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 73595a182..b3e41b57d 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -903,10 +903,10 @@ rpl::producer AddCurrencyAction( .append(QChar(' ')) .append(Info::ChannelEarn::MajorPart(balance)) .append(Info::ChannelEarn::MinorPart(balance)), - Core::MarkedTextContext{ + Core::TextContext({ .session = &user->session(), - .customEmojiRepaint = [=] { name->update(); }, - }); + .repaint = [=] { name->update(); }, + })); name->resizeToNaturalWidth(available); name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); }, name->lifetime()); @@ -976,10 +976,10 @@ rpl::producer AddCreditsAction( base::duplicate(icon) .append(QChar(' ')) .append(Lang::FormatStarsAmountDecimal(balance)), - Core::MarkedTextContext{ + Core::TextContext({ .session = &user->session(), - .customEmojiRepaint = [=] { name->update(); }, - }); + .repaint = [=] { name->update(); }, + })); name->resizeToNaturalWidth(available); name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); }, name->lifetime()); diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 799416844..6a5e27cf0 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "boxes/peer_list_widgets.h" #include "chat_helpers/stickers_gift_box_pack.h" -#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "core/ui_integration.h" // TextContext #include "data/data_channel.h" #include "data/data_credits.h" #include "data/data_session.h" @@ -747,9 +747,10 @@ rpl::producer BoostsController::totalBoostsValue() const { class CreditsRow final : public PeerListRow { public: struct Descriptor final { + not_null session; Data::CreditsHistoryEntry entry; Data::SubscriptionEntry subscription; - Core::MarkedTextContext context; + Ui::Text::MarkedContext context; int rowHeight = 0; Fn)> updateCallback; }; @@ -790,14 +791,14 @@ public: private: void init(); + const not_null _session; const Data::CreditsHistoryEntry _entry; const Data::SubscriptionEntry _subscription; - const Core::MarkedTextContext _context; + const Ui::Text::MarkedContext _context; const int _rowHeight; PaintRoundImageCallback _paintUserpicCallback; std::optional _rightLabel; - QString _title; QString _name; Ui::Text::String _description; @@ -813,6 +814,7 @@ CreditsRow::CreditsRow( not_null peer, const Descriptor &descriptor) : PeerListRow(peer, UniqueRowIdFromEntry(descriptor.entry)) +, _session(descriptor.session) , _entry(descriptor.entry) , _subscription(descriptor.subscription) , _context(descriptor.context) @@ -838,6 +840,7 @@ CreditsRow::CreditsRow( CreditsRow::CreditsRow(const Descriptor &descriptor) : PeerListRow(UniqueRowIdFromEntry(descriptor.entry)) +, _session(descriptor.session) , _entry(descriptor.entry) , _subscription(descriptor.subscription) , _context(descriptor.context) @@ -850,9 +853,13 @@ void CreditsRow::init() { const auto name = !isSpecial ? PeerListRow::generateName() : Ui::GenerateEntryName(_entry).text; - _name = _entry.title.isEmpty() - ? name - : (!_entry.subscriptionUntil.isNull() && !isSpecial) + _name = _entry.paidMessagesCount + ? tr::lng_credits_paid_messages_fee( + tr::now, + lt_count, + _entry.paidMessagesCount) + : ((!_entry.subscriptionUntil.isNull() && !isSpecial) + || _entry.title.isEmpty()) ? name : _entry.title; setSkipPeerBadge(true); @@ -861,6 +868,8 @@ void CreditsRow::init() { tr::now, lt_count_decimal, _entry.floodSkip) + : _entry.paidMessagesCount + ? name : (!_entry.subscriptionUntil.isNull() && !_entry.title.isEmpty()) ? _entry.title : _entry.refunded @@ -897,19 +906,19 @@ void CreditsRow::init() { : _subscription.photoId; if (descriptionPhotoId) { _descriptionThumbnail = Ui::MakePhotoThumbnail( - _context.session->data().photo(descriptionPhotoId), + _session->data().photo(descriptionPhotoId), {}); _descriptionThumbnail->subscribeToUpdates([this] { const auto thumbnailSide = st::defaultTextStyle.font->height; _descriptionThumbnailCache = Images::Round( _descriptionThumbnail->image(thumbnailSide), ImageRoundRadius::Large); - if (_context.customEmojiRepaint) { - _context.customEmojiRepaint(); + if (_context.repaint) { + _context.repaint(); } }); } - auto &manager = _context.session->data().customEmojiManager(); + auto &manager = _session->data().customEmojiManager(); if (_entry) { constexpr auto kMinus = QChar(0x2212); _rightText.setMarkedText( @@ -925,9 +934,9 @@ void CreditsRow::init() { if (!_paintUserpicCallback) { _paintUserpicCallback = _entry.stargift ? Ui::GenerateGiftStickerUserpicCallback( - _context.session, + _session, _entry.bareGiftStickerId, - _context.customEmojiRepaint) + _context.repaint) : !isSpecial ? PeerListRow::generatePaintUserpicCallback(false) : Ui::GenerateCreditsPaintUserpicCallback(_entry); @@ -943,11 +952,7 @@ const Data::SubscriptionEntry &CreditsRow::subscription() const { } QString CreditsRow::generateName() { - return (!_entry.title.isEmpty() && !_entry.subscriptionUntil.isNull()) - ? _name - : _entry.title.isEmpty() - ? _name - : _entry.title; + return _name; } PaintRoundImageCallback CreditsRow::generatePaintUserpicCallback(bool force) { @@ -1109,7 +1114,7 @@ private: Api::CreditsHistory _api; Data::CreditsStatusSlice _firstSlice; Data::CreditsStatusSlice::OffsetToken _apiToken; - Core::MarkedTextContext _context; + Ui::Text::MarkedContext _context; rpl::variable _allLoaded = false; bool _requesting = false; @@ -1122,10 +1127,7 @@ CreditsController::CreditsController(CreditsDescriptor d) , _entryClickedCallback(std::move(d.entryClickedCallback)) , _api(d.peer, d.in, d.out) , _firstSlice(std::move(d.firstSlice)) -, _context(Core::MarkedTextContext{ - .session = _session, - .customEmojiRepaint = [] {}, -}) { +, _context(Core::TextContext({ .session = _session })) { PeerListController::setStyleOverrides(&st::creditsHistoryEntriesList); } @@ -1165,6 +1167,7 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) { const Data::CreditsHistoryEntry &i, const Data::SubscriptionEntry &s) { const auto descriptor = CreditsRow::Descriptor{ + .session = &session(), .entry = i, .subscription = s, .context = _context, diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp index 2f8b1d069..1a5da3a0d 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp @@ -87,10 +87,10 @@ MessagePreview::MessagePreview( st::defaultPeerListItem.nameStyle, item->toPreview({ .generateImages = false }).text, Ui::DialogTextOptions(), - Core::MarkedTextContext{ + Core::TextContext({ .session = &item->history()->session(), - .customEmojiRepaint = [=] { update(); }, - }); + .repaint = [=] { update(); }, + })); if (item->media() && item->media()->hasSpoiler()) { _spoiler = std::make_unique([=] { update(); }); } @@ -131,10 +131,10 @@ MessagePreview::MessagePreview( st::defaultPeerListItem.nameStyle, { tr::lng_in_dlg_story(tr::now) }, Ui::DialogTextOptions(), - Core::MarkedTextContext{ + Core::TextContext({ .session = &story->peer()->session(), - .customEmojiRepaint = [=] { update(); }, - }); + .repaint = [=] { update(); }, + })); if (_preview.isNull()) { if (const auto photo = story->photo()) { _photoMedia = photo->createMediaView(); diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp index 280b5c1b6..572809145 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp @@ -104,7 +104,7 @@ void PreviewPainter::setDocument( } if (_player) { _player->setRepaintCallback(updateCallback); - } else { + } else if (updateCallback) { updateCallback(); } }, _lifetime); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 05e094f3e..99f69d6f7 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "core/local_url_handlers.h" #include "core/shortcuts.h" +#include "core/ui_integration.h" // TextContext #include "data/components/location_pickers.h" #include "data/data_bot_app.h" #include "data/data_changes.h" @@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_stickers.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_item_helpers.h" #include "info/bot/starref/info_bot_starref_common.h" // MakePeerBubbleButton #include "info/profile/info_profile_values.h" #include "inline_bots/inline_bot_result.h" @@ -383,21 +385,16 @@ void FillBotUsepic( not_null box, not_null bot, base::weak_ptr weak) { - auto arrow = Ui::Text::SingleCustomEmoji( - bot->owner().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); auto aboutLabel = Ui::CreateLabelWithCustomEmoji( box->verticalLayout(), tr::lng_allow_bot_webview_details( lt_emoji, - rpl::single(std::move(arrow)), + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), Ui::Text::RichLangValue ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link(std::move(text), u"internal:"_q); }), - { .session = &bot->session() }, + Core::TextContext({ .session = &bot->session() }), st::defaultFlatLabel); const auto userpic = Ui::CreateChild( box->verticalLayout(), @@ -449,12 +446,6 @@ std::unique_ptr MakeEmojiSetStatusPreview( not_null parent, not_null peer, not_null document) { - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = &peer->session(), - .customEmojiRepaint = update, - }; - }; const auto emoji = Ui::CreateChild>( parent, object_ptr( @@ -467,8 +458,9 @@ std::unique_ptr MakeEmojiSetStatusPreview( : QString()))), st::botEmojiStatusEmoji, st::defaultPopupMenu, - makeContext), + Core::TextContext({ .session = &peer->session() })), style::margins(st::normalFont->spacew, 0, 0, 0)); + emoji->entity()->resizeToWidth(emoji->entity()->textMaxWidth()); auto result = Info::BotStarRef::MakePeerBubbleButton( parent, @@ -1024,12 +1016,12 @@ void WebViewInstance::resolveApp( void WebViewInstance::confirmOpen(Fn done) { if (_bot->isVerified() - || _session->local().isBotTrustedOpenWebView(_bot->id)) { + || _session->local().isPeerTrustedOpenWebView(_bot->id)) { done(); return; } const auto callback = [=](Fn close) { - _session->local().markBotTrustedOpenWebView(_bot->id); + _session->local().markPeerTrustedOpenWebView(_bot->id); close(); done(); }; @@ -1061,14 +1053,14 @@ void WebViewInstance::confirmAppOpen( bool forceConfirmation) { if (!forceConfirmation && (_bot->isVerified() - || _session->local().isBotTrustedOpenWebView(_bot->id))) { + || _session->local().isPeerTrustedOpenWebView(_bot->id))) { done(writeAccess); return; } _parentShow->show(Box([=](not_null box) { const auto allowed = std::make_shared(); const auto callback = [=](Fn close) { - _session->local().markBotTrustedOpenWebView(_bot->id); + _session->local().markPeerTrustedOpenWebView(_bot->id); done((*allowed) && (*allowed)->checked()); close(); }; @@ -1816,11 +1808,15 @@ void WebViewInstance::botSendPreparedMessage( QPointer preview; QPointer choose; rpl::event_stream> recipient; + Fn send; + SendPaymentHelper sendPayment; bool sent = false; }; const auto state = std::make_shared(); auto recipient = state->recipient.events(); - const auto send = [=](std::vector> list) { + const auto send = [=]( + std::vector> list, + Api::SendOptions options) { if (state->sent) { return; } @@ -1859,7 +1855,7 @@ void WebViewInstance::botSendPreparedMessage( bot->session().api().sendInlineResult( bot, parsed.get(), - Api::SendAction(thread), + Api::SendAction(thread, options), std::nullopt, done); } @@ -1888,7 +1884,33 @@ void WebViewInstance::botSendPreparedMessage( state->choose = box.data(); panel->showBox(std::move(box)); }, [=](not_null thread) { - send({ thread }); + const auto weak = base::make_weak(thread); + state->send = [=](Api::SendOptions options) { + const auto strong = weak.get(); + if (!strong) { + state->send = nullptr; + return; + } + const auto withPaymentApproved = [=](int stars) { + if (const auto onstack = state->send) { + auto copy = options; + copy.starsApproved = stars; + onstack(copy); + } + }; + const auto checked = state->sendPayment.check( + uiShow(), + strong->peer(), + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + state->send = nullptr; + send({ strong }, options); + }; + state->send({}); }); box->boxClosing() | rpl::start_with_next([=] { if (!state->sent) { @@ -2490,18 +2512,46 @@ void ChooseAndSendLocation( not_null controller, const Ui::LocationPickerConfig &config, Api::SendAction action) { + const auto weak = base::make_weak(controller); const auto session = &controller->session(); if (const auto picker = session->locationPickers().lookup(action)) { picker->activate(); return; } - const auto callback = [=](Data::InputVenue venue) { + struct State { + SendPaymentHelper sendPayment; + Fn send; + }; + const auto state = std::make_shared(); + state->send = [=](Data::InputVenue venue, Api::SendAction action) { + if (const auto strong = weak.get()) { + const auto withPaymentApproved = [=](int stars) { + if (const auto onstack = state->send) { + auto copy = action; + copy.options.starsApproved = stars; + onstack(venue, copy); + } + }; + const auto checked = state->sendPayment.check( + strong, + action.history->peer, + 1, + action.options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + } + state->send = nullptr; if (venue.justLocation()) { Api::SendLocation(action, venue.lat, venue.lon); } else { Api::SendVenue(action, venue); } }; + const auto callback = [=](Data::InputVenue venue) { + state->send(venue, action); + }; const auto picker = Ui::LocationPicker::Show({ .parent = controller->widget(), .config = config, @@ -2552,7 +2602,8 @@ std::unique_ptr MakeAttachBotsMenu( const auto source = action.options.scheduled ? Api::SendType::Scheduled : Api::SendType::Normal; - const auto sendMenuType = action.replyTo.topicRootId + const auto sendMenuType = (action.replyTo.topicRootId + || action.history->peer->starsPerMessageChecked()) ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); @@ -2576,7 +2627,8 @@ std::unique_ptr MakeAttachBotsMenu( ChooseAndSendLocation(controller, config, actionFactory()); }, &st::menuIconAddress); } - const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline); + const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline) + && !peer->starsPerMessageChecked(); for (const auto &bot : bots->attachBots()) { if (!addBots || !bot.inAttachMenu diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index 616280a7e..7f6f41581 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -49,8 +49,8 @@ constexpr auto kMaxInlineArea = 1280 * 720; return dimensions.width() * dimensions.height() <= kMaxInlineArea; } -FileBase::FileBase(not_null context, not_null result) -: ItemBase(context, result) { +FileBase::FileBase(not_null context, std::shared_ptr result) +: ItemBase(context, std::move(result)) { } FileBase::FileBase( @@ -95,8 +95,8 @@ int FileBase::content_duration() const { return getResultDuration(); } -Gif::Gif(not_null context, not_null result) -: FileBase(context, result) { +Gif::Gif(not_null context, std::shared_ptr result) +: FileBase(context, std::move(result)) { Expects(getResultDocument() != nullptr); } @@ -442,8 +442,8 @@ void Gif::clipCallback(Media::Clip::Notification notification) { } } -Sticker::Sticker(not_null context, not_null result) -: FileBase(context, result) { +Sticker::Sticker(not_null context, std::shared_ptr result) +: FileBase(context, std::move(result)) { Expects(getResultDocument() != nullptr); } @@ -654,8 +654,8 @@ void Sticker::clipCallback(Media::Clip::Notification notification) { update(); } -Photo::Photo(not_null context, not_null result) -: ItemBase(context, result) { +Photo::Photo(not_null context, std::shared_ptr result) +: ItemBase(context, std::move(result)) { Expects(getShownPhoto() != nullptr); } @@ -769,8 +769,8 @@ void Photo::prepareThumbnail(QSize size, QSize frame) const { validateThumbnail(_photoMedia->thumbnailInline(), size, frame, false); } -Video::Video(not_null context, not_null result) -: FileBase(context, result) +Video::Video(not_null context, std::shared_ptr result) +: FileBase(context, std::move(result)) , _link(getResultPreviewHandler()) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { @@ -925,11 +925,11 @@ void CancelFileClickHandler::onClickImpl() const { _result->cancelFile(); } -File::File(not_null context, not_null result) -: FileBase(context, result) +File::File(not_null context, std::shared_ptr result) +: FileBase(context, std::move(result)) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineFileSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineFileSize - st::inlineThumbSkip) -, _cancel(std::make_shared(result)) +, _cancel(std::make_shared(_result.get())) , _document(getShownDocument()) { Expects(getResultDocument() != nullptr); @@ -1173,8 +1173,8 @@ void File::setStatusSize( } } -Contact::Contact(not_null context, not_null result) -: ItemBase(context, result) +Contact::Contact(not_null context, std::shared_ptr result) +: ItemBase(context, std::move(result)) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { } @@ -1265,16 +1265,16 @@ void Contact::prepareThumbnail(int width, int height) const { Article::Article( not_null context, - not_null result, + std::shared_ptr result, bool withThumb) -: ItemBase(context, result) +: ItemBase(context, std::move(result)) , _url(getResultUrlHandler()) , _link(getResultPreviewHandler()) , _withThumb(withThumb) , _title(st::emojiPanWidth / 2) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { if (!_link) { - if (const auto point = result->getLocationPoint()) { + if (const auto point = _result->getLocationPoint()) { _link = std::make_shared(*point); } } @@ -1300,7 +1300,7 @@ void Article::initDimensions() { _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; } -int32 Article::resizeGetHeight(int32 width) { +int Article::resizeGetHeight(int width) { _width = qMin(width, _maxw); if (_url) { _urlText = getResultUrl(); @@ -1422,8 +1422,8 @@ void Article::prepareThumbnail(int width, int height) const { }); } -Game::Game(not_null context, not_null result) -: ItemBase(context, result) +Game::Game(not_null context, std::shared_ptr result) +: ItemBase(context, std::move(result)) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { countFrameSize(); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h index 83794b1b6..e483f858a 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h @@ -29,7 +29,7 @@ namespace internal { class FileBase : public ItemBase { public: - FileBase(not_null context, not_null result); + FileBase(not_null context, std::shared_ptr result); // For saved gif layouts. FileBase(not_null context, not_null document); @@ -58,7 +58,7 @@ private: class Gif final : public FileBase { public: - Gif(not_null context, not_null result); + Gif(not_null context, std::shared_ptr result); Gif( not_null context, not_null document, @@ -138,7 +138,7 @@ private: class Photo : public ItemBase { public: - Photo(not_null context, not_null result); + Photo(not_null context, std::shared_ptr result); // Not used anywhere currently. //Photo(not_null context, not_null photo); @@ -178,7 +178,7 @@ private: class Sticker : public FileBase { public: - Sticker(not_null context, not_null result); + Sticker(not_null context, std::shared_ptr result); ~Sticker(); // Not used anywhere currently. //Sticker(not_null context, not_null document); @@ -229,7 +229,7 @@ private: class Video : public FileBase { public: - Video(not_null context, not_null result); + Video(not_null context, std::shared_ptr result); void initDimensions() override; @@ -269,7 +269,7 @@ private: class File : public FileBase { public: - File(not_null context, not_null result); + File(not_null context, std::shared_ptr result); ~File(); void initDimensions() override; @@ -347,7 +347,7 @@ private: class Contact : public ItemBase { public: - Contact(not_null context, not_null result); + Contact(not_null context, std::shared_ptr result); void initDimensions() override; @@ -366,7 +366,10 @@ private: class Article : public ItemBase { public: - Article(not_null context, not_null result, bool withThumb); + Article( + not_null context, + std::shared_ptr result, + bool withThumb); void initDimensions() override; int resizeGetHeight(int width) override; @@ -391,7 +394,7 @@ private: class Game : public ItemBase { public: - Game(not_null context, not_null result); + Game(not_null context, std::shared_ptr result); void setPosition(int32 position) override; void initDimensions() override; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp index 34acc4093..89b0e41b1 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp @@ -29,7 +29,7 @@ base::NeverFreedPointer documentItemsMap; } // namespace -Result *ItemBase::getResult() const { +std::shared_ptr ItemBase::getResult() const { return _result; } @@ -92,33 +92,37 @@ void ItemBase::layoutChanged() { std::unique_ptr ItemBase::createLayout( not_null context, - not_null result, + std::shared_ptr result, bool forceThumb) { using Type = Result::Type; switch (result->_type) { case Type::Photo: - return std::make_unique(context, result); + return std::make_unique(context, std::move(result)); case Type::Audio: case Type::File: - return std::make_unique(context, result); + return std::make_unique(context, std::move(result)); case Type::Video: - return std::make_unique(context, result); + return std::make_unique(context, std::move(result)); case Type::Sticker: - return std::make_unique(context, result); + return std::make_unique( + context, + std::move(result)); case Type::Gif: - return std::make_unique(context, result); + return std::make_unique(context, std::move(result)); case Type::Article: case Type::Geo: case Type::Venue: return std::make_unique( context, - result, + std::move(result), forceThumb); case Type::Game: - return std::make_unique(context, result); + return std::make_unique(context, std::move(result)); case Type::Contact: - return std::make_unique(context, result); + return std::make_unique( + context, + std::move(result)); } return nullptr; } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h index 67574f7e0..e370b3477 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h @@ -59,7 +59,7 @@ public: class ItemBase : public LayoutItemBase { public: - ItemBase(not_null context, not_null result) + ItemBase(not_null context, std::shared_ptr result) : _result(result) , _context(context) { } @@ -80,7 +80,7 @@ public: return false; } - Result *getResult() const; + std::shared_ptr getResult() const; DocumentData *getDocument() const; PhotoData *getPhoto() const; @@ -112,7 +112,7 @@ public: static std::unique_ptr createLayout( not_null context, - not_null result, + std::shared_ptr result, bool forceThumb); static std::unique_ptr createLayoutGif( not_null context, @@ -135,7 +135,7 @@ protected: } Data::FileOrigin fileOrigin() const; - Result *_result = nullptr; + std::shared_ptr _result; DocumentData *_document = nullptr; PhotoData *_photo = nullptr; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index e5e1566d5..0b5c01cd3 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -53,7 +53,7 @@ Result::Result(not_null session, const Creator &creator) , _type(creator.type) { } -std::unique_ptr Result::Create( +std::shared_ptr Result::Create( not_null session, uint64 queryId, const MTPBotInlineResult &data) { @@ -84,7 +84,7 @@ std::unique_ptr Result::Create( return nullptr; } - auto result = std::make_unique( + auto result = std::make_shared( session, Creator{ queryId, type }); const auto message = data.match([&](const MTPDbotInlineResult &data) { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index 67b3a0fd1..bf40bc2ee 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -43,7 +43,7 @@ public: // You should use create() static method instead. Result(not_null session, const Creator &creator); - static std::unique_ptr Create( + static std::shared_ptr Create( not_null session, uint64 queryId, const MTPBotInlineResult &mtpData); @@ -130,7 +130,7 @@ private: }; struct ResultSelected { - not_null result; + std::shared_ptr result; not_null bot; PeerData *recipientOverride = nullptr; Api::SendOptions options; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp index 29f73dc77..971c49858 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp @@ -331,7 +331,7 @@ void Inner::selectInlineResult( if (const auto inlineResult = item->getResult()) { if (inlineResult->onChoose(item)) { _resultSelectedCallback({ - .result = inlineResult, + .result = std::move(inlineResult), .bot = _inlineBot, .options = std::move(options), .messageSendingFrom = messageSendingFrom(), @@ -448,11 +448,15 @@ void Inner::clearInlineRows(bool resultsDeleted) { _mosaic.clearRows(resultsDeleted); } -ItemBase *Inner::layoutPrepareInlineResult(Result *result) { - auto it = _inlineLayouts.find(result); +ItemBase *Inner::layoutPrepareInlineResult(std::shared_ptr result) { + const auto raw = result.get(); + auto it = _inlineLayouts.find(raw); if (it == _inlineLayouts.cend()) { - if (auto layout = ItemBase::createLayout(this, result, _inlineWithThumb)) { - it = _inlineLayouts.emplace(result, std::move(layout)).first; + if (auto layout = ItemBase::createLayout( + this, + std::move(result), + _inlineWithThumb)) { + it = _inlineLayouts.emplace(raw, std::move(layout)).first; it->second->initDimensions(); } else { return nullptr; @@ -560,8 +564,8 @@ int Inner::refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntr const auto resultItems = entry->results | ranges::views::slice( from, count - ) | ranges::views::transform([&](const std::unique_ptr &r) { - return layoutPrepareInlineResult(r.get()); + ) | ranges::views::transform([&](const std::shared_ptr &r) { + return layoutPrepareInlineResult(r); }) | ranges::views::filter([](const ItemBase *item) { return item != nullptr; }) | ranges::to>>; @@ -585,7 +589,7 @@ int Inner::validateExistingInlineRows(const Results &results) { const auto until = _mosaic.validateExistingRows([&]( not_null item, int untilIndex) { - return item->getResult() != results[untilIndex].get(); + return item->getResult().get() != results[untilIndex].get(); }, results.size()); if (_mosaic.empty()) { diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.h b/Telegram/SourceFiles/inline_bots/inline_results_inner.h index 28b305513..e7d47a30a 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.h +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.h @@ -50,7 +50,7 @@ namespace InlineBots { namespace Layout { class ItemBase; -using Results = std::vector>; +using Results = std::vector>; struct CacheEntry { QString nextOffset; @@ -135,7 +135,7 @@ private: void updateInlineItems(); void repaintItems(crl::time now = 0); void clearInlineRows(bool resultsDeleted); - ItemBase *layoutPrepareInlineResult(Result *result); + ItemBase *layoutPrepareInlineResult(std::shared_ptr result); void updateRestrictedLabelGeometry(); void deleteUnusedInlineLayouts(); diff --git a/Telegram/SourceFiles/intro/intro_step.cpp b/Telegram/SourceFiles/intro/intro_step.cpp index 2e30d3989..e37b6a314 100644 --- a/Telegram/SourceFiles/intro/intro_step.cpp +++ b/Telegram/SourceFiles/intro/intro_step.cpp @@ -108,13 +108,7 @@ Step::Step( text.entities, EntityType::Spoiler, &EntityInText::type); - if (hasSpoiler) { - label->setMarkedText( - text, - CommonTextContext{ [=] { label->update(); } }); - } else { - label->setMarkedText(text); - } + label->setMarkedText(text); label->setAttribute(Qt::WA_TransparentForMouseEvents, hasSpoiler); updateLabelsPosition(); }, lifetime()); diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp index 9a4190f34..5a2f894ed 100644 --- a/Telegram/SourceFiles/main/main_account.cpp +++ b/Telegram/SourceFiles/main/main_account.cpp @@ -174,7 +174,8 @@ void Account::createSession( MTPPeerColor(), // color MTPPeerColor(), // profile_color MTPint(), // bot_active_users - MTPlong()), // bot_verification_icon + MTPlong(), // bot_verification_icon + MTPlong()), // send_paid_messages_stars serialized, streamVersion, std::move(settings)); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index c26d57126..4326d7559 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -73,6 +73,26 @@ int AppConfig::starrefCommissionMax() const { return get(u"starref_max_commission_permille"_q, 900); } +float64 AppConfig::starsWithdrawRate() const { + return get(u"stars_usd_withdraw_rate_x1000"_q, 1300) / 1000.; +} + +bool AppConfig::paidMessagesAvailable() const { + return get(u"stars_paid_messages_available"_q, false); +} + +int AppConfig::paidMessageStarsMax() const { + return get(u"stars_paid_message_amount_max"_q, 10'000); +} + +int AppConfig::paidMessageCommission() const { + return get(u"stars_paid_message_commission_permille"_q, 850); +} + +int AppConfig::pinnedGiftsLimit() const { + return get(u"stargifts_pinned_to_top_limit"_q, 6); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 3092aa567..308656bbe 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -72,6 +72,13 @@ public: [[nodiscard]] int starrefCommissionMin() const; [[nodiscard]] int starrefCommissionMax() const; + [[nodiscard]] float64 starsWithdrawRate() const; + [[nodiscard]] bool paidMessagesAvailable() const; + [[nodiscard]] int paidMessageStarsMax() const; + [[nodiscard]] int paidMessageCommission() const; + + [[nodiscard]] int pinnedGiftsLimit() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp b/Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp index c8b90f798..7d9f99400 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp @@ -44,7 +44,13 @@ constexpr auto kFrameSize = 4096; return {}; } + +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 79, 100) auto inCodec = (const AVCodec*)nullptr; +#else + auto inCodec = (AVCodec*)nullptr; +#endif + const auto streamId = av_find_best_stream( input.get(), AVMEDIA_TYPE_AUDIO, @@ -152,10 +158,10 @@ constexpr auto kFrameSize = 4096; inCodecContext->sample_rate, &outCodecContext->ch_layout, #else // DA_FFMPEG_NEW_CHANNEL_LAYOUT - &inCodecContext->channel_layout, + inCodecContext->channel_layout, inCodecContext->sample_fmt, inCodecContext->sample_rate, - &outCodecContext->channel_layout, + outCodecContext->channel_layout, #endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT outCodecContext->sample_fmt, outCodecContext->sample_rate); diff --git a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp index f3c4a112f..218add340 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp @@ -30,10 +30,9 @@ CaptionFullView::CaptionFullView(not_null controller) object_ptr(_scroll.get(), st::storiesCaptionFull), st::mediaviewCaptionPadding + _controller->repostCaptionPadding()))) , _text(_wrap->entity()) { - _text->setMarkedText(controller->captionText(), Core::MarkedTextContext{ + _text->setMarkedText(controller->captionText(), Core::TextContext({ .session = &controller->uiShow()->session(), - .customEmojiRepaint = [=] { _text->update(); }, - }); + })); startAnimation(); _controller->layoutValue( diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 7277109c3..a77c700b2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -403,10 +403,7 @@ void Header::show(HeaderData data) { const auto prefix = data.fromPeer ? data.fromPeer : data.repostPeer; _repost->setMarkedText( (prefix ? Ui::Text::Link(prefixName) : prefixName), - Core::MarkedTextContext{ - .session = &data.peer->session(), - .customEmojiRepaint = [=] { _repost->update(); }, - }); + Core::TextContext({ .session = &data.peer->session() })); if (prefix) { _repost->setClickHandlerFilter([=](const auto &...) { _controller->uiShow()->show(PrepareShortInfoBox(prefix)); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 09e222737..21ae90050 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -15,11 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/premium_limits_box.h" #include "boxes/send_files_box.h" +#include "boxes/share_box.h" // ShareBoxStyleOverrides #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/tabbed_selector.h" #include "core/file_utilities.h" #include "core/mime_type.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_changes.h" #include "data/data_chat_participant_status.h" #include "data/data_document.h" #include "data/data_message_reaction_id.h" @@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "history/view/controls/compose_controls_common.h" #include "history/view/controls/history_view_compose_controls.h" +#include "history/view/history_view_schedule_box.h" // ScheduleBoxStyleArgs #include "history/history_item_helpers.h" #include "history/history.h" #include "inline_bots/inline_bot_result.h" @@ -36,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_stealth.h" #include "menu/menu_send.h" +#include "settings/settings_credits_graphics.h" // DarkCreditsEntryBoxStyle #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/storage_media_prepare.h" @@ -52,14 +56,19 @@ namespace { [[nodiscard]] rpl::producer PlaceholderText( const std::shared_ptr &show, - rpl::producer isComment) { + rpl::producer isComment, + rpl::producer starsPerMessage) { return rpl::combine( show->session().data().stories().stealthModeValue(), - std::move(isComment) - ) | rpl::map([](Data::StealthMode value, bool isComment) { - return std::tuple(value.enabledTill, isComment); + std::move(isComment), + std::move(starsPerMessage) + ) | rpl::map([]( + Data::StealthMode value, + bool isComment, + int starsPerMessage) { + return std::tuple(value.enabledTill, isComment, starsPerMessage); }) | rpl::distinct_until_changed( - ) | rpl::map([](TimeId till, bool isComment) { + ) | rpl::map([](TimeId till, bool isComment, int starsPerMessage) { return rpl::single( rpl::empty ) | rpl::then( @@ -71,7 +80,13 @@ namespace { }) | rpl::then( rpl::single(0) ) | rpl::map([=](TimeId left) { - return left + return starsPerMessage + ? tr::lng_message_paid_ph( + lt_amount, + tr::lng_prize_credits_amount( + lt_count, + rpl::single(starsPerMessage * 1.))) + : left ? tr::lng_stealth_mode_countdown( lt_left, rpl::single(TimeLeftText(left))) @@ -128,7 +143,8 @@ ReplyArea::ReplyArea(not_null controller) .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), .customPlaceholder = PlaceholderText( _controller->uiShow(), - rpl::deferred([=] { return _isComment.value(); })), + rpl::deferred([=] { return _isComment.value(); }), + rpl::deferred([=] { return _starsForMessage.value(); })), .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), .voiceLockFromBottom = true, .features = { @@ -199,7 +215,7 @@ bool ReplyArea::sendReaction(const Data::ReactionId &id) { } } return !message.textWithTags.empty() - && send(std::move(message), {}, true); + && send(std::move(message), true); } void ReplyArea::send(Api::SendOptions options) { @@ -209,29 +225,45 @@ void ReplyArea::send(Api::SendOptions options) { message.textWithTags = _controls->getTextWithAppliedMarkdown(); message.webPage = webPageDraft; - send(std::move(message), options); + send(std::move(message)); } bool ReplyArea::send( Api::MessageToSend message, - Api::SendOptions options, bool skipToast) { - if (!options.scheduled && showSlowmodeError()) { + if (!message.action.options.scheduled && showSlowmodeError()) { return false; } - const auto error = GetErrorForSending( - _data.peer, - { - .topicRootId = MsgId(0), - .text = &message.textWithTags, - .ignoreSlowmodeCountdown = (options.scheduled != 0), - }); + auto request = SendingErrorRequest{ + .topicRootId = MsgId(0), + .text = &message.textWithTags, + .ignoreSlowmodeCountdown = (message.action.options.scheduled != 0), + }; + request.messagesCount = ComputeSendingMessagesCount( + message.action.history, + request); + const auto error = GetErrorForSending(_data.peer, request); if (error) { Data::ShowSendErrorToast(_controller->uiShow(), _data.peer, error); return false; } + if (!message.action.options.scheduled) { + const auto withPaymentApproved = [=](int approved) { + auto copy = message; + copy.action.options.starsApproved = approved; + send(copy); + }; + const auto checked = checkSendPayment( + request.messagesCount, + message.action.options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } + } + session().api().sendMessage(std::move(message)); finishSending(skipToast); @@ -239,7 +271,40 @@ bool ReplyArea::send( return true; } -void ReplyArea::sendVoice(VoiceToSend &&data) { +bool ReplyArea::checkSendPayment( + int messagesCount, + int starsApproved, + Fn withPaymentApproved) { + const auto st1 = ::Settings::DarkCreditsEntryBoxStyle(); + const auto st2 = st1.shareBox.get(); + const auto st3 = st2 ? st2->scheduleBox.get() : nullptr; + return _data.peer + && _sendPayment.check( + _controller->uiShow(), + _data.peer, + messagesCount, + starsApproved, + std::move(withPaymentApproved), + { + .label = st3 ? st3->chooseDateTimeArgs.labelStyle : nullptr, + .checkbox = st2 ? st2->checkbox : nullptr, + }); +} + +void ReplyArea::sendVoice(const VoiceToSend &data) { + const auto withPaymentApproved = [=](int approved) { + auto copy = data; + copy.options.starsApproved = approved; + sendVoice(copy); + }; + const auto checked = checkSendPayment( + 1, + data.options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + auto action = prepareSendAction(data.options); session().api().sendVoiceMessage( data.bytes, @@ -269,6 +334,18 @@ bool ReplyArea::sendExistingDocument( || Window::ShowSendPremiumError(show, document)) { return false; } + const auto withPaymentApproved = [=](int approved) { + auto copy = messageToSend; + copy.action.options.starsApproved = approved; + sendExistingDocument(document, std::move(copy), localId); + }; + const auto checked = checkSendPayment( + 1, + messageToSend.action.options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } Api::SendExistingDocument(std::move(messageToSend), document, localId); @@ -296,6 +373,18 @@ bool ReplyArea::sendExistingPhoto( } else if (showSlowmodeError()) { return false; } + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendExistingPhoto(photo, copy); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return false; + } Api::SendExistingPhoto( Api::MessageToSend(prepareSendAction(options)), @@ -307,7 +396,7 @@ bool ReplyArea::sendExistingPhoto( } void ReplyArea::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot) { if (const auto error = result->getErrorOnSend(history())) { const auto show = _controller->uiShow(); @@ -318,13 +407,30 @@ void ReplyArea::sendInlineResult( } void ReplyArea::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options, std::optional localMessageId) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendInlineResult(result, bot, copy, localMessageId); + }; + const auto checked = checkSendPayment( + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + auto action = prepareSendAction(options); action.generateLocal = true; - session().api().sendInlineResult(bot, result, action, localMessageId); + session().api().sendInlineResult( + bot, + result.get(), + action, + localMessageId); auto &bots = cRefRecentInlineBots(); const auto index = bots.indexOf(bot); @@ -560,25 +666,47 @@ void ReplyArea::sendingFilesConfirmed( std::move(list), way, _data.peer->slowmodeApplied()); - const auto type = way.sendImagesAsPhotos() - ? SendMediaType::Photo - : SendMediaType::File; + auto bundle = PrepareFilesBundle( + std::move(groups), + way, + std::move(caption), + ctrlShiftEnter); + sendingFilesConfirmed(std::move(bundle), options); +} + +void ReplyArea::sendingFilesConfirmed( + std::shared_ptr bundle, + Api::SendOptions options) { + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + sendingFilesConfirmed(bundle, copy); + }; + const auto checked = checkSendPayment( + bundle->totalCount, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if ((groups.size() != 1 || !groups.front().sentWithCaption()) - && !caption.text.isEmpty()) { + if (bundle->sendComment) { auto message = Api::MessageToSend(action); - message.textWithTags = base::take(caption); + message.textWithTags = base::take(bundle->caption); session().api().sendMessage(std::move(message)); } - for (auto &group : groups) { + for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; session().api().sendFiles( std::move(group.list), type, - base::take(caption), + base::take(bundle->caption), album, action); } @@ -614,8 +742,8 @@ void ReplyArea::initActions() { }, _lifetime); _controls->sendVoiceRequests( - ) | rpl::start_with_next([=](VoiceToSend &&data) { - sendVoice(std::move(data)); + ) | rpl::start_with_next([=](const VoiceToSend &data) { + sendVoice(data); }, _lifetime); _controls->attachRequests( @@ -693,6 +821,16 @@ void ReplyArea::show( _controls->clear(); } return; + } else if (const auto peer = _data.peer) { + using Flag = Data::PeerUpdate::Flag; + _starsForMessage = peer->session().changes().peerFlagsValue( + peer, + Flag::StarsPerMessage | Flag::FullInfo + ) | rpl::map([=] { + return peer->starsPerMessageChecked(); + }); + } else { + _starsForMessage = 0; } invalidate_weak_ptrs(&_shownPeerGuard); const auto peer = data.peer; @@ -705,7 +843,7 @@ void ReplyArea::show( using namespace HistoryView::Controls; return (can || !user - || !user->meRequiresPremiumToWrite() + || !user->requiresPremiumToWrite() || user->session().premium()) ? WriteRestriction() : WriteRestriction{ diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 981cc48c1..bb7fe15f0 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/weak_ptr.h" +#include "history/history_item_helpers.h" class History; enum class SendMediaType; @@ -44,6 +45,7 @@ struct Details; namespace Ui { struct PreparedList; +struct PreparedBundle; class SendFilesWay; class RpWidget; } // namespace Ui @@ -90,9 +92,13 @@ private: bool send( Api::MessageToSend message, - Api::SendOptions options, bool skipToast = false); + [[nodiscard]] bool checkSendPayment( + int messagesCount, + int starsApproved, + Fn withPaymentApproved); + void uploadFile(const QByteArray &fileContent, SendMediaType type); bool confirmSendingFiles( QImage &&image, @@ -116,6 +122,9 @@ private: TextWithTags &&caption, Api::SendOptions options, bool ctrlShiftEnter); + void sendingFilesConfirmed( + std::shared_ptr bundle, + Api::SendOptions options); void finishSending(bool skipToast = false); bool sendExistingDocument( @@ -127,10 +136,10 @@ private: not_null photo, Api::SendOptions options); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options, std::optional localMessageId); @@ -141,7 +150,7 @@ private: [[nodiscard]] Api::SendAction prepareSendAction( Api::SendOptions options) const; void send(Api::SendOptions options); - void sendVoice(VoiceToSend &&data); + void sendVoice(const VoiceToSend &data); void chooseAttach(std::optional overrideSendImagesAsPhotos); [[nodiscard]] Fn sendMenuDetails() const; @@ -151,6 +160,7 @@ private: const not_null _controller; rpl::variable _isComment; + rpl::variable _starsForMessage; const std::unique_ptr _controls; std::unique_ptr _cant; @@ -160,6 +170,8 @@ private: bool _chooseAttachRequest = false; rpl::variable _choosingAttach; + SendPaymentHelper _sendPayment; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp index 05d7b588b..eb50a06fc 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp @@ -248,19 +248,18 @@ void RepostView::recountDimensions() { auto nameFull = TextWithEntities(); nameFull.append(HistoryView::Reply::PeerEmoji(owner, _sourcePeer)); nameFull.append(name); - auto context = Core::MarkedTextContext{ + auto context = Core::TextContext({ .session = &_story->session(), - .customEmojiRepaint = [] {}, .customEmojiLoopLimit = 1, - }; + }); _name.setMarkedText( st::semiboldTextStyle, nameFull, Ui::NameTextOptions(), context); - context.customEmojiRepaint = crl::guard(this, [=] { + context.repaint = crl::guard(this, [=] { _controller->repaint(); - }), + }); _text.setMarkedText( st::defaultTextStyle, text, diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 67b1dc925..80ec8e75b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -66,7 +66,7 @@ namespace Media::Stories { const auto state = std::make_shared(); auto filterCallback = [=](not_null thread) { if (const auto user = thread->peer()->asUser()) { - if (user->canSendIgnoreRequirePremium()) { + if (user->canSendIgnoreMoneyRestrictions()) { return true; } } @@ -76,8 +76,12 @@ namespace Media::Stories { auto copyLinkCallback = canCopyLink ? Fn(std::move(copyCallback)) : Fn(); + auto countMessagesCallback = [=](const TextWithTags &comment) { + return comment.text.isEmpty() ? 1 : 2; + }; auto submitCallback = [=]( std::vector> &&result, + Fn checkPaid, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions forwardOptions) { @@ -95,6 +99,8 @@ namespace Media::Stories { if (error.error) { show->showBox(MakeSendErrorBox(error, result.size() > 1)); return; + } else if (!checkPaid()) { + return; } const auto api = &story->owner().session().api(); @@ -111,25 +117,33 @@ namespace Media::Stories { const auto threadPeer = thread->peer(); const auto threadHistory = thread->owningHistory(); const auto randomId = base::RandomValue(); - auto sendFlags = MTPmessages_SendMedia::Flags(0); + using SendFlag = MTPmessages_SendMedia::Flag; + auto sendFlags = SendFlag(0) | SendFlag(0); if (action.replyTo) { - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; + sendFlags |= SendFlag::f_reply_to; } const auto silentPost = ShouldSendSilent(threadPeer, options); if (silentPost) { - sendFlags |= MTPmessages_SendMedia::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (options.scheduled) { - sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (options.shortcutId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + sendFlags |= SendFlag::f_quick_reply_shortcut; } if (options.effectId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + sendFlags |= SendFlag::f_effect; } if (options.invertCaption) { - sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; + sendFlags |= SendFlag::f_invert_media; + } + const auto starsPaid = std::min( + threadHistory->peer->starsPerMessageChecked(), + options.starsApproved); + if (starsPaid) { + options.starsApproved -= starsPaid; + sendFlags |= SendFlag::f_allow_paid_stars; } const auto done = [=] { if (!--state->requests) { @@ -155,7 +169,8 @@ namespace Media::Stories { MTP_int(options.scheduled), 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) { @@ -175,10 +190,11 @@ namespace Media::Stories { return Box(ShareBox::Descriptor{ .session = session, .copyCallback = std::move(copyLinkCallback), + .countMessagesCallback = std::move(countMessagesCallback), .submitCallback = std::move(submitCallback), .filterCallback = std::move(filterCallback), .st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(), - .premiumRequiredError = SharePremiumRequiredError(), + .moneyRestrictionError = ShareMessageMoneyRestrictionError(), }); } @@ -227,7 +243,7 @@ object_ptr PrepareShareAtTimeBox( const auto requiresInline = item->requiresSendInlineRight(); auto filterCallback = [=](not_null thread) { if (const auto user = thread->peer()->asUser()) { - if (user->canSendIgnoreRequirePremium()) { + if (user->canSendIgnoreMoneyRestrictions()) { return true; } } @@ -242,6 +258,9 @@ object_ptr PrepareShareAtTimeBox( return Box(ShareBox::Descriptor{ .session = session, .copyCallback = std::move(copyLinkCallback), + .countMessagesCallback = ShareBox::DefaultForwardCountMessages( + history, + { id }), .submitCallback = ShareBox::DefaultForwardCallback( show, history, @@ -257,7 +276,7 @@ object_ptr PrepareShareAtTimeBox( .captionsCount = ItemsForwardCaptionsCount({ item }), .show = !hasOnlyForcedForwardedInfo, }, - .premiumRequiredError = SharePremiumRequiredError(), + .moneyRestrictionError = ShareMessageMoneyRestrictionError(), }); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index bc35c68bd..4e73c0df5 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1519,6 +1519,9 @@ void OverlayWidget::refreshCaptionGeometry() { if (_caption.isEmpty() && (!_stories || !_stories->repost())) { _captionRect = QRect(); return; + } else if (_fullScreenVideo) { + _captionRect = QRect(); + return; } if (_groupThumbs && _groupThumbs->hiding()) { @@ -3352,12 +3355,12 @@ void OverlayWidget::refreshCaption() { } update(captionGeometry()); }; - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = (_stories ? _storiesSession : &_message->history()->session()), - .customEmojiRepaint = captionRepaint, - }; + .repaint = captionRepaint, + }); _caption.setMarkedText( st::mediaviewCaptionStyle, (base.isEmpty() @@ -6117,7 +6120,7 @@ void OverlayWidget::updateOver(QPoint pos) { auto textState = _saveMsgText.getState(pos - _saveMsg.topLeft() - QPoint(st::mediaviewSaveMsgPadding.left(), st::mediaviewSaveMsgPadding.top()), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right()); lnk = textState.link; lnkhost = this; - } else if (_captionRect.contains(pos)) { + } else if (_captionRect.contains(pos) && !_fullScreenVideo) { auto request = Ui::Text::StateRequestElided(); const auto lineHeight = st::mediaviewCaptionStyle.font->height; request.lines = _captionRect.height() / lineHeight; diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index 6f93a1012..7eef6e221 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -366,6 +366,7 @@ void PipPanel::init() { ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { // Workaround Qt's forced transient parent. Ui::Platform::ClearTransientParent(widget()); + Ui::Platform::SetWindowMargins(widget(), _padding); }, rp()->lifetime()); rp()->screenValue( @@ -882,6 +883,9 @@ void PipPanel::updateDecorations() { _padding = padding; _useTransparency = use; widget()->setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency); + if (widget()->windowHandle()) { + Ui::Platform::SetWindowMargins(widget(), _padding); + } setGeometry(newGeometry); update(); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index a1b51ed75..231da1ee0 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" +#include "core/ui_integration.h" // TextContext #include "data/components/sponsored_messages.h" #include "data/data_premium_limits.h" #include "data/data_session.h" @@ -191,11 +192,7 @@ void AboutBox( } Ui::AddSkip(content); { - const auto arrow = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); + const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); const auto available = box->width() - rect::m::sum::h(st::boxRowPadding); box->addRow( @@ -213,7 +210,7 @@ void AboutBox( return Ui::Text::Link(std::move(t), kUrl.utf16()); }), Ui::Text::RichLangValue), - { .session = session }, + Core::TextContext({ .session = session }), st::channelEarnLearnDescription))->resizeToWidth(available); } Ui::AddSkip(content); diff --git a/Telegram/SourceFiles/menu/menu_ttl_validator.cpp b/Telegram/SourceFiles/menu/menu_ttl_validator.cpp index 181c16786..b388e7016 100644 --- a/Telegram/SourceFiles/menu/menu_ttl_validator.cpp +++ b/Telegram/SourceFiles/menu/menu_ttl_validator.cpp @@ -113,7 +113,8 @@ bool TTLValidator::can() const { && !_peer->isSelf() && !_peer->isNotificationsUser() && !_peer->asUser()->isInaccessible() - && (!_peer->asUser()->meRequiresPremiumToWrite() + && !_peer->asUser()->starsPerMessage() + && (!_peer->asUser()->requiresPremiumToWrite() || _peer->session().premium())) || (_peer->isChat() && _peer->asChat()->canEditInformation() diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index e18220968..c48f0f224 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/131.0.0.0 Safari/537.36"); + "Chrome/133.0.0.0 Safari/537.36"); return kResult; } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index b2d27fd06..830b26396 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -84,7 +84,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#4b46c37e flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long = User; +user#20b1422 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -99,11 +99,11 @@ userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#e00998b7 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long = Chat; +channel#7482147e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#52d6806b flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; +channelFull#52d6806b flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -116,7 +116,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#96fdbbe9 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int = Message; +message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message; messageService#d3d28540 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -175,7 +175,7 @@ messageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; messageActionRequestedPeer#31518e9b button_id:int peers:Vector = MessageAction; messageActionSetChatWallPaper#5060a3f4 flags:# same:flags.0?true for_both:flags.1?true wallpaper:WallPaper = MessageAction; -messageActionGiftCode#56d03994 flags:# via_giveaway:flags.0?true unclaimed:flags.2?true boost_peer:flags.1?Peer months:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long message:flags.4?TextWithEntities = MessageAction; +messageActionGiftCode#56d03994 flags:# via_giveaway:flags.0?true unclaimed:flags.5?true boost_peer:flags.1?Peer months:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long message:flags.4?TextWithEntities = MessageAction; messageActionGiveawayLaunch#a80f51e4 flags:# stars:flags.0?long = MessageAction; messageActionGiveawayResults#87e2f155 flags:# stars:flags.0?true winners_count:int unclaimed_count:int = MessageAction; messageActionBoostApply#cc02aa6d boosts:int = MessageAction; @@ -220,7 +220,7 @@ inputPeerNotifySettings#cacb6ae2 flags:# show_previews:flags.0?Bool silent:flags peerNotifySettings#99622c0c flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_ios_sound:flags.8?NotificationSound stories_android_sound:flags.9?NotificationSound stories_other_sound:flags.10?NotificationSound = PeerNotifySettings; -peerSettings#acd66c5e flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true business_bot_paused:flags.11?true business_bot_can_reply:flags.12?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int business_bot_id:flags.13?long business_bot_manage_url:flags.13?string = PeerSettings; +peerSettings#f47741f7 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true business_bot_paused:flags.11?true business_bot_can_reply:flags.12?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int business_bot_id:flags.13?long business_bot_manage_url:flags.13?string charge_paid_message_stars:flags.14?long registration_month:flags.15?string phone_country:flags.16?string name_change_date:flags.17?int photo_change_date:flags.18?int = PeerSettings; wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; @@ -236,7 +236,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#4d975bbc flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification = UserFull; +userFull#d2234ea0 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -534,6 +534,7 @@ inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey; inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey; inputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey; +inputPrivacyKeyNoPaidMessages#bdc597b4 = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; @@ -547,6 +548,7 @@ privacyKeyVoiceMessages#697f414 = PrivacyKey; privacyKeyAbout#a486b761 = PrivacyKey; privacyKeyBirthday#2000a518 = PrivacyKey; privacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey; +privacyKeyNoPaidMessages#17d348d2 = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; @@ -1309,7 +1311,7 @@ statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInvite stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; -globalPrivacySettings#734c4ccb flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true = GlobalPrivacySettings; +globalPrivacySettings#c9d8df1c flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true noncontact_peers_paid_stars:flags.5?long = GlobalPrivacySettings; help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector patterns:flags.1?Vector = help.CountryCode; @@ -1478,6 +1480,7 @@ inputInvoiceChatInviteSubscription#34e793f1 hash:string = InputInvoice; inputInvoiceStarGift#e8625e92 flags:# hide_name:flags.0?true include_upgrade:flags.2?true peer:InputPeer gift_id:long message:flags.1?TextWithEntities = InputInvoice; inputInvoiceStarGiftUpgrade#4d818d5d flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = InputInvoice; inputInvoiceStarGiftTransfer#4a5f5bd9 stargift:InputSavedStarGift to_id:InputPeer = InputInvoice; +inputInvoicePremiumGiftStars#dabab2ef flags:# user_id:InputUser months:int message:flags.0?TextWithEntities = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1493,8 +1496,6 @@ inputStorePaymentStarsTopup#dddd0f56 stars:long currency:string amount:long = In inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose; -premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; - paymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod; emojiStatusEmpty#2de11aae = EmojiStatus; @@ -1838,7 +1839,7 @@ starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#64dfc926 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount = StarsTransaction; +starsTransaction#a39fd94a flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int = StarsTransaction; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; @@ -1925,7 +1926,7 @@ payments.uniqueStarGift#caa2f60b gift:StarGift users:Vector = payments.Uni messages.webPagePreview#b53e8b21 media:MessageMedia users:Vector = messages.WebPagePreview; -savedStarGift#6056dba5 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long = SavedStarGift; +savedStarGift#6056dba5 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long = SavedStarGift; payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; @@ -1938,6 +1939,12 @@ paidReactionPrivacyDefault#206ad49e = PaidReactionPrivacy; paidReactionPrivacyAnonymous#1f0c1ad9 = PaidReactionPrivacy; paidReactionPrivacyPeer#dc6cfcf0 peer:InputPeer = PaidReactionPrivacy; +account.paidMessagesRevenue#1e109708 stars_amount:long = account.PaidMessagesRevenue; + +requirementToContactEmpty#50a9839 = RequirementToContact; +requirementToContactPremium#e581e4e9 = RequirementToContact; +requirementToContactPaidMessages#b4f67e93 stars_amount:long = RequirementToContact; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2089,11 +2096,13 @@ account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings; account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings; account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; +account.addNoPaidMessagesException#6f688aa7 flags:# refund_charged:flags.0?true user_id:InputUser = Bool; +account.getPaidMessagesRevenue#f1266f38 user_id:InputUser = account.PaidMessagesRevenue; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; -users.getIsPremiumRequiredToContact#a622aa10 id:Vector = Vector; +users.getRequirementsToContact#d89a83a3 id:Vector = Vector; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; @@ -2131,9 +2140,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; -messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; -messages.forwardMessages#6d74da08 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int = Updates; +messages.sendMessage#fbf2340a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; +messages.sendMedia#a550cd78 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; +messages.forwardMessages#bb9fa475 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#fc78af9b peer:InputPeer id:Vector option:bytes message:string = ReportResult; @@ -2176,7 +2185,7 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#3ebee86a flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendInlineBotResult#c0cf7646 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut allow_paid_stars:flags.21?long = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; @@ -2211,7 +2220,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMultiMedia#1bf89d74 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -2454,6 +2463,7 @@ channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; +channels.updatePaidMessagesPrice#fc84653f channel:InputChannel send_paid_messages_stars:long = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2533,6 +2543,7 @@ payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude payments.getSavedStarGift#b455a106 stargift:Vector = payments.SavedStarGifts; payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; +payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector = Bool; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2653,4 +2664,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 199 +// LAYER 200 diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index fd8468a53..05e1c0ad7 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "overview/overview_layout.h" #include "overview/overview_layout_delegate.h" -#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "core/ui_integration.h" // TextContext #include "data/data_document.h" #include "data/data_document_resolver.h" #include "data/data_session.h" @@ -1042,10 +1042,10 @@ void Voice::updateName() { st::defaultTextStyle, parent()->originalText(), Ui::DialogTextOptions(), - Core::MarkedTextContext{ + Core::TextContext({ .session = &parent()->history()->session(), - .customEmojiRepaint = [=] { delegate()->repaintItem(this); }, - }); + .repaint = [=] { delegate()->repaintItem(this); }, + })); } bool Voice::updateStatusText() { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 0925d8937..379036e6e 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -66,7 +66,6 @@ void CheckoutProcess::Start( Mode mode, Fn reactivate, Fn nonPanelPaymentFormProcess) { - const auto hasNonPanelPaymentFormProcess = !!nonPanelPaymentFormProcess; auto &processes = LookupSessionProcesses(&item->history()->session()); const auto media = item->media(); const auto invoice = media ? media->invoice() : nullptr; @@ -87,9 +86,7 @@ void CheckoutProcess::Start( i->second->setReactivateCallback(std::move(reactivate)); i->second->setNonPanelPaymentFormProcess( std::move(nonPanelPaymentFormProcess)); - if (!hasNonPanelPaymentFormProcess) { - i->second->requestActivate(); - } + i->second->requestActivate(); return; } const auto j = processes.byItem.emplace( @@ -100,9 +97,7 @@ void CheckoutProcess::Start( std::move(reactivate), std::move(nonPanelPaymentFormProcess), PrivateTag{})).first; - if (!hasNonPanelPaymentFormProcess) { - j->second->requestActivate(); - } + j->second->requestActivate(); } void CheckoutProcess::Start( @@ -110,16 +105,13 @@ void CheckoutProcess::Start( const QString &slug, Fn reactivate, Fn nonPanelPaymentFormProcess) { - const auto hasNonPanelPaymentFormProcess = !!nonPanelPaymentFormProcess; auto &processes = LookupSessionProcesses(session); const auto i = processes.bySlug.find(slug); if (i != end(processes.bySlug)) { i->second->setReactivateCallback(std::move(reactivate)); i->second->setNonPanelPaymentFormProcess( std::move(nonPanelPaymentFormProcess)); - if (!hasNonPanelPaymentFormProcess) { - i->second->requestActivate(); - } + i->second->requestActivate(); return; } const auto j = processes.bySlug.emplace( @@ -130,20 +122,21 @@ void CheckoutProcess::Start( std::move(reactivate), std::move(nonPanelPaymentFormProcess), PrivateTag{})).first; - if (!hasNonPanelPaymentFormProcess) { - j->second->requestActivate(); - } + j->second->requestActivate(); } void CheckoutProcess::Start( InvoicePremiumGiftCode giftCodeInvoice, - Fn reactivate) { + Fn reactivate, + Fn nonPanelPaymentFormProcess) { const auto randomId = giftCodeInvoice.randomId; auto id = InvoiceId{ std::move(giftCodeInvoice) }; auto &processes = LookupSessionProcesses(SessionFromId(id)); const auto i = processes.byRandomId.find(randomId); if (i != end(processes.byRandomId)) { i->second->setReactivateCallback(std::move(reactivate)); + i->second->setNonPanelPaymentFormProcess( + std::move(nonPanelPaymentFormProcess)); i->second->requestActivate(); return; } @@ -153,7 +146,7 @@ void CheckoutProcess::Start( std::move(id), Mode::Payment, std::move(reactivate), - nullptr, + std::move(nonPanelPaymentFormProcess), PrivateTag{})).first; j->second->requestActivate(); } @@ -372,7 +365,9 @@ void CheckoutProcess::setNonPanelPaymentFormProcess( } void CheckoutProcess::requestActivate() { - _panel->requestActivate(); + if (!_nonPanelPaymentFormProcess) { + _panel->requestActivate(); + } } not_null CheckoutProcess::panelDelegate() { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 8131a4533..d5f6ce945 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -88,7 +88,8 @@ public: Fn nonPanelPaymentFormProcess); static void Start( InvoicePremiumGiftCode giftCodeInvoice, - Fn reactivate); + Fn reactivate, + Fn nonPanelPaymentFormProcess = nullptr); static void Start( InvoiceCredits creditsInvoice, Fn reactivate); diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 7f4e26a9f..a17b2dcfd 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -178,7 +178,7 @@ MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL( MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL( const InvoicePremiumGiftCode &invoice) { - Expects(invoice.creditsAmount.has_value()); + Expects(invoice.giveawayCredits.has_value()); const auto &giveaway = v::get( invoice.purpose); using Flag = MTPDinputStorePaymentStarsGiveaway::Flag; @@ -199,7 +199,7 @@ MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL( | (giveaway.additionalPrize.isEmpty() ? Flag() : Flag::f_prize_description)), - MTP_long(*invoice.creditsAmount), + MTP_long(*invoice.giveawayCredits), giveaway.boostPeer->input, MTP_vector_from_range(ranges::views::all( giveaway.additionalChannels @@ -219,6 +219,13 @@ MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL( MTP_int(invoice.users)); } +bool IsPremiumForStarsInvoice(const InvoiceId &id) { + const auto giftCode = std::get_if(&id.value); + return giftCode + && !giftCode->giveawayCredits + && (giftCode->currency == ::Ui::kCreditsCurrency); +} + Form::Form(InvoiceId id, bool receipt) : _id(id) , _session(SessionFromId(id)) @@ -395,7 +402,7 @@ MTPInputInvoice Form::inputInvoice() const { Api::ConvertOption::SkipLocal))); } const auto &giftCode = v::get(_id.value); - if (giftCode.creditsAmount) { + if (giftCode.giveawayCredits) { return MTP_inputInvoiceStars(InvoiceCreditsGiveawayToTL(giftCode)); } using Flag = MTPDpremiumGiftCodeOption::Flag; @@ -412,12 +419,29 @@ MTPInputInvoice Form::inputInvoice() const { MTP_long(giftCode.amount)); const auto users = std::get_if( &giftCode.purpose); - if (users) { + auto message = (users && !users->message.empty()) + ? MTP_textWithEntities( + MTP_string(users->message.text), + Api::EntitiesToMTP( + &users->users.front()->session(), + users->message.entities, + Api::ConvertOption::SkipLocal)) + : std::optional(); + if (users + && users->users.size() == 1 + && giftCode.currency == ::Ui::kCreditsCurrency) { + using Flag = MTPDinputInvoicePremiumGiftStars::Flag; + return MTP_inputInvoicePremiumGiftStars( + MTP_flags(message ? Flag::f_message : Flag()), + users->users.front()->inputUser, + MTP_int(giftCode.months), + message.value_or(MTPTextWithEntities())); + } else if (users) { using Flag = MTPDinputStorePaymentPremiumGiftCode::Flag; return MTP_inputInvoicePremiumGiftCode( MTP_inputStorePaymentPremiumGiftCode( MTP_flags((users->boostPeer ? Flag::f_boost_peer : Flag()) - | (users->message.empty() ? Flag(0) : Flag::f_message)), + | (message ? Flag::f_message : Flag())), MTP_vector_from_range(ranges::views::all( users->users ) | ranges::views::transform([](not_null user) { @@ -426,12 +450,7 @@ MTPInputInvoice Form::inputInvoice() const { users->boostPeer ? users->boostPeer->input : MTPInputPeer(), MTP_string(giftCode.currency), MTP_long(giftCode.amount), - MTP_textWithEntities( - MTP_string(users->message.text), - Api::EntitiesToMTP( - &users->users.front()->session(), - users->message.entities, - Api::ConvertOption::SkipLocal))), + message.value_or(MTPTextWithEntities())), option); } else { return MTP_inputInvoicePremiumGiftCode( @@ -904,7 +923,7 @@ void Form::submit() { if (index < list.size() && password.isEmpty()) { _updates.fire(TmpPasswordRequired{}); return; - } else if (!_session->local().isBotTrustedPayment(_details.botId)) { + } else if (!_session->local().isPeerTrustedPayment(_details.botId)) { _updates.fire(BotTrustRequired{ .bot = _session->data().user(_details.botId), .provider = _session->data().user(_details.providerId), @@ -1307,7 +1326,7 @@ void Form::acceptTerms() { } void Form::trustBot() { - _session->local().markBotTrustedPayment(_details.botId); + _session->local().markPeerTrustedPayment(_details.botId); } void Form::processShippingOptions(const QVector &data) { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 8dae0d008..2e199e359 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -153,7 +153,7 @@ struct InvoicePremiumGiftCode { QString currency; QString storeProduct; - std::optional creditsAmount; + std::optional giveawayCredits; uint64 randomId = 0; uint64 amount = 0; int storeQuantity = 0; @@ -287,6 +287,8 @@ struct FormUpdate : std::variant< [[nodiscard]] MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL( const InvoicePremiumGiftCode &invoice); +[[nodiscard]] bool IsPremiumForStarsInvoice(const InvoiceId &id); + class Form final : public base::has_weak_ptr { public: Form(InvoiceId id, bool receipt); diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index c76430820..d63b505a9 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -57,7 +57,8 @@ void ProcessCreditsPayment( onstack(CheckoutResult::Cancelled); } return; - } else if (form->starGiftForm) { + } else if (form->starGiftForm + || IsPremiumForStarsInvoice(form->id)) { const auto done = [=](std::optional error) { const auto onstack = maybeReturnToBot; if (error) { @@ -86,7 +87,7 @@ void ProcessCreditsPayment( onstack(CheckoutResult::Paid); } }; - Ui::SendStarGift(&show->session(), form, done); + Ui::SendStarsForm(&show->session(), form, done); return; } const auto unsuccessful = std::make_shared(true); diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index 4018b2b8a..b51fa0bb0 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "apiwrap.h" #include "boxes/send_credits_box.h" // CreditsEmojiSmall. -#include "core/ui_integration.h" // MarkedTextContext. +#include "core/ui_integration.h" // TextContext. #include "data/components/credits.h" #include "data/data_channel.h" #include "data/data_message_reactions.h" @@ -186,10 +186,7 @@ void ShowPaidReactionDetails( ) | rpl::map([=](TextWithEntities &&text) { return Ui::TextWithContext{ .text = std::move(text), - .context = Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [] {}, - }, + .context = Core::TextContext({ .session = session }), }; }); }; diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 7cc513f7b..1786d250c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -147,53 +147,11 @@ void PaidReactionSlider( } [[nodiscard]] QImage GenerateBadgeImage(int count) { - const auto text = Lang::FormatCountDecimal(count); - const auto length = st::chatSimilarBadgeFont->width(text); - const auto contents = st::chatSimilarLockedIconPosition.x() - + st::paidReactTopStarIcon.width() - + st::paidReactTopStarSkip - + length; - const auto badge = QRect( - st::chatSimilarBadgePadding.left(), - st::chatSimilarBadgePadding.top(), - contents, - st::chatSimilarBadgeFont->height); - const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding); - - auto result = QImage( - rect.size() * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(style::DevicePixelRatio()); - result.fill(Qt::transparent); - auto q = QPainter(&result); - - const auto &font = st::chatSimilarBadgeFont; - const auto textTop = badge.y() + font->ascent; - const auto icon = &st::paidReactTopStarIcon; - const auto position = st::chatSimilarLockedIconPosition; - - auto hq = PainterHighQualityEnabler(q); - q.setBrush(st::creditsBg3); - q.setPen(Qt::NoPen); - const auto radius = rect.height() / 2.; - q.drawRoundedRect(rect, radius, radius); - - auto textLeft = 0; - if (icon) { - icon->paint( - q, - badge.x() + position.x(), - badge.y() + position.y(), - rect.width()); - textLeft += position.x() + icon->width() + st::paidReactTopStarSkip; - } - - q.setFont(font); - q.setPen(st::premiumButtonFg); - q.drawText(textLeft, textTop, text); - q.end(); - - return result; + return GenerateSmallBadgeImage( + Lang::FormatCountDecimal(count), + st::paidReactTopStarIcon, + st::creditsBg3->c, + st::premiumButtonFg->c); } void AddArrowDown(not_null widget) { @@ -321,7 +279,6 @@ void SelectShownPeer( updateUserpic(); } (*menu)->popup(QCursor::pos()); - } void FillTopReactors( @@ -633,4 +590,65 @@ object_ptr MakePaidReactionBox(PaidReactionBoxArgs &&args) { return Box(PaidReactionsBox, std::move(args)); } +QImage GenerateSmallBadgeImage( + QString text, + const style::icon &icon, + QColor bg, + QColor fg, + const style::RoundCheckbox *borderSt) { + const auto length = st::chatSimilarBadgeFont->width(text); + const auto contents = st::chatSimilarLockedIconPosition.x() + + icon.width() + + st::paidReactTopStarSkip + + length; + const auto badge = QRect( + st::chatSimilarBadgePadding.left(), + st::chatSimilarBadgePadding.top(), + contents, + st::chatSimilarBadgeFont->height); + const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding); + const auto add = borderSt ? borderSt->width : 0; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + (rect + QMargins(add, add, add, add)).size() * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::transparent); + auto q = QPainter(&result); + + const auto &font = st::chatSimilarBadgeFont; + const auto textTop = badge.y() + font->ascent; + const auto position = st::chatSimilarLockedIconPosition; + + auto hq = PainterHighQualityEnabler(q); + q.translate(add, add); + q.setBrush(bg); + if (borderSt) { + q.setPen(QPen(borderSt->border->c, borderSt->width)); + } else { + q.setPen(Qt::NoPen); + } + const auto radius = rect.height() / 2.; + const auto shift = add / 2.; + q.drawRoundedRect( + QRectF(rect) + QMarginsF(shift, shift, shift, shift), + radius, + radius); + + auto textLeft = 0; + icon.paint( + q, + badge.x() + position.x(), + badge.y() + position.y(), + rect.width()); + textLeft += position.x() + icon.width() + st::paidReactTopStarSkip; + + q.setFont(font); + q.setPen(fg); + q.drawText(textLeft, textTop, text); + q.end(); + + return result; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 034b04289..1468d53ba 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" +namespace style { +struct RoundCheckbox; +} // namespace style + namespace Ui { class BoxContent; @@ -17,7 +21,7 @@ class DynamicImage; struct TextWithContext { TextWithEntities text; - std::any context; + Text::MarkedContext context; }; struct PaidReactionTop { @@ -48,4 +52,11 @@ void PaidReactionsBox( [[nodiscard]] object_ptr MakePaidReactionBox( PaidReactionBoxArgs &&args); +[[nodiscard]] QImage GenerateSmallBadgeImage( + QString text, + const style::icon &icon, + QColor bg, + QColor fg, + const style::RoundCheckbox *borderSt = nullptr); + } // namespace Ui diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index 147a9f03c..e8696772e 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -10,10 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_integration.h" #include "base/platform/base_platform_info.h" #include "base/platform/linux/base_linux_xdp_utilities.h" -#include "window/notifications_manager.h" #include "core/sandbox.h" #include "core/application.h" -#include "core/core_settings.h" #include "base/random.h" #include @@ -27,32 +25,6 @@ namespace { using namespace gi::repository; namespace GObject = gi::repository::GObject; -std::vector AnyVectorFromVariant(GLib::Variant value) { - std::vector result; - - GLib::VariantIter iter; - iter.allocate_(); - iter.init(value); - - const auto uint64Type = GLib::VariantType::new_("t"); - const auto int64Type = GLib::VariantType::new_("x"); - - while (auto value = iter.next_value()) { - value = value.get_variant(); - if (value.is_of_type(uint64Type)) { - result.push_back(std::make_any(value.get_uint64())); - } else if (value.is_of_type(int64Type)) { - result.push_back(std::make_any(value.get_int64())); - } else if (value.is_container()) { - result.push_back( - std::make_any>( - AnyVectorFromVariant(value))); - } - } - - return result; -} - class Application : public Gio::impl::ApplicationImpl { public: Application(); @@ -125,42 +97,18 @@ Application::Application() }); actionMap.add_action(quitAction); - using Window::Notifications::Manager; - using NotificationId = Manager::NotificationId; - - const auto notificationIdVariantType = GLib::VariantType::new_("av"); + const auto notificationIdVariantType = GLib::VariantType::new_("a{sv}"); auto notificationActivateAction = Gio::SimpleAction::new_( "notification-activate", notificationIdVariantType); - notificationActivateAction.signal_activate().connect([]( - Gio::SimpleAction, - GLib::Variant parameter) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - Core::App().notifications().manager().notificationActivated( - NotificationId::FromAnyVector( - AnyVectorFromVariant(parameter))); - }); - }); - actionMap.add_action(notificationActivateAction); auto notificationMarkAsReadAction = Gio::SimpleAction::new_( "notification-mark-as-read", notificationIdVariantType); - notificationMarkAsReadAction.signal_activate().connect([]( - Gio::SimpleAction, - GLib::Variant parameter) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - Core::App().notifications().manager().notificationReplied( - NotificationId::FromAnyVector( - AnyVectorFromVariant(parameter)), - {}); - }); - }); - actionMap.add_action(notificationMarkAsReadAction); } diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index d03c83efb..b41d0e4a9 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -139,419 +139,6 @@ bool UseGNotification() { return KSandbox::isFlatpak() && !ServiceRegistered; } -GLib::Variant AnyVectorToVariant(const std::vector &value) { - return GLib::Variant::new_array( - value | ranges::views::transform([](const std::any &value) { - try { - return GLib::Variant::new_variant( - GLib::Variant::new_uint64(std::any_cast(value))); - } catch (...) { - } - - try { - return GLib::Variant::new_variant( - GLib::Variant::new_int64(std::any_cast(value))); - } catch (...) { - } - - try { - return GLib::Variant::new_variant( - AnyVectorToVariant( - std::any_cast>(value))); - } catch (...) { - } - - return GLib::Variant(nullptr); - }) | ranges::to_vector); -} - -class NotificationData final : public base::has_weak_ptr { -public: - using NotificationId = Window::Notifications::Manager::NotificationId; - using Info = Window::Notifications::NativeManager::NotificationInfo; - - NotificationData( - not_null manager, - XdgNotifications::NotificationsProxy proxy, - NotificationId id); - - [[nodiscard]] bool init(const Info &info); - - NotificationData(const NotificationData &other) = delete; - NotificationData &operator=(const NotificationData &other) = delete; - NotificationData(NotificationData &&other) = delete; - NotificationData &operator=(NotificationData &&other) = delete; - - ~NotificationData(); - - void show(); - void close(); - void setImage(QImage image); - -private: - const not_null _manager; - NotificationId _id; - - Media::Audio::LocalDiskCache _sounds; - - Gio::Application _application; - Gio::Notification _notification; - const std::string _guid; - - XdgNotifications::NotificationsProxy _proxy; - XdgNotifications::Notifications _interface; - std::string _title; - std::string _body; - std::vector _actions; - GLib::VariantDict _hints; - std::string _imageKey; - - uint _notificationId = 0; - ulong _actionInvokedSignalId = 0; - ulong _activationTokenSignalId = 0; - ulong _notificationRepliedSignalId = 0; - ulong _notificationClosedSignalId = 0; - -}; - -using Notification = std::unique_ptr; - -NotificationData::NotificationData( - not_null manager, - XdgNotifications::NotificationsProxy proxy, - NotificationId id) -: _manager(manager) -, _id(id) -, _sounds(cWorkingDir() + u"tdata/audio_cache"_q) -, _application(UseGNotification() - ? Gio::Application::get_default() - : nullptr) -, _guid(_application ? std::string(Gio::dbus_generate_guid()) : std::string()) -, _proxy(proxy) -, _interface(proxy) -, _hints(GLib::VariantDict::new_()) -, _imageKey(GetImageKey()) { -} - -bool NotificationData::init(const Info &info) { - const auto &title = info.title; - const auto &subtitle = info.subtitle; - - if (_application) { - _notification = Gio::Notification::new_( - subtitle.isEmpty() - ? title.toStdString() - : subtitle.toStdString() + " (" + title.toStdString() + ')'); - - _notification.set_body(info.message.toStdString()); - - _notification.set_icon( - Gio::ThemedIcon::new_(base::IconName().toStdString())); - - // for chat messages, according to - // https://docs.gtk.org/gio/enum.NotificationPriority.html - _notification.set_priority(Gio::NotificationPriority::HIGH_); - - // glib 2.70+, we keep glib 2.56+ compatibility - static const auto set_category = [] { - // reset dlerror after dlsym call - const auto guard = gsl::finally([] { dlerror(); }); - return reinterpret_cast( - dlsym(RTLD_DEFAULT, "g_notification_set_category")); - }(); - - if (set_category) { - set_category(_notification.gobj_(), "im.received"); - } - - const auto idVariant = AnyVectorToVariant(_id.toAnyVector()); - - _notification.set_default_action_and_target( - "app.notification-activate", - idVariant); - - if (!info.options.hideMarkAsRead) { - _notification.add_button_with_target( - tr::lng_context_mark_read(tr::now).toStdString(), - "app.notification-mark-as-read", - idVariant); - } - - return true; - } - - if (!_interface) { - return false; - } - - const auto &text = info.message; - if (HasCapability("body-markup")) { - _title = title.toStdString(); - - _body = subtitle.isEmpty() - ? text.toHtmlEscaped().toStdString() - : u"%1\n%2"_q.arg( - subtitle.toHtmlEscaped(), - text.toHtmlEscaped()).toStdString(); - } else { - _title = subtitle.isEmpty() - ? title.toStdString() - : subtitle.toStdString() + " (" + title.toStdString() + ')'; - - _body = text.toStdString(); - } - - if (HasCapability("actions")) { - _actions.push_back("default"); - _actions.push_back(tr::lng_open_link(tr::now).toStdString()); - - if (!info.options.hideMarkAsRead) { - // icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html - _actions.push_back("mail-mark-read"); - _actions.push_back( - tr::lng_context_mark_read(tr::now).toStdString()); - } - - if (HasCapability("inline-reply") - && !info.options.hideReplyButton) { - _actions.push_back("inline-reply"); - _actions.push_back( - tr::lng_notification_reply(tr::now).toStdString()); - - _notificationRepliedSignalId - = _interface.signal_notification_replied().connect([=]( - XdgNotifications::Notifications, - uint id, - std::string text) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - if (id == _notificationId) { - _manager->notificationReplied( - _id, - { QString::fromStdString(text), {} }); - } - }); - }); - } - - _actionInvokedSignalId = _interface.signal_action_invoked().connect( - [=]( - XdgNotifications::Notifications, - uint id, - std::string actionName) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - if (id == _notificationId) { - if (actionName == "default") { - _manager->notificationActivated(_id); - } else if (actionName == "mail-mark-read") { - _manager->notificationReplied(_id, {}); - } - } - }); - }); - - _activationTokenSignalId - = _interface.signal_activation_token().connect([=]( - XdgNotifications::Notifications, - uint id, - std::string token) { - if (id == _notificationId) { - GLib::setenv("XDG_ACTIVATION_TOKEN", token, true); - } - }); - } - - if (HasCapability("action-icons")) { - _hints.insert_value("action-icons", GLib::Variant::new_boolean(true)); - } - - if (HasCapability("sound")) { - const auto sound = info.sound - ? info.sound() - : Media::Audio::LocalSound(); - - const auto path = sound - ? _sounds.path(sound).toStdString() - : std::string(); - - if (!path.empty()) { - _hints.insert_value( - "sound-file", - GLib::Variant::new_string(path)); - } else { - _hints.insert_value( - "suppress-sound", - GLib::Variant::new_boolean(true)); - } - } - - if (HasCapability("x-canonical-append")) { - _hints.insert_value( - "x-canonical-append", - GLib::Variant::new_string("true")); - } - - _hints.insert_value("category", GLib::Variant::new_string("im.received")); - - _hints.insert_value("desktop-entry", GLib::Variant::new_string( - QGuiApplication::desktopFileName().toStdString())); - - _notificationClosedSignalId = - _interface.signal_notification_closed().connect([=]( - XdgNotifications::Notifications, - uint id, - uint reason) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - /* - * From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html - * The reason the notification was closed - * 1 - The notification expired. - * 2 - The notification was dismissed by the user. - * 3 - The notification was closed by a call to CloseNotification. - * 4 - Undefined/reserved reasons. - * - * If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history. - * We do not need to send a "CloseNotification" call later to clear it from history. - * Therefore we can drop the notification reference now. - * In all other cases we keep the notification reference so that we may clear the notification later from history, - * if the message for that notification is read (e.g. chat is opened or read from another device). - */ - if (id == _notificationId && reason == 2) { - _manager->clearNotification(_id); - } - }); - }); - - return true; -} - -NotificationData::~NotificationData() { - if (_interface) { - if (_actionInvokedSignalId != 0) { - _interface.disconnect(_actionInvokedSignalId); - } - - if (_activationTokenSignalId != 0) { - _interface.disconnect(_activationTokenSignalId); - } - - if (_notificationRepliedSignalId != 0) { - _interface.disconnect(_notificationRepliedSignalId); - } - - if (_notificationClosedSignalId != 0) { - _interface.disconnect(_notificationClosedSignalId); - } - } -} - -void NotificationData::show() { - if (_application && _notification) { - _application.send_notification(_guid, _notification); - return; - } - - // a hack for snap's activation restriction - const auto weak = base::make_weak(this); - StartServiceAsync(_proxy.get_connection(), crl::guard(weak, [=] { - const auto iconName = _imageKey.empty() - || !_hints.lookup_value(_imageKey) - ? base::IconName().toStdString() - : std::string(); - - auto actions = _actions - | ranges::views::transform(&std::string::c_str) - | ranges::to_vector; - actions.push_back(nullptr); - - const auto callbackWrap = gi::unwrap( - Gio::AsyncReadyCallback( - crl::guard(weak, [=](GObject::Object, Gio::AsyncResult res) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - const auto result = _interface.call_notify_finish( - res); - - if (!result) { - Gio::DBusErrorNS_::strip_remote_error( - result.error()); - LOG(("Native Notification Error: %1").arg( - result.error().message_().c_str())); - _manager->clearNotification(_id); - return; - } - - _notificationId = std::get<1>(*result); - }); - })), - gi::scope_async); - - xdg_notifications_notifications_call_notify( - _interface.gobj_(), - AppName.data(), - 0, - iconName.c_str(), - _title.c_str(), - _body.c_str(), - actions.data(), - _hints.end().gobj_(), - -1, - nullptr, - &callbackWrap->wrapper, - callbackWrap); - })); -} - -void NotificationData::close() { - if (_application) { - _application.withdraw_notification(_guid); - } else { - _interface.call_close_notification(_notificationId, nullptr); - } - _manager->clearNotification(_id); -} - -void NotificationData::setImage(QImage image) { - if (_notification) { - const auto imageData = std::make_shared(); - QBuffer buffer(imageData.get()); - buffer.open(QIODevice::WriteOnly); - image.save(&buffer, "PNG"); - - _notification.set_icon( - Gio::BytesIcon::new_( - GLib::Bytes::new_with_free_func( - reinterpret_cast(imageData->constData()), - imageData->size(), - [imageData] {}))); - - return; - } - - if (_imageKey.empty()) { - return; - } - - if (image.hasAlphaChannel()) { - image.convertTo(QImage::Format_RGBA8888); - } else { - image.convertTo(QImage::Format_RGB888); - } - - _hints.insert_value(_imageKey, GLib::Variant::new_tuple({ - GLib::Variant::new_int32(image.width()), - GLib::Variant::new_int32(image.height()), - GLib::Variant::new_int32(image.bytesPerLine()), - GLib::Variant::new_boolean(image.hasAlphaChannel()), - GLib::Variant::new_int32(8), - GLib::Variant::new_int32(image.hasAlphaChannel() ? 4 : 3), - GLib::Variant::new_from_data( - GLib::VariantType::new_("ay"), - reinterpret_cast(image.constBits()), - image.sizeInBytes(), - true, - [image] {}), - })); -} - } // namespace class Manager::Private : public base::has_weak_ptr { @@ -571,17 +158,22 @@ public: void clearNotification(NotificationId id); void invokeIfNotInhibited(Fn callback); - ~Private(); - private: - const not_null _manager; + struct NotificationData : public base::has_weak_ptr { + std::variant id; + rpl::lifetime lifetime; + }; + using Notification = std::unique_ptr; + const not_null _manager; + Gio::Application _application; + XdgNotifications::NotificationsProxy _proxy; + XdgNotifications::Notifications _interface; + Media::Audio::LocalDiskCache _sounds; base::flat_map< ContextId, base::flat_map> _notifications; - - XdgNotifications::NotificationsProxy _proxy; - XdgNotifications::Notifications _interface; + rpl::lifetime _lifetime; }; @@ -728,7 +320,11 @@ void Create(Window::Notifications::System *system) { } Manager::Private::Private(not_null manager) -: _manager(manager) { +: _manager(manager) +, _application(UseGNotification() + ? Gio::Application::get_default() + : nullptr) +, _sounds(cWorkingDir() + u"tdata/audio_cache"_q) { const auto &serverInformation = CurrentServerInformation; if (!serverInformation.name.empty()) { @@ -760,11 +356,163 @@ Manager::Private::Private(not_null manager) return a + (a.empty() ? "" : ", ") + b; }).c_str())); } + + if (auto actionMap = Gio::ActionMap(_application)) { + const auto dictToNotificationId = [](GLib::VariantDict dict) { + return NotificationId{ + .contextId = ContextId{ + .sessionId = dict.lookup_value("session").get_uint64(), + .peerId = PeerId(dict.lookup_value("peer").get_uint64()), + .topicRootId = dict.lookup_value("topic").get_int64(), + }, + .msgId = dict.lookup_value("msgid").get_int64(), + }; + }; + + auto activate = gi::wrap( + G_SIMPLE_ACTION( + actionMap.lookup_action("notification-activate").gobj_()), + gi::transfer_none); + + const auto activateSig = activate.signal_activate().connect([=]( + Gio::SimpleAction, + GLib::Variant parameter) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + _manager->notificationActivated( + dictToNotificationId(GLib::VariantDict::new_(parameter))); + }); + }); + + _lifetime.add([=]() mutable { + activate.disconnect(activateSig); + }); + + auto markAsRead = gi::wrap( + G_SIMPLE_ACTION( + actionMap.lookup_action("notification-mark-as-read").gobj_()), + gi::transfer_none); + + const auto markAsReadSig = markAsRead.signal_activate().connect([=]( + Gio::SimpleAction, + GLib::Variant parameter) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + _manager->notificationReplied( + dictToNotificationId(GLib::VariantDict::new_(parameter)), + {}); + }); + }); + + _lifetime.add([=]() mutable { + markAsRead.disconnect(markAsReadSig); + }); + } } void Manager::Private::init(XdgNotifications::NotificationsProxy proxy) { _proxy = proxy; _interface = proxy; + + if (_application || !_interface) { + return; + } + + const auto actionInvoked = _interface.signal_action_invoked().connect([=]( + XdgNotifications::Notifications, + uint id, + std::string actionName) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + for (const auto &[key, notifications] : _notifications) { + for (const auto &[msgId, notification] : notifications) { + if (id == v::get(notification->id)) { + if (actionName == "default") { + _manager->notificationActivated({ key, msgId }); + } else if (actionName == "mail-mark-read") { + _manager->notificationReplied({ key, msgId }, {}); + } + return; + } + } + } + }); + }); + + _lifetime.add([=] { + _interface.disconnect(actionInvoked); + }); + + const auto replied = _interface.signal_notification_replied().connect([=]( + XdgNotifications::Notifications, + uint id, + std::string text) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + for (const auto &[key, notifications] : _notifications) { + for (const auto &[msgId, notification] : notifications) { + if (id == v::get(notification->id)) { + _manager->notificationReplied( + { key, msgId }, + { QString::fromStdString(text), {} }); + return; + } + } + } + }); + }); + + _lifetime.add([=] { + _interface.disconnect(replied); + }); + + const auto tokenSignal = _interface.signal_activation_token().connect([=]( + XdgNotifications::Notifications, + uint id, + std::string token) { + for (const auto &[key, notifications] : _notifications) { + for (const auto &[msgId, notification] : notifications) { + if (id == v::get(notification->id)) { + GLib::setenv("XDG_ACTIVATION_TOKEN", token, true); + return; + } + } + } + }); + + _lifetime.add([=] { + _interface.disconnect(tokenSignal); + }); + + const auto closed = _interface.signal_notification_closed().connect([=]( + XdgNotifications::Notifications, + uint id, + uint reason) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + for (const auto &[key, notifications] : _notifications) { + for (const auto &[msgId, notification] : notifications) { + /* + * From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html + * The reason the notification was closed + * 1 - The notification expired. + * 2 - The notification was dismissed by the user. + * 3 - The notification was closed by a call to CloseNotification. + * 4 - Undefined/reserved reasons. + * + * If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history. + * We do not need to send a "CloseNotification" call later to clear it from history. + * Therefore we can drop the notification reference now. + * In all other cases we keep the notification reference so that we may clear the notification later from history, + * if the message for that notification is read (e.g. chat is opened or read from another device). + */ + if (id == v::get(notification->id) && reason == 2) { + clearNotification({ key, msgId }); + return; + } + } + } + }); + }); + + _lifetime.add([=] { + _interface.disconnect(closed); + }); } void Manager::Private::showNotification( @@ -781,85 +529,284 @@ void Manager::Private::showNotification( .contextId = key, .msgId = info.itemId, }; - auto notification = std::make_unique( - _manager, - _proxy, - notificationId); - const auto inited = notification->init(info); - if (!inited) { - return; + auto notification = _application + ? Gio::Notification::new_( + info.subtitle.isEmpty() + ? info.title.toStdString() + : info.subtitle.toStdString() + + " (" + info.title.toStdString() + ')') + : Gio::Notification(); + + std::vector actions; + auto hints = GLib::VariantDict::new_(); + if (notification) { + notification.set_body(info.message.toStdString()); + + notification.set_icon( + Gio::ThemedIcon::new_(base::IconName().toStdString())); + + // for chat messages, according to + // https://docs.gtk.org/gio/enum.NotificationPriority.html + notification.set_priority(Gio::NotificationPriority::HIGH_); + + // glib 2.70+, we keep glib 2.56+ compatibility + static const auto set_category = [] { + // reset dlerror after dlsym call + const auto guard = gsl::finally([] { dlerror(); }); + return reinterpret_cast( + dlsym(RTLD_DEFAULT, "g_notification_set_category")); + }(); + + if (set_category) { + set_category(notification.gobj_(), "im.received"); + } + + const auto notificationVariant = GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("session"), + GLib::Variant::new_variant( + GLib::Variant::new_uint64(peer->session().uniqueId()))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("peer"), + GLib::Variant::new_variant( + GLib::Variant::new_uint64(peer->id.value))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("peer"), + GLib::Variant::new_variant( + GLib::Variant::new_uint64(peer->id.value))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("topic"), + GLib::Variant::new_variant( + GLib::Variant::new_int64(info.topicRootId.bare))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("msgid"), + GLib::Variant::new_variant( + GLib::Variant::new_int64(info.itemId.bare))), + }); + + notification.set_default_action_and_target( + "app.notification-activate", + notificationVariant); + + if (!options.hideMarkAsRead) { + notification.add_button_with_target( + tr::lng_context_mark_read(tr::now).toStdString(), + "app.notification-mark-as-read", + notificationVariant); + } + } else { + if (HasCapability("actions")) { + actions.push_back("default"); + actions.push_back(tr::lng_open_link(tr::now).toStdString()); + + if (!options.hideMarkAsRead) { + // icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html + actions.push_back("mail-mark-read"); + actions.push_back( + tr::lng_context_mark_read(tr::now).toStdString()); + } + + if (HasCapability("inline-reply") + && !options.hideReplyButton) { + actions.push_back("inline-reply"); + actions.push_back( + tr::lng_notification_reply(tr::now).toStdString()); + } + + actions.push_back({}); + } + + if (HasCapability("action-icons")) { + hints.insert_value( + "action-icons", + GLib::Variant::new_boolean(true)); + } + + if (HasCapability("sound")) { + const auto sound = info.sound + ? info.sound() + : Media::Audio::LocalSound(); + + const auto path = sound + ? _sounds.path(sound).toStdString() + : std::string(); + + if (!path.empty()) { + hints.insert_value( + "sound-file", + GLib::Variant::new_string(path)); + } else { + hints.insert_value( + "suppress-sound", + GLib::Variant::new_boolean(true)); + } + } + + if (HasCapability("x-canonical-append")) { + hints.insert_value( + "x-canonical-append", + GLib::Variant::new_string("true")); + } + + hints.insert_value( + "category", + GLib::Variant::new_string("im.received")); + + hints.insert_value("desktop-entry", GLib::Variant::new_string( + QGuiApplication::desktopFileName().toStdString())); } + const auto imageKey = GetImageKey(); if (!options.hideNameAndPhoto) { - notification->setImage( - Window::Notifications::GenerateUserpic(peer, userpicView)); - } + if (notification) { + QByteArray imageData; + QBuffer buffer(&imageData); + buffer.open(QIODevice::WriteOnly); + Window::Notifications::GenerateUserpic(peer, userpicView).save( + &buffer, + "PNG"); - auto i = _notifications.find(key); - if (i != end(_notifications)) { - auto j = i->second.find(info.itemId); - if (j != end(i->second)) { - auto oldNotification = std::move(j->second); - i->second.erase(j); - oldNotification->close(); - i = _notifications.find(key); + notification.set_icon( + Gio::BytesIcon::new_( + GLib::Bytes::new_with_free_func( + reinterpret_cast(imageData.constData()), + imageData.size(), + [imageData] {}))); + } else if (!imageKey.empty()) { + const auto image = Window::Notifications::GenerateUserpic( + peer, + userpicView + ).convertToFormat(QImage::Format_RGBA8888); + + hints.insert_value(imageKey, GLib::Variant::new_tuple({ + GLib::Variant::new_int32(image.width()), + GLib::Variant::new_int32(image.height()), + GLib::Variant::new_int32(image.bytesPerLine()), + GLib::Variant::new_boolean(true), + GLib::Variant::new_int32(8), + GLib::Variant::new_int32(4), + GLib::Variant::new_from_data( + GLib::VariantType::new_("ay"), + reinterpret_cast(image.constBits()), + image.sizeInBytes(), + true, + [image] {}), + })); } } - if (i == end(_notifications)) { - i = _notifications.emplace( - key, - base::flat_map()).first; + + const auto &data + = _notifications[key][info.itemId] + = std::make_unique(); + data->lifetime.add([=, notification = data.get()] { + v::match(notification->id, [&](const std::string &id) { + _application.withdraw_notification(id); + }, [&](uint id) { + _interface.call_close_notification(id, nullptr); + }, [](v::null_t) {}); + }); + + if (notification) { + const auto id = Gio::dbus_generate_guid(); + data->id = id; + _application.send_notification(id, notification); + } else { + // work around snap's activation restriction + const auto weak = base::make_weak(data); + StartServiceAsync( + _proxy.get_connection(), + crl::guard(weak, [=]() mutable { + const auto hasImage = !imageKey.empty() + && hints.lookup_value(imageKey); + + const auto hasBodyMarkup = HasCapability("body-markup"); + + const auto callbackWrap = gi::unwrap( + Gio::AsyncReadyCallback( + crl::guard(this, [=]( + GObject::Object, + Gio::AsyncResult res) { + auto &sandbox = Core::Sandbox::Instance(); + sandbox.customEnterFromEventLoop([&] { + const auto result + = _interface.call_notify_finish(res); + + if (!result) { + Gio::DBusErrorNS_::strip_remote_error( + result.error()); + LOG(("Native Notification Error: %1").arg( + result.error().message_().c_str())); + clearNotification(notificationId); + return; + } + + if (!weak) { + _interface.call_close_notification( + std::get<1>(*result), + nullptr); + return; + } + + weak->id = std::get<1>(*result); + }); + })), + gi::scope_async); + + xdg_notifications_notifications_call_notify( + _interface.gobj_(), + AppName.data(), + 0, + (!hasImage + ? base::IconName().toStdString() + : std::string()).c_str(), + (hasBodyMarkup || info.subtitle.isEmpty() + ? info.title.toStdString() + : info.subtitle.toStdString() + + " (" + info.title.toStdString() + ')').c_str(), + (hasBodyMarkup + ? info.subtitle.isEmpty() + ? info.message.toHtmlEscaped().toStdString() + : u"%1\n%2"_q.arg( + info.subtitle.toHtmlEscaped(), + info.message.toHtmlEscaped()).toStdString() + : info.message.toStdString()).c_str(), + !actions.empty() + ? (actions + | ranges::views::transform(&gi::cstring::c_str) + | ranges::to_vector).data() + : nullptr, + hints.end().gobj_(), + -1, + nullptr, + &callbackWrap->wrapper, + callbackWrap); + })); } - const auto j = i->second.emplace( - info.itemId, - std::move(notification)).first; - j->second->show(); } void Manager::Private::clearAll() { - for (const auto &[key, notifications] : base::take(_notifications)) { - for (const auto &[msgId, notification] : notifications) { - notification->close(); - } - } + _notifications.clear(); } void Manager::Private::clearFromItem(not_null item) { - const auto key = ContextId{ + const auto i = _notifications.find(ContextId{ .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), - }; - const auto i = _notifications.find(key); - if (i == _notifications.cend()) { - return; - } - const auto j = i->second.find(item->id); - if (j == i->second.end()) { - return; - } - const auto taken = base::take(j->second); - i->second.erase(j); - if (i->second.empty()) { + }); + if (i != _notifications.cend() + && i->second.remove(item->id) + && i->second.empty()) { _notifications.erase(i); } - taken->close(); } void Manager::Private::clearFromTopic(not_null topic) { - const auto key = ContextId{ + _notifications.remove(ContextId{ .sessionId = topic->session().uniqueId(), - .peerId = topic->history()->peer->id - }; - const auto i = _notifications.find(key); - if (i != _notifications.cend()) { - const auto temp = base::take(i->second); - _notifications.erase(i); - - for (const auto &[msgId, notification] : temp) { - notification->close(); - } - } + .peerId = topic->history()->peer->id, + .topicRootId = topic->rootId(), + }); } void Manager::Private::clearFromHistory(not_null history) { @@ -872,12 +819,7 @@ void Manager::Private::clearFromHistory(not_null history) { while (i != _notifications.cend() && i->first.sessionId == sessionId && i->first.peerId == peerId) { - const auto temp = base::take(i->second); i = _notifications.erase(i); - - for (const auto &[msgId, notification] : temp) { - notification->close(); - } } } @@ -887,21 +829,16 @@ void Manager::Private::clearFromSession(not_null session) { .sessionId = sessionId, }); while (i != _notifications.cend() && i->first.sessionId == sessionId) { - const auto temp = base::take(i->second); i = _notifications.erase(i); - - for (const auto &[msgId, notification] : temp) { - notification->close(); - } } } void Manager::Private::clearNotification(NotificationId id) { auto i = _notifications.find(id.contextId); - if (i != _notifications.cend()) { - if (i->second.remove(id.msgId) && i->second.empty()) { - _notifications.erase(i); - } + if (i != _notifications.cend() + && i->second.remove(id.msgId) + && i->second.empty()) { + _notifications.erase(i); } } @@ -911,19 +848,11 @@ void Manager::Private::invokeIfNotInhibited(Fn callback) { } } -Manager::Private::~Private() { - clearAll(); -} - Manager::Manager(not_null system) : NativeManager(system) , _private(std::make_unique(this)) { } -void Manager::clearNotification(NotificationId id) { - _private->clearNotification(id); -} - Manager::~Manager() = default; void Manager::doShowNativeNotification( diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h index ecf1ce0c0..8ab17f55b 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h @@ -15,7 +15,6 @@ namespace Notifications { class Manager : public Window::Notifications::NativeManager { public: Manager(not_null system); - void clearNotification(NotificationId id); ~Manager(); protected: diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 780dc7428..193fa258d 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -261,7 +261,7 @@ bool GenerateDesktopFile( -1, GLib::KeyFileFlags::KEEP_COMMENTS_ | GLib::KeyFileFlags::KEEP_TRANSLATIONS_); - + if (!loaded) { if (!silent) { LOG(("App Error: %1").arg(loaded.error().message_().c_str())); diff --git a/Telegram/SourceFiles/platform/linux/tray_linux.cpp b/Telegram/SourceFiles/platform/linux/tray_linux.cpp index 2d0e23aec..2cc92f331 100644 --- a/Telegram/SourceFiles/platform/linux/tray_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/tray_linux.cpp @@ -44,66 +44,50 @@ public: explicit IconGraphic(); ~IconGraphic(); - [[nodiscard]] bool isRefreshNeeded( - const QIcon &systemIcon, - const QString &iconThemeName, - int counter, - bool muted) const; - [[nodiscard]] QIcon systemIcon( - const QString &iconThemeName, - bool monochrome, - int counter, - bool muted) const; - [[nodiscard]] QIcon trayIcon( - const QIcon &systemIcon, - const QString &iconThemeName, - bool monochrome, - int counter, - bool muted); + void updateState(); + [[nodiscard]] bool isRefreshNeeded() const; + [[nodiscard]] QIcon trayIcon(); private: + struct State { + QIcon systemIcon; + QString iconThemeName; + bool monochrome = false; + int32 counter = 0; + bool muted = false; + }; + + [[nodiscard]] QIcon systemIcon() const; + [[nodiscard]] bool isCounterNeeded(const State &state) const; [[nodiscard]] int counterSlice(int counter) const; - void updateIconRegenerationNeeded( - const QIcon &icon, - const QIcon &systemIcon, - const QString &iconThemeName, - bool monochrome, - int counter, - bool muted); [[nodiscard]] QSize dprSize(const QImage &image) const; const int _iconSizes[7]; - bool _muted = true; - int32 _count = 0; base::flat_map _imageBack; QIcon _trayIcon; - QIcon _systemIcon; - QString _themeName; - bool _monochrome; + State _current; + State _new; }; IconGraphic::IconGraphic() : _iconSizes{ 16, 22, 32, 48, 64, 128, 256 } { + updateState(); } IconGraphic::~IconGraphic() = default; -QIcon IconGraphic::systemIcon( - const QString &iconThemeName, - bool monochrome, - int counter, - bool muted) const { - if (iconThemeName == _themeName - && monochrome == _monochrome - && (counter > 0) == (_count > 0) - && muted == _muted) { - return _systemIcon; +QIcon IconGraphic::systemIcon() const { + if (_new.iconThemeName == _current.iconThemeName + && _new.monochrome == _current.monochrome + && (_new.counter > 0) == (_current.counter > 0) + && _new.muted == _current.muted) { + return _current.systemIcon; } const auto candidates = { - monochrome ? PanelIconName(counter, muted) : QString(), + _new.monochrome ? PanelIconName(_new.counter, _new.muted) : QString(), base::IconName(), }; @@ -120,80 +104,67 @@ QIcon IconGraphic::systemIcon( return QIcon(); } +bool IconGraphic::isCounterNeeded(const State &state) const { + return state.systemIcon.name() != PanelIconName( + state.counter, + state.muted); +} int IconGraphic::counterSlice(int counter) const { - return (counter >= 1000) - ? (1000 + (counter % 100)) + return (counter >= 100) + ? (100 + (counter % 10)) : counter; } -bool IconGraphic::isRefreshNeeded( - const QIcon &systemIcon, - const QString &iconThemeName, - int counter, - bool muted) const { - return _trayIcon.isNull() - || iconThemeName != _themeName - || systemIcon.name() != _systemIcon.name() - || (systemIcon.name() != PanelIconName(counter, muted) - ? muted != _muted || counterSlice(counter) != _count - : false); -} - -void IconGraphic::updateIconRegenerationNeeded( - const QIcon &icon, - const QIcon &systemIcon, - const QString &iconThemeName, - bool monochrome, - int counter, - bool muted) { - _trayIcon = icon; - _systemIcon = systemIcon; - _themeName = iconThemeName; - _monochrome = monochrome; - _count = counterSlice(counter); - _muted = muted; -} - QSize IconGraphic::dprSize(const QImage &image) const { return image.size() / image.devicePixelRatio(); } -QIcon IconGraphic::trayIcon( - const QIcon &systemIcon, - const QString &iconThemeName, - bool monochrome, - int counter, - bool muted) { - if (!isRefreshNeeded(systemIcon, iconThemeName, counter, muted)) { +void IconGraphic::updateState() { + _new.iconThemeName = QIcon::themeName(); + _new.monochrome = Core::App().settings().trayIconMonochrome(); + _new.counter = Core::App().unreadBadge(); + _new.muted = Core::App().unreadBadgeMuted(); + _new.systemIcon = systemIcon(); +} + +bool IconGraphic::isRefreshNeeded() const { + return _trayIcon.isNull() + || _new.iconThemeName != _current.iconThemeName + || _new.systemIcon.name() != _current.systemIcon.name() + || (isCounterNeeded(_new) + ? _new.muted != _current.muted + || counterSlice(_new.counter) != counterSlice( + _current.counter) + : false); +} + +QIcon IconGraphic::trayIcon() { + if (!isRefreshNeeded()) { return _trayIcon; } - if (systemIcon.name() == PanelIconName(counter, muted)) { - updateIconRegenerationNeeded( - systemIcon, - systemIcon, - iconThemeName, - monochrome, - counter, - muted); + const auto guard = gsl::finally([&] { + _current = _new; + }); - return systemIcon; + if (!isCounterNeeded(_new)) { + _trayIcon = _new.systemIcon; + return _trayIcon; } QIcon result; - for (const auto iconSize : _iconSizes) { auto ¤tImageBack = _imageBack[iconSize]; const auto desiredSize = QSize(iconSize, iconSize); if (currentImageBack.isNull() - || iconThemeName != _themeName - || systemIcon.name() != _systemIcon.name()) { - if (!systemIcon.isNull()) { + || _new.iconThemeName != _current.iconThemeName + || _new.systemIcon.name() != _current.systemIcon.name()) { + if (!_new.systemIcon.isNull()) { // We can't use QIcon::actualSize here // since it works incorrectly with svg icon themes - currentImageBack = systemIcon + currentImageBack = _new.systemIcon .pixmap(desiredSize) .toImage(); @@ -202,7 +173,8 @@ QIcon IconGraphic::trayIcon( // if current icon theme is not a svg one, Qt can return // a pixmap that less in size even if there are a bigger one if (firstAttemptSize.width() < desiredSize.width()) { - const auto availableSizes = systemIcon.availableSizes(); + const auto availableSizes + = _new.systemIcon.availableSizes(); const auto biggestSize = ranges::max_element( availableSizes, @@ -210,7 +182,7 @@ QIcon IconGraphic::trayIcon( &QSize::width); if (biggestSize->width() > firstAttemptSize.width()) { - currentImageBack = systemIcon + currentImageBack = _new.systemIcon .pixmap(*biggestSize) .toImage(); } @@ -227,24 +199,17 @@ QIcon IconGraphic::trayIcon( } } - result.addPixmap(Ui::PixmapFromImage(counter > 0 + result.addPixmap(Ui::PixmapFromImage(_new.counter > 0 ? Window::WithSmallCounter(std::move(currentImageBack), { .size = iconSize, - .count = counter, - .bg = muted ? st::trayCounterBgMute : st::trayCounterBg, + .count = _new.counter, + .bg = _new.muted ? st::trayCounterBgMute : st::trayCounterBg, .fg = st::trayCounterFg, }) : std::move(currentImageBack))); } - updateIconRegenerationNeeded( - result, - systemIcon, - iconThemeName, - monochrome, - counter, - muted); - - return result; + _trayIcon = result; + return _trayIcon; } class TrayEventFilter final : public QObject { @@ -321,22 +286,8 @@ void Tray::createIcon() { }); }; - const auto iconThemeName = QIcon::themeName(); - const auto monochrome = Core::App().settings().trayIconMonochrome(); - const auto counter = Core::App().unreadBadge(); - const auto muted = Core::App().unreadBadgeMuted(); - _icon = base::make_unique_q(nullptr); - _icon->setIcon(_iconGraphic->trayIcon( - _iconGraphic->systemIcon( - iconThemeName, - monochrome, - counter, - muted), - iconThemeName, - monochrome, - counter, - muted)); + _icon->setIcon(_iconGraphic->trayIcon()); _icon->setToolTip(AppName.utf16()); using Reason = QSystemTrayIcon::ActivationReason; @@ -375,27 +326,10 @@ void Tray::updateIcon() { if (!_icon || !_iconGraphic) { return; } - const auto counter = Core::App().unreadBadge(); - const auto muted = Core::App().unreadBadgeMuted(); - const auto monochrome = Core::App().settings().trayIconMonochrome(); - const auto iconThemeName = QIcon::themeName(); - const auto systemIcon = _iconGraphic->systemIcon( - iconThemeName, - monochrome, - counter, - muted); - if (_iconGraphic->isRefreshNeeded( - systemIcon, - iconThemeName, - counter, - muted)) { - _icon->setIcon(_iconGraphic->trayIcon( - systemIcon, - iconThemeName, - monochrome, - counter, - muted)); + _iconGraphic->updateState(); + if (_iconGraphic->isRefreshNeeded()) { + _icon->setIcon(_iconGraphic->trayIcon()); } } diff --git a/Telegram/SourceFiles/platform/win/file_utilities_win.cpp b/Telegram/SourceFiles/platform/win/file_utilities_win.cpp index e07298811..f3cc6fb89 100644 --- a/Telegram/SourceFiles/platform/win/file_utilities_win.cpp +++ b/Telegram/SourceFiles/platform/win/file_utilities_win.cpp @@ -117,6 +117,28 @@ HBITMAP IconToBitmap(LPWSTR icon, int iconindex) { return (HBITMAP)CopyImage(result, IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_CREATEDIBSECTION); } +bool ShouldSaveZoneInformation() { + // Check if the "Do not preserve zone information in file attachments" policy is enabled. + const auto keyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments"; + const auto valueName = L"SaveZoneInformation"; + auto key = HKEY(); + auto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key); + if (result != ERROR_SUCCESS) { + // If the registry key cannot be opened, assume the default behavior: + // Windows preserves zone information for downloaded files. + return true; + } + + DWORD value = 0, type = 0, size = sizeof(value); + result = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size); + RegCloseKey(key); + + if (result != ERROR_SUCCESS || type != REG_DWORD) { + return true; + } + + return (value != 1); +} } // namespace void UnsafeOpenEmailLink(const QString &email) { @@ -277,6 +299,11 @@ void UnsafeLaunch(const QString &filepath) { } void PostprocessDownloaded(const QString &filepath) { + // Mark file saved to the NTFS file system as originating from the Internet security zone + // unless this feature is disabled by Group Policy. + if (!ShouldSaveZoneInformation()) { + return; + } auto wstringZoneFile = QDir::toNativeSeparators(filepath).toStdWString() + L":Zone.Identifier"; auto f = CreateFile(wstringZoneFile.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (f == INVALID_HANDLE_VALUE) { // :( diff --git a/Telegram/SourceFiles/settings/business/settings_chat_links.cpp b/Telegram/SourceFiles/settings/business/settings_chat_links.cpp index e0d35efca..44025db44 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_links.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_links.cpp @@ -164,10 +164,10 @@ Row::Row(not_null delegate, const ChatLinkData &data) } void Row::updateStatus(const ChatLinkData &data) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = _delegate->rowSession(), - .customEmojiRepaint = [=] { _delegate->rowUpdateRow(this); }, - }; + .repaint = [=] { _delegate->rowUpdateRow(this); }, + }); _status.setMarkedText( st::messageTextStyle, data.message, diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 995a98202..db5a4d99a 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -223,10 +223,10 @@ private: not_null photo, Api::SendOptions options); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot); void sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options, std::optional localMessageId); @@ -1534,7 +1534,7 @@ bool ShortcutMessages::sendExistingPhoto( } void ShortcutMessages::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot) { if (showPremiumRequired()) { return; @@ -1542,11 +1542,11 @@ void ShortcutMessages::sendInlineResult( Data::ShowSendErrorToast(_controller, _history->peer, error); return; } - sendInlineResult(result, bot, {}, std::nullopt); + sendInlineResult(std::move(result), bot, {}, std::nullopt); } void ShortcutMessages::sendInlineResult( - not_null result, + std::shared_ptr result, not_null bot, Api::SendOptions options, std::optional localMessageId) { @@ -1555,7 +1555,11 @@ void ShortcutMessages::sendInlineResult( } auto action = prepareSendAction(options); action.generateLocal = true; - _session->api().sendInlineResult(bot, result, action, localMessageId); + _session->api().sendInlineResult( + bot, + result.get(), + action, + localMessageId); _composeControls->clear(); //_saveDraftText = true; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp index 4b6c8024f..e8bb2607a 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp @@ -143,10 +143,7 @@ void SetupHeader( content, v::text::take_marked(std::move(about)), st, - st::defaultPopupMenu, - [=](Fn update) { - return CommonTextContext{ std::move(update) }; - })), + st::defaultPopupMenu)), st::changePhoneDescriptionPadding); wrap->setAttribute(Qt::WA_TransparentForMouseEvents); wrap->resize( diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index 82c0b9964..b1bee4942 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_links.h" #include "boxes/premium_preview_box.h" #include "core/click_handler_types.h" +#include "core/ui_integration.h" // TextContext #include "data/business/data_business_info.h" #include "data/business/data_business_chatbots.h" #include "data/business/data_shortcut_messages.h" @@ -526,11 +527,7 @@ void Business::setupContent() { const auto session = &_controller->session(); { - const auto arrow = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); + const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); inner->add(object_ptr( inner, Ui::CreateLabelWithCustomEmoji( @@ -547,7 +544,7 @@ void Business::setupContent() { return Ui::Text::Link(text, url); }), Ui::Text::RichLangValue), - { .session = session }, + Core::TextContext({ .session = session }), st::boxDividerLabel), st::defaultBoxDividerLabelPadding, RectPart::Top | RectPart::Bottom)); diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 0ac5c025a..0e132d82f 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -429,8 +429,7 @@ void Credits::setupContent() { Ui::AddSkip(content); struct State final { - rpl::variable confirmButtonBusy = false; - std::optional api; + BuyStarsHandler buyStars; }; const auto state = content->lifetime().make_state(); @@ -438,60 +437,21 @@ void Credits::setupContent() { object_ptr( content, rpl::conditional( - state->confirmButtonBusy.value(), + state->buyStars.loadingValue(), rpl::single(QString()), tr::lng_credits_buy_button()), st::creditsSettingsBigBalanceButton), st::boxRowPadding); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); const auto show = _controller->uiShow(); - const auto optionsBox = [=](not_null box) { - box->setStyle(st::giveawayGiftCodeBox); - box->setWidth(st::boxWideWidth); - box->setTitle(tr::lng_credits_summary_options_subtitle()); - const auto inner = box->verticalLayout(); - const auto self = show->session().user(); - const auto options = state->api - ? state->api->options() - : Data::CreditTopupOptions(); - const auto amount = StarsAmount(); - FillCreditOptions(show, inner, self, amount, paid, nullptr, options); - - const auto button = box->addButton(tr::lng_close(), [=] { - box->closeBox(); - }); - const auto buttonWidth = st::boxWideWidth - - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - button->resizeToWidth(buttonWidth); - }, button->lifetime()); - }; - button->setClickedCallback([=] { - if (state->api && !state->api->options().empty()) { - state->confirmButtonBusy = false; - show->show(Box(optionsBox)); - } else { - state->confirmButtonBusy = true; - state->api.emplace(show->session().user()); - state->api->request( - ) | rpl::start_with_error_done([=](const QString &error) { - state->confirmButtonBusy = false; - show->showToast(error); - }, [=] { - state->confirmButtonBusy = false; - show->show(Box(optionsBox)); - }, content->lifetime()); - } - }); + button->setClickedCallback(state->buyStars.handler(show, paid)); { using namespace Info::Statistics; const auto loadingAnimation = InfiniteRadialAnimationWidget( button, button->height() / 2); AddChildToWidgetCenter(button, loadingAnimation); - loadingAnimation->showOn(state->confirmButtonBusy.value()); + loadingAnimation->showOn(state->buyStars.loadingValue()); } const auto paddings = rect::m::sum::h(st::boxRowPadding); button->widthValue() | rpl::filter([=] { @@ -699,4 +659,66 @@ Type CreditsId() { return Credits::Id(); } +BuyStarsHandler::BuyStarsHandler() = default; + +BuyStarsHandler::~BuyStarsHandler() = default; + +Fn BuyStarsHandler::handler( + std::shared_ptr<::Main::SessionShow> show, + Fn paid) { + const auto optionsBox = [=](not_null box) { + box->setStyle(st::giveawayGiftCodeBox); + box->setWidth(st::boxWideWidth); + box->setTitle(tr::lng_credits_summary_options_subtitle()); + const auto inner = box->verticalLayout(); + const auto self = show->session().user(); + const auto options = _api + ? _api->options() + : Data::CreditTopupOptions(); + const auto amount = StarsAmount(); + const auto weak = Ui::MakeWeak(box); + FillCreditOptions(show, inner, self, amount, [=] { + if (const auto strong = weak.data()) { + strong->closeBox(); + } + if (const auto onstack = paid) { + onstack(); + } + }, nullptr, options); + + const auto button = box->addButton(tr::lng_close(), [=] { + box->closeBox(); + }); + const auto buttonWidth = st::boxWideWidth + - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); + }; + return crl::guard(this, [=] { + if (_api && !_api->options().empty()) { + _loading = false; + show->show(Box(crl::guard(this, optionsBox))); + } else { + _loading = true; + const auto user = show->session().user(); + _api = std::make_unique(user); + _api->request( + ) | rpl::start_with_error_done([=](const QString &error) { + _loading = false; + show->showToast(error); + }, [=] { + _loading = false; + show->show(Box(crl::guard(this, optionsBox))); + }, _lifetime); + } + }); +} + +rpl::producer BuyStarsHandler::loadingValue() const { + return _loading.value(); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_credits.h b/Telegram/SourceFiles/settings/settings_credits.h index c0e32e1be..27692cd3d 100644 --- a/Telegram/SourceFiles/settings/settings_credits.h +++ b/Telegram/SourceFiles/settings/settings_credits.h @@ -9,9 +9,33 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" +namespace Api { +class CreditsTopupOptions; +} // namespace Api + +namespace Main { +class SessionShow; +} // namespace Main + namespace Settings { [[nodiscard]] Type CreditsId(); -} // namespace Settings +class BuyStarsHandler final : public base::has_weak_ptr { +public: + BuyStarsHandler(); + ~BuyStarsHandler(); + [[nodiscard]] Fn handler( + std::shared_ptr<::Main::SessionShow> show, + Fn paid = nullptr); + [[nodiscard]] rpl::producer loadingValue() const; + +private: + std::unique_ptr _api; + rpl::variable _loading; + rpl::lifetime _lifetime; + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 9e3153117..4056c3bfb 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -178,10 +178,10 @@ private: const Data::CreditsHistoryEntry &entry) { return !entry.stargift ? Data::SavedStarGiftId() - : (entry.bareGiftListPeerId && entry.giftSavedId) + : (entry.bareEntryOwnerId && entry.giftChannelSavedId) ? Data::SavedStarGiftId::Chat( - session->data().peer(PeerId(entry.bareGiftListPeerId)), - entry.giftSavedId) + session->data().peer(PeerId(entry.bareEntryOwnerId)), + entry.giftChannelSavedId) : Data::SavedStarGiftId::User(MsgId(entry.bareMsgId)); } @@ -189,7 +189,7 @@ void ToggleStarGiftSaved( std::shared_ptr show, Data::SavedStarGiftId savedId, bool save, - Fn done) { + Fn done = nullptr) { using Flag = MTPpayments_SaveStarGift::Flag; const auto api = &show->session().api(); const auto channelGift = savedId.chat(); @@ -197,7 +197,15 @@ void ToggleStarGiftSaved( MTP_flags(save ? Flag(0) : Flag::f_unsave), Api::InputSavedStarGiftId(savedId) )).done([=] { - done(true); + using GiftAction = Data::GiftUpdate::Action; + show->session().data().notifyGiftUpdate({ + .id = savedId, + .action = (save ? GiftAction::Save : GiftAction::Unsave), + }); + + if (const auto onstack = done) { + onstack(true); + } show->showToast((save ? (channelGift ? tr::lng_gift_display_done_channel @@ -206,7 +214,58 @@ void ToggleStarGiftSaved( ? tr::lng_gift_display_done_hide_channel : tr::lng_gift_display_done_hide))(tr::now)); }).fail([=](const MTP::Error &error) { - done(false); + if (const auto onstack = done) { + onstack(false); + } + show->showToast(error.type()); + }).send(); +} + +void ToggleStarGiftPinned( + std::shared_ptr show, + Data::SavedStarGiftId savedId, + std::vector already, + bool pinned, + Fn done = nullptr) { + already.erase(ranges::remove(already, savedId), end(already)); + if (pinned) { + already.insert(begin(already), savedId); + const auto limit = show->session().appConfig().pinnedGiftsLimit(); + if (already.size() > limit) { + already.erase(begin(already) + limit, end(already)); + } + } + + auto inputs = QVector(); + inputs.reserve(already.size()); + for (const auto &id : already) { + inputs.push_back(Api::InputSavedStarGiftId(id)); + } + + const auto api = &show->session().api(); + const auto peer = savedId.chat() + ? savedId.chat() + : show->session().user(); + api->request(MTPpayments_ToggleStarGiftsPinnedToTop( + peer->input, + MTP_vector(std::move(inputs)) + )).done([=] { + using GiftAction = Data::GiftUpdate::Action; + show->session().data().notifyGiftUpdate({ + .id = savedId, + .action = (pinned ? GiftAction::Pin : GiftAction::Unpin), + }); + + if (const auto onstack = done) { + onstack(true); + } + if (pinned) { + show->showToast(tr::lng_gift_pinned_done(tr::now)); + } + }).fail([=](const MTP::Error &error) { + if (const auto onstack = done) { + onstack(false); + } show->showToast(error.type()); }).send(); } @@ -410,10 +469,7 @@ SubscriptionRightLabel PaintSubscriptionRightLabelCallback( .append(QChar::Space) .append(Lang::FormatCountDecimal(amount)), kMarkupTextOptions, - Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [] {}, - }); + Core::TextContext({ .session = session })); const auto &font = text->style()->font; const auto &statusFont = st::contactsStatusFont; const auto status = tr::lng_group_invite_joined_right(tr::now); @@ -569,8 +625,8 @@ void FillCreditOptions( if (const auto strong = weak.data()) { strong->window()->setFocus(); if (result == Payments::CheckoutResult::Paid) { - if (paid) { - paid(); + if (const auto onstack = paid) { + onstack(); } } } @@ -762,10 +818,10 @@ void BoostCreditsBox( st, std::move(textWithEntities), kMarkupTextOptions, - Core::MarkedTextContext{ + Core::TextContext({ .session = session, - .customEmojiRepaint = [=] { badge->update(); }, - }); + .repaint = [=] { badge->update(); }, + })); badge->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(badge); @@ -849,26 +905,75 @@ void FillUniqueGiftMenu( std::shared_ptr show, not_null menu, const Data::CreditsHistoryEntry &e, + SavedStarGiftMenuType type, CreditsEntryBoxStyleOverrides st) { - Expects(e.uniqueGift != nullptr); + const auto session = &show->session(); + const auto savedId = EntryToSavedStarGiftId(session, e); + const auto giftChannel = savedId.chat(); + const auto canToggle = savedId + && e.id.isEmpty() + && (e.in || (giftChannel && giftChannel->canManageGifts())) + && !e.giftTransferred + && !e.giftRefunded; const auto unique = e.uniqueGift; - const auto local = u"nft/"_q + unique->slug; - const auto url = show->session().createInternalLinkFull(local); - menu->addAction(tr::lng_context_copy_link(tr::now), [=] { - TextUtilities::SetClipboardText({ url }); - show->showToast(tr::lng_channel_public_link_copied(tr::now)); - }, st.link ? st.link : &st::menuIconLink); + if (unique + && canToggle + && e.savedToProfile + && type == SavedStarGiftMenuType::List) { + const auto already = [session, entries = e.pinnedSavedGifts] { + Expects(entries != nullptr); - const auto shareBoxSt = st.shareBox; - menu->addAction(tr::lng_chat_link_share(tr::now), [=] { - FastShareLink( - show, - url, - shareBoxSt ? *shareBoxSt : ShareBoxStyleOverrides()); - }, st.share ? st.share : &st::menuIconShare); + auto list = entries(); + auto result = std::vector(); + result.reserve(list.size()); + for (const auto &entry : list) { + result.push_back(EntryToSavedStarGiftId(session, entry)); + } + return result; + }; + if (e.giftPinned) { + menu->addAction(tr::lng_context_unpin_from_top(tr::now), [=] { + ToggleStarGiftPinned(show, savedId, already(), false); + }, st.unpin ? st.unpin : &st::menuIconUnpin); + } else { + menu->addAction(tr::lng_context_pin_to_top(tr::now), [=] { + ToggleStarGiftPinned(show, savedId, already(), true); + }, st.pin ? st.pin : &st::menuIconPin); + } + } + if (unique) { + const auto local = u"nft/"_q + unique->slug; + const auto url = show->session().createInternalLinkFull(local); + menu->addAction(tr::lng_context_copy_link(tr::now), [=] { + TextUtilities::SetClipboardText({ url }); + show->showToast(tr::lng_channel_public_link_copied(tr::now)); + }, st.link ? st.link : &st::menuIconLink); - const auto savedId = EntryToSavedStarGiftId(&show->session(), e); + const auto shareBoxSt = st.shareBox; + menu->addAction(tr::lng_chat_link_share(tr::now), [=] { + FastShareLink( + show, + url, + shareBoxSt ? *shareBoxSt : ShareBoxStyleOverrides()); + }, st.share ? st.share : &st::menuIconShare); + } + + if (canToggle && type == SavedStarGiftMenuType::List) { + if (e.savedToProfile) { + menu->addAction(tr::lng_gift_menu_hide(tr::now), [=] { + ToggleStarGiftSaved(show, savedId, false); + }, st.hide ? st.hide : &st::menuIconStealth); + } else { + menu->addAction(tr::lng_gift_menu_show(tr::now), [=] { + ToggleStarGiftSaved(show, savedId, true); + }, st.show ? st.show : &st::menuIconShowInChat); + } + } + + if (!unique) { + return; + } const auto transfer = savedId && (savedId.isUser() ? e.in : savedId.chat()->canTransferGifts()) && (unique->starsForTransfer >= 0); @@ -924,6 +1029,10 @@ CreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle() { .transfer = &st::darkGiftTransfer, .wear = &st::darkGiftNftWear, .takeoff = &st::darkGiftNftTakeOff, + .show = &st::darkGiftShow, + .hide = &st::darkGiftHide, + .pin = &st::darkGiftPin, + .unpin = &st::darkGiftUnpin, .shareBox = std::make_shared( DarkShareBoxStyle()), .giftWearBox = std::make_shared( @@ -950,9 +1059,9 @@ void GenericCreditsEntryBox( const auto giftToSelf = isStarGift && (e.barePeerId == selfPeerId) && (e.in || e.bareGiftOwnerId == selfPeerId); - const auto giftChannel = (isStarGift && e.giftSavedId) + const auto giftChannel = (isStarGift && e.giftChannelSavedId) ? session->data().peer( - PeerId(e.bareGiftListPeerId))->asChannel() + PeerId(e.bareEntryOwnerId))->asChannel() : nullptr; const auto giftToChannel = (giftChannel != nullptr); const auto giftToChannelCanManage = giftToChannel @@ -1029,7 +1138,8 @@ void GenericCreditsEntryBox( AddSkip(content, st::defaultVerticalListSkip * 2); AddUniqueCloseButton(box, st, [=](not_null menu) { - FillUniqueGiftMenu(show, menu, e, st); + const auto type = SavedStarGiftMenuType::View; + FillUniqueGiftMenu(show, menu, e, type, st); }); } else if (const auto callback = Ui::PaintPreviewCallback(session, e)) { const auto thumb = content->add(object_ptr>( @@ -1051,7 +1161,7 @@ void GenericCreditsEntryBox( content->add(object_ptr>( content, GenericEntryPhoto(content, callback, stUser.photoSize))); - } else if (peer && !e.gift) { + } else if (peer && !e.gift && !e.premiumMonthsForStars) { if (e.subscriptionUntil.isNull() && s.until.isNull()) { content->add(object_ptr>( content, @@ -1061,7 +1171,7 @@ void GenericCreditsEntryBox( content, SubscriptionUserpic(content, peer, stUser.photoSize))); } - } else if (e.gift || isPrize) { + } else if (e.gift || isPrize || e.premiumMonthsForStars) { struct State final { DocumentData *sticker = nullptr; std::shared_ptr media; @@ -1079,7 +1189,9 @@ void GenericCreditsEntryBox( auto &packs = session->giftBoxStickersPacks(); const auto document = starGiftSticker ? starGiftSticker - : packs.lookup(packs.monthsForStars(e.credits.whole())); + : packs.lookup(e.premiumMonthsForStars + ? e.premiumMonthsForStars + : packs.monthsForStars(e.credits.whole())); if (document && document->sticker()) { state->sticker = document; state->media = document->createMediaView(); @@ -1159,6 +1271,13 @@ void GenericCreditsEntryBox( ? tr::lng_credits_box_history_entry_giveaway_name(tr::now) : (!e.subscriptionUntil.isNull() && e.title.isEmpty()) ? tr::lng_credits_box_history_entry_subscription(tr::now) + : e.paidMessagesCount + ? tr::lng_credits_paid_messages_fee( + tr::now, + lt_count, + e.paidMessagesCount) + : e.premiumMonthsForStars + ? tr::lng_premium_summary_title(tr::now) : !e.title.isEmpty() ? e.title : e.starrefCommission @@ -1206,10 +1325,10 @@ void GenericCreditsEntryBox( object_ptr( content, st::defaultTextStyle.font->height)); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = session, - .customEmojiRepaint = [=] { amount->update(); }, - }; + .repaint = [=] { amount->update(); }, + }); if (e.soldOutInfo) { text->setText( st::defaultTextStyle, @@ -1315,6 +1434,8 @@ void GenericCreditsEntryBox( rpl::single(e.description), st::creditsBoxAbout))); } + + const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); if (!uniqueGift && starGiftCanManage) { Ui::AddSkip(content); const auto about = box->addRow( @@ -1380,11 +1501,6 @@ void GenericCreditsEntryBox( } else if (isStarGift) { } else if (e.gift || isPrize) { Ui::AddSkip(content); - const auto arrow = Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); auto link = tr::lng_credits_box_history_entry_gift_about_link( lt_emoji, rpl::single(arrow), @@ -1409,7 +1525,32 @@ void GenericCreditsEntryBox( lt_link, std::move(link), Ui::Text::RichLangValue), - { .session = session }, + Core::TextContext({ .session = session }), + st::creditsBoxAbout))); + } else if (e.paidMessagesCommission && e.barePeerId) { + Ui::AddSkip(content); + auto link = tr::lng_credits_paid_messages_fee_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([id = e.barePeerId](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + u"internal:edit_paid_messages_fee/"_q + QString::number(id)); + }); + const auto percent = 100. - (e.paidMessagesCommission / 10.); + box->addRow(object_ptr>( + box, + Ui::CreateLabelWithCustomEmoji( + box, + tr::lng_credits_paid_messages_fee_about( + lt_percent, + rpl::single( + Ui::Text::Bold(QString::number(percent) + '%')), + lt_link, + std::move(link), + Ui::Text::RichLangValue), + Core::TextContext({ .session = session }), st::creditsBoxAbout))); } @@ -1429,21 +1570,12 @@ void GenericCreditsEntryBox( const auto showSection = !e.fromGiftsList; const auto savedId = EntryToSavedStarGiftId(&show->session(), e); const auto done = [=](bool ok) { - if (ok) { - using GiftAction = Data::GiftUpdate::Action; - show->session().data().notifyGiftUpdate({ - .id = savedId, - .action = (save - ? GiftAction::Save - : GiftAction::Unsave), - }); - if (showSection) { - if (const auto window = show->resolveWindow()) { - window->showSection( - std::make_shared( - window->session().user(), - Info::Section::Type::PeerGifts)); - } + if (ok && showSection) { + if (const auto window = show->resolveWindow()) { + window->showSection( + std::make_shared( + window->session().user(), + Info::Section::Type::PeerGifts)); } } if (const auto strong = weak.data()) { @@ -1695,7 +1827,7 @@ void GenericCreditsEntryBox( : canUpgradeFree ? tr::lng_gift_upgrade_free() : (canToggle && !e.savedToProfile) - ? (e.giftSavedId + ? (e.giftChannelSavedId ? tr::lng_gift_show_on_channel : tr::lng_gift_show_on_page)() : tr::lng_box_ok())); @@ -1864,46 +1996,62 @@ void GlobalStarGiftBox( st); } +Data::CreditsHistoryEntry SavedStarGiftEntry( + not_null owner, + const Data::SavedStarGift &data) { + const auto chatGiftPeer = data.manageId.chat(); + return { + .description = data.message, + .date = base::unixtime::parse(data.date), + .credits = StarsAmount(data.info.stars), + .bareMsgId = uint64(data.manageId.userMessageId().bare), + .barePeerId = data.fromId.value, + .bareGiftStickerId = data.info.document->id, + .bareGiftOwnerId = owner->id.value, + .bareActorId = data.fromId.value, + .bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0, + .giftChannelSavedId = data.manageId.chatSavedId(), + .stargiftId = data.info.id, + .uniqueGift = data.info.unique, + .peerType = Data::CreditsHistoryEntry::PeerType::Peer, + .limitedCount = data.info.limitedCount, + .limitedLeft = data.info.limitedLeft, + .starsConverted = int(data.starsConverted), + .starsToUpgrade = int(data.info.starsToUpgrade), + .starsUpgradedBySender = int(data.starsUpgradedBySender), + .converted = false, + .anonymous = data.anonymous, + .stargift = true, + .giftPinned = data.pinned, + .savedToProfile = !data.hidden, + .fromGiftsList = true, + .canUpgradeGift = data.upgradable, + .in = data.mine, + .gift = true, + }; +} + void SavedStarGiftBox( not_null box, not_null controller, not_null owner, const Data::SavedStarGift &data) { - const auto chatGiftPeer = data.manageId.chat(); Settings::ReceiptCreditsBox( box, controller, - Data::CreditsHistoryEntry{ - .description = data.message, - .date = base::unixtime::parse(data.date), - .credits = StarsAmount(data.info.stars), - .bareMsgId = uint64(data.manageId.userMessageId().bare), - .barePeerId = data.fromId.value, - .bareGiftStickerId = data.info.document->id, - .bareGiftOwnerId = owner->id.value, - .bareActorId = data.fromId.value, - .bareGiftListPeerId = chatGiftPeer ? chatGiftPeer->id.value : 0, - .giftSavedId = data.manageId.chatSavedId(), - .stargiftId = data.info.id, - .uniqueGift = data.info.unique, - .peerType = Data::CreditsHistoryEntry::PeerType::Peer, - .limitedCount = data.info.limitedCount, - .limitedLeft = data.info.limitedLeft, - .starsConverted = int(data.starsConverted), - .starsToUpgrade = int(data.info.starsToUpgrade), - .starsUpgradedBySender = int(data.starsUpgradedBySender), - .converted = false, - .anonymous = data.anonymous, - .stargift = true, - .savedToProfile = !data.hidden, - .fromGiftsList = true, - .canUpgradeGift = data.upgradable, - .in = data.mine, - .gift = true, - }, + SavedStarGiftEntry(owner, data), Data::SubscriptionEntry()); } +void FillSavedStarGiftMenu( + std::shared_ptr show, + not_null menu, + const Data::CreditsHistoryEntry &e, + SavedStarGiftMenuType type, + CreditsEntryBoxStyleOverrides st) { + FillUniqueGiftMenu(show, menu, e, type, st); +} + void StarGiftViewBox( not_null box, not_null controller, @@ -1927,8 +2075,8 @@ void StarGiftViewBox( ? data.unique->ownerId.value : toId.value), .bareActorId = (toChannel ? data.channelFrom->id.value : 0), - .bareGiftListPeerId = (toChannel ? data.channel->id.value : 0), - .giftSavedId = data.channelSavedId, + .bareEntryOwnerId = (toChannel ? data.channel->id.value : 0), + .giftChannelSavedId = data.channelSavedId, .stargiftId = data.stargiftId, .uniqueGift = data.unique, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, @@ -2090,6 +2238,10 @@ void SmallBalanceBox( return QString(); }, [&](SmallBalanceStarGift value) { return owner->peer(value.recipientId)->shortName(); + }, [&](SmallBalanceForMessage value) { + return value.recipientId + ? owner->peer(value.recipientId)->shortName() + : QString(); }); auto needed = show->session().credits().balanceValue( @@ -2128,6 +2280,14 @@ void SmallBalanceBox( lt_user, rpl::single(Ui::Text::Bold(name)), Ui::Text::RichLangValue) + : v::is(source) + ? (name.isEmpty() + ? tr::lng_credits_small_balance_for_messages( + Ui::Text::RichLangValue) + : tr::lng_credits_small_balance_for_message( + lt_user, + rpl::single(Ui::Text::Bold(name)), + Ui::Text::RichLangValue)) : name.isEmpty() ? tr::lng_credits_small_balance_fallback( Ui::Text::RichLangValue) @@ -2308,10 +2468,6 @@ void AddWithdrawalWidget( st::settingsPremiumIconStar, { 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, true)); - const auto context = Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [=] { label->update(); }, - }; using Balance = rpl::variable; const auto currentBalance = input->lifetime().make_state( rpl::duplicate(availableBalanceValue)); @@ -2329,7 +2485,7 @@ void AddWithdrawalWidget( lt_emoji, buttonEmoji, Ui::Text::RichLangValue), - context); + Core::TextContext({ .session = session })); } }; QObject::connect(input, &Ui::MaskedInputField::changed, process); @@ -2397,10 +2553,10 @@ void AddWithdrawalWidget( constexpr auto kDateUpdateInterval = crl::time(250); const auto was = base::unixtime::serialize(dt); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = session, - .customEmojiRepaint = [=] { lockedLabel->update(); }, - }; + .repaint = [=] { lockedLabel->update(); }, + }); const auto emoji = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( st::chatSimilarLockedIcon, @@ -2477,11 +2633,7 @@ void AddWithdrawalWidget( Ui::AddSkip(container); Ui::AddSkip(container); - const auto arrow = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); + const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); auto about = Ui::CreateLabelWithCustomEmoji( container, tr::lng_bot_earn_learn_credits_out_about( @@ -2496,7 +2648,7 @@ void AddWithdrawalWidget( tr::lng_bot_earn_balance_about_url(tr::now)); }), Ui::Text::RichLangValue), - { .session = session }, + Core::TextContext({ .session = session }), st::boxDividerLabel); Ui::AddSkip(container); container->add(object_ptr( diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 599ced1f3..8a1ee55bc 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -52,6 +52,7 @@ namespace Ui { class GenericBox; class RpWidget; class VerticalLayout; +class PopupMenu; } // namespace Ui namespace Settings { @@ -112,6 +113,10 @@ struct CreditsEntryBoxStyleOverrides { const style::icon *transfer = nullptr; const style::icon *wear = nullptr; const style::icon *takeoff = nullptr; + const style::icon *show = nullptr; + const style::icon *hide = nullptr; + const style::icon *pin = nullptr; + const style::icon *unpin = nullptr; std::shared_ptr shareBox; std::shared_ptr giftWearBox; }; @@ -149,11 +154,26 @@ void GlobalStarGiftBox( std::shared_ptr show, const Data::StarGift &data, CreditsEntryBoxStyleOverrides st = {}); + +[[nodiscard]] Data::CreditsHistoryEntry SavedStarGiftEntry( + not_null owner, + const Data::SavedStarGift &data); void SavedStarGiftBox( not_null box, not_null controller, not_null owner, const Data::SavedStarGift &data); +enum class SavedStarGiftMenuType { + List, + View, +}; +void FillSavedStarGiftMenu( + std::shared_ptr show, + not_null menu, + const Data::CreditsHistoryEntry &e, + SavedStarGiftMenuType type, + CreditsEntryBoxStyleOverrides st = {}); + void StarGiftViewBox( not_null box, not_null controller, @@ -200,12 +220,16 @@ struct SmallBalanceDeepLink { struct SmallBalanceStarGift { PeerId recipientId; }; +struct SmallBalanceForMessage { + PeerId recipientId; +}; struct SmallBalanceSource : std::variant< SmallBalanceBot, SmallBalanceReaction, SmallBalanceSubscription, SmallBalanceDeepLink, - SmallBalanceStarGift> { + SmallBalanceStarGift, + SmallBalanceForMessage> { using variant::variant; }; diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index ddcc9071c..995591894 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -199,11 +199,11 @@ void FilterRowButton::updateData( st::contactsNameStyle, title.text, kMarkupTextOptions, - Core::MarkedTextContext{ + Core::TextContext({ .session = _session, - .customEmojiRepaint = [=] { update(); }, + .repaint = [=] { update(); }, .customEmojiLoopLimit = title.isStatic ? -1 : 0, - }); + })); _icon = Ui::ComputeFilterIcon(filter); _colorIndex = filter.colorIndex(); if (!ignoreCount) { diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 7d6172a62..45f054bf7 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -13,7 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/click_handler_types.h" #include "core/local_url_handlers.h" // Core::TryConvertUrlToLocal. -#include "core/ui_integration.h" // MarkedTextContext. +#include "core/ui_integration.h" // TextContext. #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_emoji_statuses.h" @@ -792,11 +792,9 @@ void TopBarUser::updateTitle( lt_link, { .text = text, .entities = entities, }, Ui::Text::WithEntities); - const auto context = Core::MarkedTextContext{ - .session = &controller->session(), - .customEmojiRepaint = [=] { _title->update(); }, - }; - _title->setMarkedText(std::move(title), context); + _title->setMarkedText( + std::move(title), + Core::TextContext({ .session = &controller->session() })); auto link = std::make_shared([=, stickerSetIdentifier = stickerInfo->set] { setPaused(true); diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 950b15b74..a843d7f04 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -203,7 +203,8 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPint(), // quick_reply_shortcut_id MTPlong(), // effect MTPFactCheck(), - MTPint() // report_delivery_until_date + MTPint(), // report_delivery_until_date + MTPlong() // paid_message_stars ).match([&](const MTPDmessage &data) { return history->makeMessage( history->nextNonHistoryEntryId(), diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index 09ae65ad8..c605ed28d 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -356,10 +356,16 @@ void AddMessagesPrivacyButton( not_null container) { const auto session = &controller->session(); const auto privacy = &session->api().globalPrivacy(); - auto label = rpl::conditional( + auto label = rpl::combine( privacy->newRequirePremium(), - tr::lng_edit_privacy_contacts_and_premium(), - tr::lng_edit_privacy_everyone()); + privacy->newChargeStars() + ) | rpl::map([=](bool requirePremium, int chargeStars) { + return chargeStars + ? tr::lng_edit_privacy_paid() + : requirePremium + ? tr::lng_edit_privacy_contacts_and_premium() + : tr::lng_edit_privacy_everyone(); + }) | rpl::flatten_latest(); const auto &st = st::settingsButtonNoIcon; const auto button = AddButtonWithLabel( container, diff --git a/Telegram/SourceFiles/settings/settings_shortcuts.cpp b/Telegram/SourceFiles/settings/settings_shortcuts.cpp index 82693c726..0fd7cf5fb 100644 --- a/Telegram/SourceFiles/settings/settings_shortcuts.cpp +++ b/Telegram/SourceFiles/settings/settings_shortcuts.cpp @@ -21,12 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" #include "styles/style_settings.h" -#include -#include - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) -#include -#endif +#include namespace Settings { namespace { @@ -388,7 +383,6 @@ struct Labeled { } const auto key = static_cast(e.get()); const auto m = key->modifiers(); - const auto integration = QGuiApplicationPrivate::platformIntegration(); const auto k = key->key(); const auto clear = !m && (k == Qt::Key_Backspace || k == Qt::Key_Delete); @@ -407,22 +401,18 @@ struct Labeled { const auto r = [&] { auto result = int(k); if (m & Qt::ShiftModifier) { + const auto keys = QKeyMapper::possibleKeys(key); + for (const auto &possible : keys) { #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - const auto mapper = integration->keyMapper(); - const auto list = mapper->possibleKeyCombinations(key); - for (const auto &possible : list) { if (possible.keyboardModifiers() == m) { return int(possible.key()); } - } #else // Qt >= 6.7.0 - const auto keys = integration->possibleKeys(key); - for (const auto possible : keys) { if (possible > int(m)) { return possible - int(m); } - } #endif // Qt < 6.7.0 + } } return result; }(); diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 7233e0f45..ea673e503 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -86,7 +86,7 @@ enum { // Local Storage Keys lskSavedGifsOld = 0x0e, // no data lskSavedGifs = 0x0f, // no data lskStickersKeys = 0x10, // no data - lskTrustedBots = 0x11, // no data + lskTrustedPeers = 0x11, // no data lskFavedStickers = 0x12, // no data lskExportSettings = 0x13, // no data lskBackgroundOld = 0x14, // no data @@ -220,7 +220,7 @@ base::flat_set Account::collectGoodNames() const { _legacyBackgroundKeyDay, _recentHashtagsAndBotsKey, _exportSettingsKey, - _trustedBotsKey, + _trustedPeersKey, _installedMasksKey, _recentMasksKey, _archivedMasksKey, @@ -308,7 +308,7 @@ Account::ReadMapResult Account::readMapWith( base::flat_map draftsMap; base::flat_map draftCursorsMap; base::flat_map draftsNotReadMap; - quint64 locationsKey = 0, reportSpamStatusesKey = 0, trustedBotsKey = 0; + quint64 locationsKey = 0, reportSpamStatusesKey = 0, trustedPeersKey = 0; quint64 recentStickersKeyOld = 0; quint64 installedStickersKey = 0, featuredStickersKey = 0, recentStickersKey = 0, favedStickersKey = 0, archivedStickersKey = 0; quint64 installedMasksKey = 0, recentMasksKey = 0, archivedMasksKey = 0; @@ -371,8 +371,8 @@ Account::ReadMapResult Account::readMapWith( map.stream >> reportSpamStatusesKey; ClearKey(reportSpamStatusesKey, _basePath); } break; - case lskTrustedBots: { - map.stream >> trustedBotsKey; + case lskTrustedPeers: { + map.stream >> trustedPeersKey; } break; case lskRecentStickersOld: { map.stream >> recentStickersKeyOld; @@ -459,7 +459,7 @@ Account::ReadMapResult Account::readMapWith( _draftsNotReadMap = draftsNotReadMap; _locationsKey = locationsKey; - _trustedBotsKey = trustedBotsKey; + _trustedPeersKey = trustedPeersKey; _recentStickersKeyOld = recentStickersKeyOld; _installedStickersKey = installedStickersKey; _featuredStickersKey = featuredStickersKey; @@ -573,7 +573,7 @@ void Account::writeMap() { if (!_draftsMap.empty()) mapSize += sizeof(quint32) * 2 + _draftsMap.size() * sizeof(quint64) * 2; if (!_draftCursorsMap.empty()) mapSize += sizeof(quint32) * 2 + _draftCursorsMap.size() * sizeof(quint64) * 2; if (_locationsKey) mapSize += sizeof(quint32) + sizeof(quint64); - if (_trustedBotsKey) mapSize += sizeof(quint32) + sizeof(quint64); + if (_trustedPeersKey) mapSize += sizeof(quint32) + sizeof(quint64); if (_recentStickersKeyOld) mapSize += sizeof(quint32) + sizeof(quint64); if (_installedStickersKey || _featuredStickersKey || _recentStickersKey || _archivedStickersKey) { mapSize += sizeof(quint32) + 4 * sizeof(quint64); @@ -619,8 +619,8 @@ void Account::writeMap() { if (_locationsKey) { mapData.stream << quint32(lskLocations) << quint64(_locationsKey); } - if (_trustedBotsKey) { - mapData.stream << quint32(lskTrustedBots) << quint64(_trustedBotsKey); + if (_trustedPeersKey) { + mapData.stream << quint32(lskTrustedPeers) << quint64(_trustedPeersKey); } if (_recentStickersKeyOld) { mapData.stream << quint32(lskRecentStickersOld) << quint64(_recentStickersKeyOld); @@ -693,7 +693,7 @@ void Account::reset() { _draftsMap.clear(); _draftCursorsMap.clear(); _draftsNotReadMap.clear(); - _locationsKey = _trustedBotsKey = 0; + _locationsKey = _trustedPeersKey = 0; _recentStickersKeyOld = 0; _installedStickersKey = 0; _featuredStickersKey = 0; @@ -3147,126 +3147,198 @@ void Account::readSelf( } } -void Account::writeTrustedBots() { - if (_trustedBots.empty()) { - if (_trustedBotsKey) { - ClearKey(_trustedBotsKey, _basePath); - _trustedBotsKey = 0; +void Account::writeTrustedPeers() { + if (_trustedPeers.empty() && _trustedPayPerMessage.empty()) { + if (_trustedPeersKey) { + ClearKey(_trustedPeersKey, _basePath); + _trustedPeersKey = 0; writeMapDelayed(); } return; } - if (!_trustedBotsKey) { - _trustedBotsKey = GenerateKey(_basePath); + if (!_trustedPeersKey) { + _trustedPeersKey = GenerateKey(_basePath); writeMapQueued(); } - quint32 size = sizeof(qint32) + _trustedBots.size() * sizeof(quint64); + quint32 size = sizeof(qint32) + + _trustedPeers.size() * sizeof(quint64) + + sizeof(qint32) + + _trustedPayPerMessage.size() * (sizeof(quint64) + sizeof(qint32)); EncryptedDescriptor data(size); - data.stream << qint32(_trustedBots.size()); - for (const auto &[peerId, mask] : _trustedBots) { - // value: 8 bit mask, 56 bit bot peer_id. + data.stream << qint32(_trustedPeers.size()); + for (const auto &[peerId, mask] : _trustedPeers) { + // value: 8 bit mask, 56 bit peer_id. auto value = SerializePeerId(peerId); Assert((value >> 56) == 0); value |= (quint64(mask) << 56); data.stream << value; } + data.stream << qint32(_trustedPayPerMessage.size()); + for (const auto &[peerId, stars] : _trustedPayPerMessage) { + data.stream << SerializePeerId(peerId) << qint32(stars); + } - FileWriteDescriptor file(_trustedBotsKey, _basePath); + FileWriteDescriptor file(_trustedPeersKey, _basePath); file.writeEncrypted(data, _localKey); } -void Account::readTrustedBots() { - if (_trustedBotsRead) { +void Account::readTrustedPeers() { + if (_trustedPeersRead) { return; } - _trustedBotsRead = true; - if (!_trustedBotsKey) { + _trustedPeersRead = true; + if (!_trustedPeersKey) { return; } FileReadDescriptor trusted; - if (!ReadEncryptedFile(trusted, _trustedBotsKey, _basePath, _localKey)) { - ClearKey(_trustedBotsKey, _basePath); - _trustedBotsKey = 0; + if (!ReadEncryptedFile(trusted, _trustedPeersKey, _basePath, _localKey)) { + ClearKey(_trustedPeersKey, _basePath); + _trustedPeersKey = 0; writeMapDelayed(); return; } - qint32 size = 0; - trusted.stream >> size; - for (int i = 0; i < size; ++i) { + qint32 trustedCount = 0; + trusted.stream >> trustedCount; + for (int i = 0; i < trustedCount; ++i) { auto value = quint64(); trusted.stream >> value; - const auto mask = base::flags::from_raw( + const auto mask = base::flags::from_raw( uchar(value >> 56)); const auto peerIdSerialized = value & ~(0xFFULL << 56); const auto peerId = DeserializePeerId(peerIdSerialized); - _trustedBots.emplace(peerId, mask); + _trustedPeers.emplace(peerId, mask); } -} - -void Account::markBotTrustedOpenGame(PeerId botId) { - if (isBotTrustedOpenGame(botId)) { + if (trusted.stream.atEnd()) { return; } - const auto i = _trustedBots.find(botId); - if (i == end(_trustedBots)) { - _trustedBots.emplace(botId, BotTrustFlag()); - } else { - i->second &= ~BotTrustFlag::NoOpenGame; + qint32 payPerMessageCount = 0; + trusted.stream >> payPerMessageCount; + const auto owner = _owner->sessionExists() + ? &_owner->session().data() + : nullptr; + for (int i = 0; i < payPerMessageCount; ++i) { + auto value = quint64(); + auto stars = qint32(); + trusted.stream >> value >> stars; + const auto peerId = DeserializePeerId(value); + const auto peer = owner ? owner->peerLoaded(peerId) : nullptr; + const auto now = peer ? peer->starsPerMessage() : stars; + if (now > 0 && now <= stars) { + _trustedPayPerMessage.emplace(peerId, stars); + } + } + if (_trustedPayPerMessage.size() != payPerMessageCount) { + writeTrustedPeers(); } - writeTrustedBots(); } -bool Account::isBotTrustedOpenGame(PeerId botId) { - readTrustedBots(); - const auto i = _trustedBots.find(botId); - return (i != end(_trustedBots)) - && ((i->second & BotTrustFlag::NoOpenGame) == 0); -} - -void Account::markBotTrustedPayment(PeerId botId) { - if (isBotTrustedPayment(botId)) { +void Account::markPeerTrustedOpenGame(PeerId peerId) { + if (isPeerTrustedOpenGame(peerId)) { return; } - const auto i = _trustedBots.find(botId); - if (i == end(_trustedBots)) { - _trustedBots.emplace( - botId, - BotTrustFlag::NoOpenGame | BotTrustFlag::Payment); + const auto i = _trustedPeers.find(peerId); + if (i == end(_trustedPeers)) { + _trustedPeers.emplace(peerId, PeerTrustFlag()); } else { - i->second |= BotTrustFlag::Payment; + i->second &= ~PeerTrustFlag::NoOpenGame; } - writeTrustedBots(); + writeTrustedPeers(); } -bool Account::isBotTrustedPayment(PeerId botId) { - readTrustedBots(); - const auto i = _trustedBots.find(botId); - return (i != end(_trustedBots)) - && ((i->second & BotTrustFlag::Payment) != 0); +bool Account::isPeerTrustedOpenGame(PeerId peerId) { + readTrustedPeers(); + const auto i = _trustedPeers.find(peerId); + return (i != end(_trustedPeers)) + && ((i->second & PeerTrustFlag::NoOpenGame) == 0); } -void Account::markBotTrustedOpenWebView(PeerId botId) { - if (isBotTrustedOpenWebView(botId)) { +void Account::markPeerTrustedPayment(PeerId peerId) { + if (isPeerTrustedPayment(peerId)) { return; } - const auto i = _trustedBots.find(botId); - if (i == end(_trustedBots)) { - _trustedBots.emplace( - botId, - BotTrustFlag::NoOpenGame | BotTrustFlag::OpenWebView); + const auto i = _trustedPeers.find(peerId); + if (i == end(_trustedPeers)) { + _trustedPeers.emplace( + peerId, + PeerTrustFlag::NoOpenGame | PeerTrustFlag::Payment); } else { - i->second |= BotTrustFlag::OpenWebView; + i->second |= PeerTrustFlag::Payment; } - writeTrustedBots(); + writeTrustedPeers(); } -bool Account::isBotTrustedOpenWebView(PeerId botId) { - readTrustedBots(); - const auto i = _trustedBots.find(botId); - return (i != end(_trustedBots)) - && ((i->second & BotTrustFlag::OpenWebView) != 0); +bool Account::isPeerTrustedPayment(PeerId peerId) { + readTrustedPeers(); + const auto i = _trustedPeers.find(peerId); + return (i != end(_trustedPeers)) + && ((i->second & PeerTrustFlag::Payment) != 0); +} + +void Account::markPeerTrustedOpenWebView(PeerId peerId) { + if (isPeerTrustedOpenWebView(peerId)) { + return; + } + const auto i = _trustedPeers.find(peerId); + if (i == end(_trustedPeers)) { + _trustedPeers.emplace( + peerId, + PeerTrustFlag::NoOpenGame | PeerTrustFlag::OpenWebView); + } else { + i->second |= PeerTrustFlag::OpenWebView; + } + writeTrustedPeers(); +} + +bool Account::isPeerTrustedOpenWebView(PeerId peerId) { + readTrustedPeers(); + const auto i = _trustedPeers.find(peerId); + return (i != end(_trustedPeers)) + && ((i->second & PeerTrustFlag::OpenWebView) != 0); +} + +void Account::markPeerTrustedPayForMessage( + PeerId peerId, + int starsPerMessage) { + if (isPeerTrustedPayForMessage(peerId, starsPerMessage)) { + return; + } + const auto i = _trustedPayPerMessage.find(peerId); + if (i == end(_trustedPayPerMessage)) { + _trustedPayPerMessage.emplace(peerId, starsPerMessage); + } else { + i->second = starsPerMessage; + } + writeTrustedPeers(); +} + +bool Account::isPeerTrustedPayForMessage( + PeerId peerId, + int starsPerMessage) { + if (starsPerMessage <= 0) { + return true; + } + readTrustedPeers(); + const auto i = _trustedPayPerMessage.find(peerId); + return (i != end(_trustedPayPerMessage)) + && (i->second >= starsPerMessage); +} + +bool Account::peerTrustedPayForMessageRead() const { + return _trustedPeersRead; +} + +bool Account::hasPeerTrustedPayForMessageEntry(PeerId peerId) const { + return _trustedPayPerMessage.contains(peerId); +} + +void Account::clearPeerTrustedPayForMessage(PeerId peerId) { + const auto i = _trustedPayPerMessage.find(peerId); + if (i != end(_trustedPayPerMessage)) { + _trustedPayPerMessage.erase(i); + writeTrustedPeers(); + } } void Account::enforceModernStorageIdBots() { diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index 497ec55ec..86bdabde6 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -167,12 +167,19 @@ public: const QByteArray& serialized, int32 streamVersion); - void markBotTrustedOpenGame(PeerId botId); - [[nodiscard]] bool isBotTrustedOpenGame(PeerId botId); - void markBotTrustedPayment(PeerId botId); - [[nodiscard]] bool isBotTrustedPayment(PeerId botId); - void markBotTrustedOpenWebView(PeerId botId); - [[nodiscard]] bool isBotTrustedOpenWebView(PeerId botId); + void markPeerTrustedOpenGame(PeerId peerId); + [[nodiscard]] bool isPeerTrustedOpenGame(PeerId peerId); + void markPeerTrustedPayment(PeerId peerId); + [[nodiscard]] bool isPeerTrustedPayment(PeerId peerId); + void markPeerTrustedOpenWebView(PeerId peerId); + [[nodiscard]] bool isPeerTrustedOpenWebView(PeerId peerId); + void markPeerTrustedPayForMessage(PeerId peerId, int starsPerMessage); + [[nodiscard]] bool isPeerTrustedPayForMessage( + PeerId peerId, + int starsPerMessage); + [[nodiscard]] bool peerTrustedPayForMessageRead() const; + [[nodiscard]] bool hasPeerTrustedPayForMessageEntry(PeerId peerId) const; + void clearPeerTrustedPayForMessage(PeerId peerId); void enforceModernStorageIdBots(); [[nodiscard]] Webview::StorageId resolveStorageIdBots(); @@ -203,12 +210,12 @@ private: IncorrectPasscode, Failed, }; - enum class BotTrustFlag : uchar { + enum class PeerTrustFlag : uchar { NoOpenGame = (1 << 0), Payment = (1 << 1), OpenWebView = (1 << 2), }; - friend inline constexpr bool is_flag_type(BotTrustFlag) { return true; }; + friend inline constexpr bool is_flag_type(PeerTrustFlag) { return true; }; [[nodiscard]] base::flat_set collectGoodNames() const; [[nodiscard]] auto prepareReadSettingsContext() const @@ -261,8 +268,8 @@ private: Data::StickersSetFlags readingFlags = 0); void importOldRecentStickers(); - void readTrustedBots(); - void writeTrustedBots(); + void readTrustedPeers(); + void writeTrustedPeers(); void readMediaLastPlaybackPositions(); void writeMediaLastPlaybackPositions(); @@ -295,7 +302,7 @@ private: Fn()> _downloadsSerialize; FileKey _locationsKey = 0; - FileKey _trustedBotsKey = 0; + FileKey _trustedPeersKey = 0; FileKey _installedStickersKey = 0; FileKey _featuredStickersKey = 0; FileKey _recentStickersKey = 0; @@ -324,8 +331,9 @@ private: qint32 _cacheTotalTimeLimit = 0; qint32 _cacheBigFileTotalTimeLimit = 0; - base::flat_map> _trustedBots; - bool _trustedBotsRead = false; + base::flat_map> _trustedPeers; + base::flat_map _trustedPayPerMessage; + bool _trustedPeersRead = false; bool _readingUserSettings = false; bool _recentHashtagsAndBotsWereRead = false; bool _searchSuggestionsRead = false; diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp index d76d6802d..fb0300664 100644 --- a/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp @@ -224,7 +224,9 @@ void CollectibleInfoBox( object_ptr(box, st::collectibleInfo), st::collectibleInfoPadding); label->setAttribute(Qt::WA_TransparentForMouseEvents); - label->setMarkedText(text, details.tonEmojiContext()); + auto context = details.tonEmojiContext; + context.repaint = [label] { label->update(); }; + label->setMarkedText(text, context); const auto more = box->addRow( object_ptr( diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.h b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h index 8b80b9650..9080819c9 100644 --- a/Telegram/SourceFiles/ui/boxes/collectible_info_box.h +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h @@ -34,7 +34,7 @@ struct CollectibleInfo { struct CollectibleDetails { TextWithEntities tonEmoji; - Fn tonEmojiContext; + Text::MarkedContext tonEmojiContext; }; void CollectibleInfoBox( diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp index 8c1a68b20..f868cc3e9 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/boxes/edit_invite_link_session.h" +#include "core/ui_integration.h" // TextContext #include "data/components/credits.h" #include "data/data_peer.h" #include "data/data_session.h" @@ -128,18 +129,13 @@ InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle( state->usdRate = peer->session().credits().rateValue(peer); - const auto arrow = Ui::Text::SingleCustomEmoji( - peer->owner().customEmojiManager().registerInternalEmoji( - st::topicButtonArrow, - st::channelEarnLearnArrowMargins, - true)); auto about = Ui::CreateLabelWithCustomEmoji( container, tr::lng_group_invite_subscription_about( lt_link, tr::lng_group_invite_subscription_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( @@ -147,7 +143,7 @@ InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle( tr::lng_group_invite_subscription_about_url(tr::now)); }), Ui::Text::RichLangValue), - { .session = &peer->session() }, + Core::TextContext({ .session = &peer->session() }), st::boxDividerLabel); Ui::AddSkip(wrap->entity()); Ui::AddSkip(wrap->entity()); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index 3c5e0e707..fb968607b 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -266,6 +266,27 @@ bool PreparedList::hasSpoilerMenu(bool compress) const { return allAreVideo || (allAreMedia && compress); } +std::shared_ptr PrepareFilesBundle( + std::vector groups, + SendFilesWay way, + TextWithTags caption, + bool ctrlShiftEnter) { + auto totalCount = 0; + for (const auto &group : groups) { + totalCount += group.list.files.size(); + } + const auto sendComment = !caption.text.isEmpty() + && (groups.size() != 1 || !groups.front().sentWithCaption()); + return std::make_shared(PreparedBundle{ + .groups = std::move(groups), + .way = way, + .caption = std::move(caption), + .totalCount = totalCount + (sendComment ? 1 : 0), + .sendComment = sendComment, + .ctrlShiftEnter = ctrlShiftEnter, + }); +} + int MaxAlbumItems() { return kMaxAlbumCount; } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index b244f321b..311903a00 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "editor/photo_editor_common.h" +#include "ui/chat/attach/attach_send_files_way.h" #include "ui/rect_part.h" #include @@ -153,6 +154,20 @@ struct PreparedGroup { SendFilesWay way, bool slowmode); +struct PreparedBundle { + std::vector groups; + SendFilesWay way; + TextWithTags caption; + int totalCount = 0; + bool sendComment = false; + bool ctrlShiftEnter = false; +}; +[[nodiscard]] std::shared_ptr PrepareFilesBundle( + std::vector groups, + SendFilesWay way, + TextWithTags caption, + bool ctrlShiftEnter); + [[nodiscard]] int MaxAlbumItems(); [[nodiscard]] bool ValidateThumbDimensions(int width, int height); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 917a1dc2f..eb3f80bf1 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -327,6 +327,11 @@ topicButtonArrowSkip: 8px; topicButtonArrowPosition: point(3px, 3px); topicButtonArrow: icon{{ "dialogs/dialogs_topic_arrow", historyReplyIconFg }}; +textMoreIconEmoji: IconEmoji { + icon: topicButtonArrow; + padding: margins(-2px, 5px, 0px, 0px); +} + msgBotKbIconPadding: 4px; msgBotKbUrlIcon: icon {{ "inline_button_url", msgBotKbIconFg }}; msgBotKbSwitchPmIcon: icon {{ "inline_button_switch", msgBotKbIconFg }}; @@ -1188,6 +1193,7 @@ botEmojiStatusName: FlatLabel(defaultFlatLabel) { maxHeight: 20px; } botEmojiStatusEmoji: FlatLabel(botEmojiStatusName) { + margin: margins(4px, 4px, 4px, 4px); textFg: profileVerifiedCheckBg; } @@ -1224,3 +1230,17 @@ chatUniqueRowSkip: 4px; chatUniqueButtonPadding: margins(12px, 4px, 12px, 16px); markupWebview: icon {{ "chat/markup_webview", windowFg }}; + +newPeerTitleMargin: margins(11px, 16px, 11px, 6px); +newPeerSubtitleMargin: margins(11px, 0px, 11px, 16px); +newPeerNonOfficial: IconEmoji { + icon: icon{{ "chat/mini_info_alert", windowFg }}; + padding: margins(0px, 2px, 0px, 0px); +} +newPeerUserpics: GroupCallUserpics { + size: 16px; + shift: 5px; + stroke: 1px; + align: align(left); +} +newPeerUserpicsPadding: margins(0px, 3px, 0px, 0px); diff --git a/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp b/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp index 077de4f32..020ffa4b0 100644 --- a/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp +++ b/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp @@ -174,7 +174,6 @@ bool ScaledCustomEmoji::readyInDefaultState() { ChatsFilterTagContext &context) { auto i = text.entities.begin(); auto ch = text.text.constData(); - auto &integration = Integration::Instance(); context.loading = false; const auto end = text.text.constData() + text.text.size(); const auto adjust = [&](EntityInText &entity) { @@ -187,9 +186,7 @@ bool ScaledCustomEmoji::readyInDefaultState() { } auto &emoji = context.emoji[data]; if (!emoji) { - emoji = integration.createCustomEmoji( - data, - context.textContext); + emoji = Text::MakeCustomEmoji(data, context.textContext); } if (!emoji->ready()) { context.loading = true; diff --git a/Telegram/SourceFiles/ui/chat/chats_filter_tag.h b/Telegram/SourceFiles/ui/chat/chats_filter_tag.h index 28da74c80..8291b5e33 100644 --- a/Telegram/SourceFiles/ui/chat/chats_filter_tag.h +++ b/Telegram/SourceFiles/ui/chat/chats_filter_tag.h @@ -8,16 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "emoji.h" - -namespace Ui::Text { -class CustomEmoji; -} // namespace Ui::Text +#include "ui/text/text.h" namespace Ui { struct ChatsFilterTagContext { base::flat_map> emoji; - std::any textContext; + Text::MarkedContext textContext; QColor color; bool active = false; bool loading = false; diff --git a/Telegram/SourceFiles/ui/chat/message_bar.h b/Telegram/SourceFiles/ui/chat/message_bar.h index 36b96658e..6132ae446 100644 --- a/Telegram/SourceFiles/ui/chat/message_bar.h +++ b/Telegram/SourceFiles/ui/chat/message_bar.h @@ -26,7 +26,7 @@ struct MessageBarContent { int count = 1; QString title; TextWithEntities text; - std::any context; + Text::MarkedContext context; QImage preview; Fn spoilerRepaint; style::margins margins; diff --git a/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp b/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp index adce1fcd7..1f2fd2417 100644 --- a/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/click_handler_types.h" -#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "core/ui_integration.h" // TextContext #include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "history/history_item_helpers.h" @@ -202,10 +202,10 @@ void FillSponsoredMessageBar( contentTextSt, textWithEntities, kMarkupTextOptions, - Core::MarkedTextContext{ + Core::TextContext({ .session = session, - .customEmojiRepaint = [=] { widget->update(); }, - }); + .repaint = [=] { widget->update(); }, + })); const auto hostedClick = [=](ClickHandlerPtr handler) { return [=] { if (const auto controller = FindSessionController(widget)) { diff --git a/Telegram/SourceFiles/ui/controls/filter_link_header.cpp b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp index 16c6cf398..e6a02ef66 100644 --- a/Telegram/SourceFiles/ui/controls/filter_link_header.cpp +++ b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp @@ -77,7 +77,7 @@ private: QPainterPath _titlePath; TextWithEntities _folderTitle; - Fn)> _makeContext; + Text::MarkedContext _aboutContext; not_null _folderIcon; bool _horizontalFilters = false; @@ -90,7 +90,7 @@ private: [[nodiscard]] PreviewState GeneratePreview( not_null parent, const TextWithEntities &title, - Fn)> makeContext, + Text::MarkedContext aboutContext, int badge) { using Tabs = Ui::ChatsFiltersTabs; auto preview = PreviewState(); @@ -126,14 +126,14 @@ private: return state->cache; }; const auto raw = &state->tabs; - const auto repaint = [=] { - state->dirty = true; - }; + const auto repaint = [=] { state->dirty = true; }; + auto context = aboutContext; + context.repaint = repaint; raw->setSections({ TextWithEntities{ tr::lng_filters_name_people(tr::now) }, title, TextWithEntities{ tr::lng_filters_name_unread(tr::now) }, - }, makeContext(repaint)); + }, context); raw->fitWidthToSections(); raw->setActiveSectionFast(1); raw->stopAnimation(); @@ -153,7 +153,7 @@ private: [[nodiscard]] PreviewState GeneratePreview( const TextWithEntities &title, - Fn)> makeContext, + Text::MarkedContext context, not_null icon, int badge) { auto preview = PreviewState(); @@ -165,7 +165,7 @@ private: bool dirty = true; }; const auto state = preview.lifetime.make_state(); - const auto repaint = [=] { + context.repaint = [=] { state->dirty = true; }; @@ -201,7 +201,7 @@ private: text, kMarkupTextOptions, available, - makeContext(repaint)); + context); }; const auto paintName = [=](QPainter &p, int top) { state->string.draw(p, { @@ -299,7 +299,7 @@ Widget::Widget( rpl::single(descriptor.about.value()), st::filterLinkAbout, st::defaultPopupMenu, - descriptor.makeAboutContext)) + descriptor.aboutContext)) , _close(CreateChild(this, st::boxTitleClose)) , _aboutPadding(st::boxRowPadding) , _badge(std::move(descriptor.badge)) @@ -307,7 +307,7 @@ Widget::Widget( , _titleFont(st::boxTitle.style.font) , _titlePadding(st::filterLinkTitlePadding) , _folderTitle(descriptor.folderTitle) -, _makeContext(descriptor.makeAboutContext) +, _aboutContext(descriptor.aboutContext) , _folderIcon(descriptor.folderIcon) , _horizontalFilters(descriptor.horizontalFilters) { setMinimumHeight(st::boxTitleHeight); @@ -417,20 +417,24 @@ void Widget::paintEvent(QPaintEvent *e) { auto hq = PainterHighQualityEnabler(p); if (!_preview.frame) { const auto badge = _badge.current(); - const auto makeContext = [=](Fn repaint) { - return _makeContext([=] { repaint(); update(); }); + auto context = _aboutContext; + context.repaint = [this, copy = context.repaint] { + if (const auto &repaint = copy) { + repaint(); + } + update(); }; if (_horizontalFilters) { _preview = GeneratePreview( this, _folderTitle, - makeContext, + context, badge); Widget::resizeEvent(nullptr); } else { _preview = GeneratePreview( _folderTitle, - makeContext, + context, _folderIcon, badge); } @@ -486,7 +490,7 @@ object_ptr FilterLinkProcessButton( not_null parent, FilterLinkHeaderType type, TextWithEntities title, - Fn)> makeContext, + Text::MarkedContext context, rpl::producer badge) { const auto st = &st::filterInviteBox.button; const auto badgeSt = &st::filterInviteButtonBadgeStyle; @@ -600,12 +604,13 @@ object_ptr FilterLinkProcessButton( } }, label->lifetime()); + context.repaint = [=] { label->update(); }; std::move(data) | rpl::start_with_next([=](Data data) { label->text.setMarkedText( st::filterInviteButtonStyle, data.text, kMarkupTextOptions, - makeContext([=] { label->update(); })); + context); label->badge.setText(st::filterInviteButtonBadgeStyle, data.badge); label->update(); }, label->lifetime()); diff --git a/Telegram/SourceFiles/ui/controls/filter_link_header.h b/Telegram/SourceFiles/ui/controls/filter_link_header.h index 55d092387..a530de23a 100644 --- a/Telegram/SourceFiles/ui/controls/filter_link_header.h +++ b/Telegram/SourceFiles/ui/controls/filter_link_header.h @@ -26,7 +26,7 @@ struct FilterLinkHeaderDescriptor { base::required type; base::required title; base::required about; - Fn)> makeAboutContext; + Text::MarkedContext aboutContext; base::required folderTitle; not_null folderIcon; rpl::producer badge; @@ -47,7 +47,7 @@ struct FilterLinkHeader { not_null parent, FilterLinkHeaderType type, TextWithEntities title, - Fn)> makeContext, + Text::MarkedContext context, rpl::producer badge); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index 812a4844c..3c904cd6b 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -341,10 +341,10 @@ bool RoundVideoRecorder::Private::initAudio() { &_swrContext); #else // DA_FFMPEG_NEW_CHANNEL_LAYOUT _swrContext = MakeSwresamplePointer( - &_audioCodec->channel_layout, + _audioCodec->channel_layout, AV_SAMPLE_FMT_S16, _audioCodec->sample_rate, - &_audioCodec->channel_layout, + _audioCodec->channel_layout, _audioCodec->sample_fmt, _audioCodec->sample_rate, &_swrContext); diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp index e0388c032..67c74b81f 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.cpp +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/controls/send_button.h" +#include "lang/lang_tag.h" #include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" #include "ui/painter.h" #include "ui/ui_utility.h" +#include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" namespace Ui { namespace { @@ -22,47 +26,58 @@ constexpr int kWideScale = 5; SendButton::SendButton(QWidget *parent, const style::SendButton &st) : RippleButton(parent, st.inner.ripple) , _st(st) { - resize(_st.inner.width, _st.inner.height); + updateSize(); } -void SendButton::setType(Type type) { - Expects(isSlowmode() || type != Type::Slowmode); - - if (isSlowmode() && type != Type::Slowmode) { - _afterSlowmodeType = type; +void SendButton::setState(State state) { + if (_state == state) { return; } - if (_type != type) { + const auto hasSlowmode = (_state.slowmodeDelay > 0); + const auto hasSlowmodeChanged = hasSlowmode != (state.slowmodeDelay > 0); + auto withSameSlowmode = state; + withSameSlowmode.slowmodeDelay = _state.slowmodeDelay; + const auto animate = hasSlowmodeChanged + || (!hasSlowmode && withSameSlowmode != _state); + if (animate) { _contentFrom = grabContent(); - _type = type; - _a_typeChanged.stop(); + } + if (_state.slowmodeDelay != state.slowmodeDelay) { + const auto seconds = state.slowmodeDelay; + const auto minutes = seconds / 60; + _slowmodeDelayText = seconds + ? u"%1:%2"_q.arg(minutes).arg(seconds % 60, 2, 10, QChar('0')) + : QString(); + } + if (!state.starsToSend || state.type != Type::Send) { + _starsToSendText = Text::String(); + } else if (_starsToSendText.isEmpty() + || _state.starsToSend != state.starsToSend) { + _starsToSendText.setMarkedText( + _st.stars.style, + Text::IconEmoji(&st::starIconEmoji).append( + Lang::FormatCountToShort(state.starsToSend).string), + kMarkupTextOptions); + } + _state = state; + if (animate) { + _stateChangeFromWidth = width(); + _stateChangeAnimation.stop(); + updateSize(); _contentTo = grabContent(); - _a_typeChanged.start( - [=] { update(); }, + _stateChangeAnimation.start( + [=] { updateSize(); update(); }, 0., 1., st::universalDuration); - setPointerCursor(_type != Type::Slowmode); + setPointerCursor(_state.type != Type::Slowmode); + updateSize(); update(); } } -void SendButton::setSlowmodeDelay(int seconds) { - Expects(seconds >= 0 && seconds < kSlowmodeDelayLimit); - - if (_slowmodeDelay == seconds) { - return; - } - _slowmodeDelay = seconds; - _slowmodeDelayText = isSlowmode() - ? u"%1:%2"_q.arg(seconds / 60).arg(seconds % 60, 2, 10, QChar('0')) - : QString(); - setType(isSlowmode() ? Type::Slowmode : _afterSlowmodeType); - update(); -} - void SendButton::finishAnimating() { - _a_typeChanged.stop(); + _stateChangeAnimation.stop(); update(); } @@ -70,26 +85,60 @@ void SendButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); auto over = (isDown() || isOver()); - auto changed = _a_typeChanged.value(1.); + auto changed = _stateChangeAnimation.value(1.); if (changed < 1.) { PainterHighQualityEnabler hq(p); + const auto ratio = style::DevicePixelRatio(); + p.setOpacity(1. - changed); - auto targetRect = QRect((1 - kWideScale) / 2 * width(), (1 - kWideScale) / 2 * height(), kWideScale * width(), kWideScale * height()); - auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * width(), changed); - auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * height(), changed); - p.drawPixmap(targetRect.marginsAdded(QMargins(hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight)), _contentFrom); + const auto fromSize = _contentFrom.size() / (kWideScale * ratio); + const auto fromShift = QPoint( + (width() - fromSize.width()) / 2, + (height() - fromSize.height()) / 2); + auto fromRect = QRect( + (1 - kWideScale) / 2 * fromSize.width(), + (1 - kWideScale) / 2 * fromSize.height(), + kWideScale * fromSize.width(), + kWideScale * fromSize.height() + ).translated(fromShift); + auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * fromSize.width(), changed); + auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * fromSize.height(), changed); + p.drawPixmap( + fromRect.marginsAdded( + { hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight }), + _contentFrom); + p.setOpacity(changed); + const auto toSize = _contentTo.size() / (kWideScale * ratio); + const auto toShift = QPoint( + (width() - toSize.width()) / 2, + (height() - toSize.height()) / 2); + auto toRect = QRect( + (1 - kWideScale) / 2 * toSize.width(), + (1 - kWideScale) / 2 * toSize.height(), + kWideScale * toSize.width(), + kWideScale * toSize.height() + ).translated(toShift); auto shownWidth = anim::interpolate((1 - kWideScale) / 2 * width(), 0, changed); - auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * height(), 0, changed); - p.drawPixmap(targetRect.marginsAdded(QMargins(shownWidth, shownHeight, shownWidth, shownHeight)), _contentTo); + auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * toSize.height(), 0, changed); + p.drawPixmap( + toRect.marginsAdded( + { shownWidth, shownHeight, shownWidth, shownHeight }), + _contentTo); return; } - switch (_type) { + switch (_state.type) { case Type::Record: paintRecord(p, over); break; case Type::Round: paintRound(p, over); break; case Type::Save: paintSave(p, over); break; case Type::Cancel: paintCancel(p, over); break; - case Type::Send: paintSend(p, over); break; + case Type::Send: + if (_starsToSendText.isEmpty()) { + paintSend(p, over); + } else { + paintStarsToSend(p, over); + } + break; case Type::Schedule: paintSchedule(p, over); break; case Type::Slowmode: paintSlowmode(p); break; } @@ -152,6 +201,23 @@ void SendButton::paintSend(QPainter &p, bool over) { } } +void SendButton::paintStarsToSend(QPainter &p, bool over) { + const auto geometry = starsGeometry(); + { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(over ? _st.stars.textBgOver : _st.stars.textBg); + const auto radius = geometry.rounded.height() / 2; + p.drawRoundedRect(geometry.rounded, radius, radius); + } + p.setPen(over ? _st.stars.textFgOver : _st.stars.textFg); + _starsToSendText.draw(p, { + .position = geometry.inner.topLeft(), + .outerWidth = width(), + .availableWidth = geometry.inner.width(), + }); +} + void SendButton::paintSchedule(QPainter &p, bool over) { { PainterHighQualityEnabler hq(p); @@ -178,8 +244,40 @@ void SendButton::paintSlowmode(QPainter &p) { style::al_center); } -bool SendButton::isSlowmode() const { - return (_slowmodeDelay > 0); +SendButton::StarsGeometry SendButton::starsGeometry() const { + const auto &st = _st.stars; + const auto inner = QRect( + 0, + 0, + _starsToSendText.maxWidth(), + st.style.font->height); + const auto rounded = inner.marginsAdded(QMargins( + st.padding.left() - st.width / 2, + st.padding.top() + st.textTop, + st.padding.right() - st.width / 2, + st.height - st.padding.top() - st.textTop - st.style.font->height)); + const auto add = (_st.inner.height - rounded.height()) / 2; + const auto outer = rounded.marginsAdded(QMargins( + add, + add, + add, + _st.inner.height - add - rounded.height())); + const auto shift = -outer.topLeft(); + return { + .inner = inner.translated(shift), + .rounded = rounded.translated(shift), + .outer = outer.translated(shift), + }; +} + +void SendButton::updateSize() { + const auto finalWidth = _starsToSendText.isEmpty() + ? _st.inner.width + : starsGeometry().outer.width(); + const auto progress = _stateChangeAnimation.value(1.); + resize( + anim::interpolate(_stateChangeFromWidth, finalWidth, progress), + _st.inner.height); } QPixmap SendButton::grabContent() { @@ -195,7 +293,7 @@ QPixmap SendButton::grabContent() { (kWideScale - 1) / 2 * height(), GrabWidget(this)); } - return Ui::PixmapFromImage(std::move(result)); + return PixmapFromImage(std::move(result)); } QImage SendButton::prepareRippleMask() const { diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h index 835974f51..b1483bbc4 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.h +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -30,11 +30,21 @@ public: Cancel, Slowmode, }; + struct State { + Type type = Type::Send; + int slowmodeDelay = 0; + int starsToSend = 0; + + friend inline constexpr auto operator<=>(State, State) = default; + friend inline constexpr bool operator==(State, State) = default; + }; [[nodiscard]] Type type() const { - return _type; + return _state.type; } - void setType(Type state); - void setSlowmodeDelay(int seconds); + [[nodiscard]] State state() const { + return _state; + } + void setState(State state); void finishAnimating(); protected: @@ -44,8 +54,15 @@ protected: QPoint prepareRippleStartPosition() const override; private: + struct StarsGeometry { + QRect inner; + QRect rounded; + QRect outer; + }; [[nodiscard]] QPixmap grabContent(); - [[nodiscard]] bool isSlowmode() const; + void updateSize(); + + [[nodiscard]] StarsGeometry starsGeometry() const; void paintRecord(QPainter &p, bool over); void paintRound(QPainter &p, bool over); @@ -54,17 +71,18 @@ private: void paintSend(QPainter &p, bool over); void paintSchedule(QPainter &p, bool over); void paintSlowmode(QPainter &p); + void paintStarsToSend(QPainter &p, bool over); const style::SendButton &_st; - Type _type = Type::Send; - Type _afterSlowmodeType = Type::Send; + State _state; QPixmap _contentFrom, _contentTo; - Ui::Animations::Simple _a_typeChanged; + Ui::Animations::Simple _stateChangeAnimation; + int _stateChangeFromWidth = 0; - int _slowmodeDelay = 0; QString _slowmodeDelayText; + Ui::Text::String _starsToSendText; }; diff --git a/Telegram/SourceFiles/ui/controls/tabbed_search.cpp b/Telegram/SourceFiles/ui/controls/tabbed_search.cpp index 59a1f6cf0..ea9f1e70f 100644 --- a/Telegram/SourceFiles/ui/controls/tabbed_search.cpp +++ b/Telegram/SourceFiles/ui/controls/tabbed_search.cpp @@ -149,7 +149,7 @@ void GroupsStrip::set(std::vector list) { .icon = std::make_unique( _factory( group.iconId, - updater(group.iconId)), + { .repaint = updater(group.iconId) }), loopCount, stopAtLastFrame), }); diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index c1d2d2187..dec06a911 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -247,6 +247,7 @@ bool UserpicButton::canSuggestPhoto(not_null user) const { // Server allows suggesting photos only in non-empty chats. return !user->isSelf() && !user->isBot() + && !user->starsPerMessageChecked() && (user->owner().history(user)->lastServerMessage() != nullptr); } diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index 9834489b1..3fe236355 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -359,7 +359,7 @@ void Action::paint(Painter &p) { if (!_custom && !_content.singleCustomEntityData.isEmpty()) { _custom = _customEmojiFactory( _content.singleCustomEntityData, - [=] { update(); }); + { .repaint = [=] { update(); } }); } if (_custom) { const auto ratio = style::DevicePixelRatio(); @@ -772,7 +772,9 @@ void WhoReactedEntryAction::setData(Data &&data) { } _type = data.type; _custom = _customEmojiFactory - ? _customEmojiFactory(data.customEntityData, [=] { update(); }) + ? _customEmojiFactory( + data.customEntityData, + { .repaint = [=] { update(); } }) : nullptr; const auto ratio = style::DevicePixelRatio(); const auto size = Emoji::GetSizeNormal() / ratio; diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 36a5f24f6..e9b20d5bb 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -62,8 +62,13 @@ creditsBoxButtonLabel: FlatLabel(defaultFlatLabel) { style: semiboldTextStyle; } -starIconSmall: icon{{ "payments/small_star", windowFg }}; -starIconSmallPadding: margins(0px, -3px, 0px, 0px); +starIconEmoji: IconEmoji { + icon: icon{{ "payments/premium_emoji", creditsBg1 }}; + padding: margins(4px, 1px, 4px, 0px); +} +starIconEmojiColored: IconEmoji(starIconEmoji) { + useIconColor: true; +} creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }}; @@ -119,10 +124,17 @@ giftBoxGiftHeight: 164px; giftBoxGiftSmall: 108px; giftBoxGiftRadius: 12px; giftBoxGiftBadgeFont: font(10px semibold); +giftBoxByStarsStyle: TextStyle(defaultTextStyle) { + font: font(10px); +} +giftBoxByStarsSkip: 2px; +giftBoxByStarsStarTop: 3px; giftBoxPremiumIconSize: 64px; giftBoxPremiumIconTop: 10px; giftBoxPremiumTextTop: 84px; +giftBoxPremiumTextTopByStars: 78px; giftBoxButtonBottom: 12px; +giftBoxButtonBottomByStars: 18px; giftBoxButtonPadding: margins(8px, 4px, 8px, 4px); giftBoxPreviewStickerPadding: margins(10px, 12px, 10px, 16px); giftBoxPreviewTitlePadding: margins(12px, 4px, 12px, 4px); @@ -131,6 +143,7 @@ giftBoxButtonMargin: margins(12px, 8px, 12px, 12px); giftBoxStickerTop: 0px; giftBoxStickerStarTop: 24px; giftBoxSmallStickerTop: 16px; +giftBoxStickerTopByStars: -4px; giftBoxStickerSize: size(80px, 80px); giftBoxUserpicSize: 24px; giftBoxUserpicSkip: 2px; @@ -165,6 +178,7 @@ giftListAboutMargin: margins(12px, 24px, 12px, 24px); giftBoxEmojiToggleTop: 7px; giftBoxLimitTop: 28px; giftBoxLockMargins: margins(-2px, 1px, 0px, 0px); +giftBoxPinIcon: icon {{ "dialogs/dialogs_pinned", premiumButtonFg }}; creditsHistoryEntriesList: PeerList(defaultPeerList) { padding: margins( diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp index d472d6867..0fbf095cc 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp @@ -554,7 +554,15 @@ TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) { Info::BotStarRef::FormatCommission(entry.starrefCommission) }, TextWithEntities::Simple) - : (entry.floodSkip + : entry.paidMessagesCount + ? tr::lng_credits_paid_messages_fee( + tr::now, + lt_count, + entry.paidMessagesCount, + TextWithEntities::Simple) + : (entry.premiumMonthsForStars + ? tr::lng_premium_summary_title + : entry.floodSkip ? tr::lng_credits_box_history_entry_api : entry.reaction ? tr::lng_credits_box_history_entry_reaction_name diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index fb35336b3..ed73836f3 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/abstract_button.h" #include "ui/effects/animations.h" +#include "ui/effects/credits_graphics.h" #include "ui/effects/gradient.h" #include "ui/effects/numbers_animation.h" #include "ui/effects/premium_bubble.h" @@ -917,6 +918,45 @@ void AddGiftOptions( }, *onceLifetime); } + constexpr auto kStar = QChar(0x2B50); + const auto removedStar = [&](QString s) { + return s.replace(kStar, QChar()); + }; + const auto &costPerMonthFont = st::shareBoxListItem.nameStyle.font; + const auto &costTotalFont = st::normalFont; + const auto costPerMonthIcon = info.costPerMonth.startsWith(kStar) + ? GenerateStars(costPerMonthFont->height, 1) + : QImage(); + const auto costPerMonthText = costPerMonthIcon.isNull() + ? info.costPerMonth + : removedStar(info.costPerMonth); + const auto costTotalEntry = [&] { + if (!info.costTotal.startsWith(kStar)) { + return QImage(); + } + const auto text = removedStar(info.costTotal); + const auto icon = GenerateStars(costTotalFont->height, 1); + auto result = QImage( + QSize(costTotalFont->spacew + costTotalFont->width(text), 0) + * style::DevicePixelRatio() + + icon.size(), + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(style::DevicePixelRatio()); + result.fill(Qt::transparent); + { + auto p = QPainter(&result); + p.drawImage(0, 0, icon); + p.setPen(st::windowSubTextFg); + p.setFont(costTotalFont); + auto copy = info.costTotal; + p.drawText( + Rect(result.size() / style::DevicePixelRatio()), + text, + style::al_right); + } + return result; + }(); + row->paintRequest( ) | rpl::start_with_next([=](const QRect &r) { auto p = QPainter(row); @@ -979,7 +1019,11 @@ void AddGiftOptions( if (st.borderWidth && (animation->nowIndex == index)) { const auto progress = animation->animation.value(1.); const auto w = row->width(); - auto gradient = QLinearGradient(w - w * progress, 0, w * 2, 0); + auto gradient = QLinearGradient( + w - w * progress, + 0, + w * 2, + 0); gradient.setSpread(QGradient::Spread::RepeatSpread); gradient.setStops(stops); const auto pen = QPen( @@ -1004,13 +1048,28 @@ void AddGiftOptions( : bottomLeftRect.width() + discountMargins.left(), 0); p.setPen(st::windowSubTextFg); - p.setFont(st::shareBoxListItem.nameStyle.font); - p.drawText(perRect, info.costPerMonth, style::al_left); + p.setFont(costPerMonthFont); + const auto perMonthLeft = costPerMonthFont->spacew + + costPerMonthIcon.width() / style::DevicePixelRatio(); + p.drawText( + perRect.translated(perMonthLeft, 0), + costPerMonthText, + style::al_left); + p.drawImage(perRect.topLeft(), costPerMonthIcon); const auto totalRect = row->rect() - QMargins(0, 0, st.rowMargins.right(), 0); - p.setFont(st::normalFont); - p.drawText(totalRect, info.costTotal, style::al_right); + if (costTotalEntry.isNull()) { + p.setFont(costTotalFont); + p.drawText(totalRect, info.costTotal, style::al_right); + } else { + const auto size = costTotalEntry.size() + / style::DevicePixelRatio(); + p.drawImage( + totalRect.width() - size.width(), + (row->height() - size.height()) / 2, + costTotalEntry); + } }, row->lifetime()); row->setClickedCallback([=, duration = st::defaultCheck.duration] { diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 504065677..f010afa62 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -23,6 +23,7 @@ menuIconStickers: icon {{ "menu/stickers", menuIconColor }}; menuIconEmoji: icon {{ "menu/emoji", menuIconColor }}; menuIconCancel: icon {{ "menu/cancel", menuIconColor }}; menuIconShowInChat: icon {{ "menu/show_in_chat", menuIconColor }}; +menuIconStealth: icon {{ "menu/stealth", menuIconColor }}; menuIconGif: icon {{ "menu/gif", menuIconColor }}; menuIconShowInFolder: icon {{ "menu/show_in_folder", menuIconColor }}; menuIconDownload: icon {{ "menu/download", menuIconColor }}; diff --git a/Telegram/SourceFiles/ui/unread_badge.cpp b/Telegram/SourceFiles/ui/unread_badge.cpp index 502493769..b2f84d658 100644 --- a/Telegram/SourceFiles/ui/unread_badge.cpp +++ b/Telegram/SourceFiles/ui/unread_badge.cpp @@ -355,9 +355,10 @@ void PeerBadge::set( _botVerifiedData = std::make_unique(); } if (details->iconId) { - _botVerifiedData->icon = factory( - Data::SerializeCustomEmojiId(details->iconId), - repaint); + _botVerifiedData->icon = std::make_unique( + factory( + Data::SerializeCustomEmojiId(details->iconId), + { .repaint = repaint })); } } diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp index 8149ffca5..5fddc9e79 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp @@ -54,7 +54,7 @@ ChatsFiltersTabs::ChatsFiltersTabs( bool ChatsFiltersTabs::setSectionsAndCheckChanged( std::vector &§ions, - const std::any &context, + const Text::MarkedContext &context, Fn paused) { const auto &was = sectionsRef(); const auto changed = [&] { diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h index 64c66c062..1375b70dc 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h @@ -29,7 +29,7 @@ public: bool setSectionsAndCheckChanged( std::vector &§ions, - const std::any &context, + const Text::MarkedContext &context, Fn paused); void fitWidthToSections() override; diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp index 64cd5eb39..176b5644c 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp @@ -175,11 +175,11 @@ void ShowFiltersListMenu( icon); action->setEnabled(i < premiumFrom); if (!title.text.empty()) { - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = session, - .customEmojiRepaint = [raw = item.get()] { raw->update(); }, + .repaint = [raw = item.get()] { raw->update(); }, .customEmojiLoopLimit = title.isStatic ? -1 : 0, - }; + }); item->setMarkedText(title.text, QString(), context); } state->menu->addAction(std::move(item)); @@ -344,10 +344,7 @@ not_null AddChatFiltersTabsStrip( if ((list.size() <= 1 && !slider->width()) || state->ignoreRefresh) { return; } - const auto context = Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [=] { slider->update(); }, - }; + const auto context = Core::TextContext({ .session = session }); const auto paused = [=] { return On(PowerSaving::kEmojiChat) || controller->isGifPausedAtLeastFor(pauseLevel); diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp index e11ea96d0..77874b58d 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp @@ -79,7 +79,8 @@ void DiscreteSlider::addSection(const QString &label) { void DiscreteSlider::addSection( const TextWithEntities &label, - const std::any &context) { + Text::MarkedContext context) { + context.repaint = [this] { update(); }; _sections.push_back(Section(label, getLabelStyle(), context)); resizeToWidth(width()); } @@ -96,9 +97,11 @@ void DiscreteSlider::setSections(const std::vector &labels) { void DiscreteSlider::setSections( const std::vector &labels, - const std::any &context) { + Text::MarkedContext context) { Assert(!labels.empty()); + context.repaint = [this] { update(); }; + _sections.clear(); for (const auto &label : labels) { _sections.push_back(Section(label, getLabelStyle(), context)); @@ -225,7 +228,7 @@ DiscreteSlider::Section::Section( DiscreteSlider::Section::Section( const TextWithEntities &label, const style::TextStyle &st, - const std::any &context) { + const Text::MarkedContext &context) { this->label.setMarkedText(st, label, kMarkupTextOptions, context); contentWidth = Section::label.maxWidth(); } diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h index 2ff6f771c..4e9476bb0 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "ui/round_rect.h" #include "ui/effects/animations.h" +#include "ui/text/text.h" namespace style { struct TextStyle; @@ -32,11 +33,11 @@ public: void addSection(const QString &label); void addSection( const TextWithEntities &label, - const std::any &context = {}); + Text::MarkedContext context = {}); void setSections(const std::vector &labels); void setSections( const std::vector &labels, - const std::any &context = {}); + Text::MarkedContext context = {}); int activeSection() const { return _activeIndex; } @@ -63,9 +64,9 @@ protected: Section( const TextWithEntities &label, const style::TextStyle &st, - const std::any &context); + const Text::MarkedContext &context); - Ui::Text::String label; + Text::String label; std::unique_ptr ripple; int left = 0; int width = 0; diff --git a/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.cpp b/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.cpp index 4502d6553..69f1d5f14 100644 --- a/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.cpp +++ b/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.cpp @@ -16,13 +16,12 @@ namespace Ui { object_ptr CreateLabelWithCustomEmoji( QWidget *parent, rpl::producer &&text, - Core::MarkedTextContext context, + Text::MarkedContext context, const style::FlatLabel &st) { auto label = object_ptr(parent, st); const auto raw = label.data(); - if (!context.customEmojiRepaint) { - context.customEmojiRepaint = [=] { raw->update(); }; - } + + context.repaint = [=] { raw->update(); }; std::move(text) | rpl::start_with_next([=](const TextWithEntities &text) { raw->setMarkedText(text, context); }, label->lifetime()); diff --git a/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.h b/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.h index f22e4e801..662ce0fa7 100644 --- a/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.h +++ b/Telegram/SourceFiles/ui/widgets/label_with_custom_emoji.h @@ -7,8 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "core/ui_integration.h" // Core::MarkedTextContext. - template class object_ptr; @@ -20,16 +18,18 @@ namespace style { struct FlatLabel; } // namespace style -namespace Ui { -class FlatLabel; -} // namespace Ui +namespace Ui::Text { +struct MarkedContext; +} // namespace Ui::Text namespace Ui { +class FlatLabel; + [[nodiscard]] object_ptr CreateLabelWithCustomEmoji( QWidget *parent, rpl::producer &&text, - Core::MarkedTextContext context, + Text::MarkedContext context, const style::FlatLabel &st); } // namespace Ui diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index c9724a8e3..77edd9a25 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -258,8 +258,6 @@ QIcon CreateIcon(Main::Session *session, bool returnNullIfDefault) { } QImage GenerateCounterLayer(CounterLayerArgs &&args) { - // platform/linux/main_window_linux depends on count used the same - // way for all the same (count % 1000) values. const auto count = args.count.value(); const auto text = (count < 1000) ? QString::number(count) @@ -336,6 +334,8 @@ QImage GenerateCounterLayer(CounterLayerArgs &&args) { } QImage WithSmallCounter(QImage image, CounterLayerArgs &&args) { + // platform/linux/tray_linux depends on count used the same + // way for all the same (count % 100) values. const auto count = args.count.value(); const auto text = (count < 100) ? QString::number(count) diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 0db556b43..d49b5259c 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -216,11 +216,6 @@ void System::setManager(Fn()> create) { } } -Manager &System::manager() const { - Expects(_manager != nullptr); - return *_manager; -} - Main::Session *System::findSession(uint64 sessionId) const { for (const auto &[index, account] : Core::App().domain().accounts()) { if (const auto session = account->maybeSession()) { @@ -393,6 +388,12 @@ void System::schedule(Data::ItemNotification notification) { registerThread(thread); _whenAlerts[thread].emplace(timing.when, notifyBy); } + if (const auto user = item->history()->peer->asUser()) { + if (user->hasStarsPerMessage() + && !user->messageMoneyRestrictionsKnown()) { + user->updateFull(); + } + } if (Core::App().settings().desktopNotify() && !_manager->skipToast()) { registerThread(thread); @@ -959,7 +960,8 @@ Manager::DisplayOptions Manager::getNotificationOptions( || (!Data::CanSendTexts(peer) && (!topic || !Data::CanSendTexts(topic))) || peer->isBroadcast() - || (peer->slowmodeSecondsLeft() > 0); + || (peer->slowmodeSecondsLeft() > 0) + || (peer->starsPerMessageChecked() > 0); result.spoilerLoginCode = item && !item->out() && peer->isNotificationsUser() diff --git a/Telegram/SourceFiles/window/notifications_manager.h b/Telegram/SourceFiles/window/notifications_manager.h index fa88f6c0a..833a79d00 100644 --- a/Telegram/SourceFiles/window/notifications_manager.h +++ b/Telegram/SourceFiles/window/notifications_manager.h @@ -100,7 +100,6 @@ public: void createManager(); void setManager(Fn()> create); - [[nodiscard]] Manager &manager() const; void checkDelayed(); void schedule(Data::ItemNotification notification); @@ -237,22 +236,6 @@ public: friend inline auto operator<=>( const ContextId&, const ContextId&) = default; - - [[nodiscard]] auto toAnyVector() const { - return std::vector{ - std::make_any(sessionId), - std::make_any(peerId.value), - std::make_any(topicRootId.bare), - }; - } - - [[nodiscard]] static auto FromAnyVector(const auto &vector) { - return ContextId{ - std::any_cast(vector[0]), - PeerIdHelper(std::any_cast(vector[1])), - std::any_cast(vector[2]), - }; - } }; struct NotificationId { ContextId contextId; @@ -261,21 +244,6 @@ public: friend inline auto operator<=>( const NotificationId&, const NotificationId&) = default; - - [[nodiscard]] auto toAnyVector() const { - return std::vector{ - std::make_any>(contextId.toAnyVector()), - std::make_any(msgId.bare), - }; - } - - [[nodiscard]] static auto FromAnyVector(const auto &vector) { - return NotificationId{ - ContextId::FromAnyVector( - std::any_cast>(vector[0])), - std::any_cast(vector[1]), - }; - } }; struct NotificationFields { not_null item; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index e5eb6caeb..da4728f86 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -203,16 +203,10 @@ void Manager::checkLastInput() { void Manager::startAllHiding() { if (!hasReplyingNotification()) { - int notHidingCount = 0; for (const auto ¬ification : _notifications) { - if (notification->isShowing()) { - ++notHidingCount; - } else { - notification->startHiding(); - } + notification->startHiding(); } - notHidingCount += _queuedNotifications.size(); - if (_hideAll && notHidingCount < 2) { + if (_hideAll && _queuedNotifications.size() < 2) { _hideAll->startHiding(); } } @@ -575,6 +569,10 @@ void Widget::hideStop() { void Widget::hideAnimated(float64 duration, const anim::transition &func) { _hiding = true; + // Stop the previous animation so as to make sure that the notification + // is fully restored before hiding it again. + // Relates to https://github.com/telegramdesktop/tdesktop/issues/28811. + _a_opacity.stop(); _a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., duration, func); } @@ -975,10 +973,10 @@ void Notification::updateNotifyDisplay() { 0, Qt::LayoutDirectionAuto, }; - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_history->session(), - .customEmojiRepaint = [=] { customEmojiCallback(); }, - }; + .repaint = [=] { customEmojiCallback(); }, + }); _textCache.setMarkedText( st::dialogsTextStyle, text, @@ -1014,10 +1012,10 @@ void Notification::updateNotifyDisplay() { const auto fullTitle = manager()->addTargetAccountName( std::move(title), &_history->session()); - const auto context = Core::MarkedTextContext{ + const auto context = Core::TextContext({ .session = &_history->session(), - .customEmojiRepaint = [=] { customEmojiCallback(); }, - }; + .repaint = [=] { customEmojiCallback(); }, + }); _titleCache.setMarkedText( st::semiboldTextStyle, fullTitle, diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index 5bd4344d8..7c8128235 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -143,7 +143,7 @@ public: int shift, Direction shiftDirection); - bool isShowing() const { + bool isFadingIn() const { return _a_opacity.animating() && !_hiding; } diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index deeec7ecc..246b911b6 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -275,13 +275,6 @@ base::unique_qptr FiltersMenu::prepareButton( Ui::FilterIcon icon, bool toBeginning) { const auto isStatic = title.isStatic; - const auto makeContext = [=](Fn update) { - return Core::MarkedTextContext{ - .session = &_session->session(), - .customEmojiRepaint = std::move(update), - .customEmojiLoopLimit = isStatic ? -1 : 0, - }; - }; const auto paused = [=] { return On(PowerSaving::kEmojiChat) || _session->isGifPausedAtLeastFor(Window::GifPauseReason::Any); @@ -290,7 +283,10 @@ base::unique_qptr FiltersMenu::prepareButton( container, id ? title.text : TextWithEntities{ tr::lng_filters_all(tr::now) }, st::windowFiltersButton, - makeContext, + Core::TextContext({ + .session = &_session->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }), paused); auto added = toBeginning ? container->insert(0, std::move(prepared)) diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index b77dbc113..08bc6ec40 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -149,6 +149,7 @@ void ShareBotGame( MTP_int(0), // schedule_date MTPInputPeer(), // send_as MTPInputQuickReplyShortcut(), + MTPlong(), MTPlong() ), [=](const MTPUpdates &, const MTP::Response &) { }, [=](const MTP::Error &error, const MTP::Response &) { @@ -637,6 +638,7 @@ void Filler::addToggleFolder() { .fillSubmenu = [&](not_null menu) { FillChooseFilterMenu(controller, menu, history); }, + .submenuSt = &st::foldersMenu, }); } @@ -1179,7 +1181,8 @@ void Filler::addCreatePoll() { : Api::SendType::Normal; const auto sendMenuType = (_request.section == Section::Scheduled) ? SendMenu::Type::Disabled - : (_request.section == Section::Replies) + : (_request.section == Section::Replies + || _peer->starsPerMessageChecked()) ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); @@ -1205,7 +1208,7 @@ void Filler::addThemeEdit() { if (!user || user->isInaccessible()) { return; } - if (user->meRequiresPremiumToWrite() && !user->session().premium()) { + if (user->requiresPremiumToWrite() && !user->session().premium()) { return; } const auto controller = _controller; @@ -1689,23 +1692,53 @@ void PeerMenuShareContactBox( auto recipient = peer->isUser() ? title : ('\xAB' + title + '\xBB'); - const auto weak = base::make_weak(thread); + struct State { + base::weak_ptr weak; + Fn share; + SendPaymentHelper sendPayment; + }; + const auto state = std::make_shared(); + state->weak = thread; + state->share = [=](Api::SendOptions options) { + const auto strong = state->weak.get(); + if (!strong) { + state->share = nullptr; + return; + } + const auto withPaymentApproved = [=](int stars) { + if (const auto onstack = state->share) { + auto copy = options; + copy.starsApproved = stars; + onstack(copy); + } + }; + const auto checked = state->sendPayment.check( + navigation, + peer, + 1, + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + navigation->showThread( + strong, + ShowAtTheEndMsgId, + Window::SectionShow::Way::ClearStack); + auto action = Api::SendAction(strong, options); + action.clearDraft = false; + strong->session().api().shareContact(user, action); + state->share = nullptr; + }; + navigation->parentController()->show( Ui::MakeConfirmBox({ .text = tr::lng_forward_share_contact( tr::now, lt_recipient, recipient), - .confirmed = [weak, user, navigation](Fn &&close) { - if (const auto strong = weak.get()) { - navigation->showThread( - strong, - ShowAtTheEndMsgId, - Window::SectionShow::Way::ClearStack); - auto action = Api::SendAction(strong); - action.clearDraft = false; - strong->session().api().shareContact(user, action); - } + .confirmed = [state](Fn &&close) { + state->share({}); close(); }, .confirmText = tr::lng_forward_send(), @@ -1717,7 +1750,7 @@ void PeerMenuShareContactBox( ChooseRecipientArgs{ .session = &navigation->session(), .callback = std::move(callback), - .premiumRequiredError = WritePremiumRequiredError, + .moneyRestrictionError = WriteMoneyRestrictionError, }), [](not_null box) { box->addButton(tr::lng_cancel(), [=] { @@ -1738,17 +1771,42 @@ void PeerMenuCreatePoll( chosen &= ~PollData::Flag::PublicVotes; disabled |= PollData::Flag::PublicVotes; } + auto starsRequired = peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::FullInfo + | Data::PeerUpdate::Flag::StarsPerMessage + ) | rpl::map([=] { + return peer->starsPerMessageChecked(); + }); auto box = Box( controller, chosen, disabled, + std::move(starsRequired), sendType, sendMenuDetails); - const auto weak = Ui::MakeWeak(box.data()); - const auto lock = box->lifetime().make_state(false); - box->submitRequests( - ) | rpl::start_with_next([=](const CreatePollBox::Result &result) { - if (std::exchange(*lock, true)) { + struct State { + Fn create; + SendPaymentHelper sendPayment; + bool lock = false; + }; + const auto weak = QPointer(box); + const auto state = box->lifetime().make_state(); + state->create = [=](const CreatePollBox::Result &result) { + const auto withPaymentApproved = crl::guard(weak, [=](int stars) { + if (const auto onstack = state->create) { + auto copy = result; + copy.options.starsApproved = stars; + onstack(copy); + } + }); + const auto checked = state->sendPayment.check( + controller, + peer, + 1, + result.options.starsApproved, + withPaymentApproved); + if (!checked || std::exchange(state->lock, true)) { return; } auto action = Api::SendAction( @@ -1763,12 +1821,15 @@ void PeerMenuCreatePoll( } const auto api = &peer->session().api(); api->polls().create(result.poll, action, crl::guard(weak, [=] { + state->create = nullptr; weak->closeBox(); }), crl::guard(weak, [=] { - *lock = false; + state->lock = false; weak->submitFailed(tr::lng_attach_failed(tr::now)); })); - }, box->lifetime()); + }; + box->submitRequests( + ) | rpl::start_with_next(state->create, box->lifetime()); controller->show(std::move(box), Ui::LayerOption::CloseOther); } @@ -1916,7 +1977,9 @@ object_ptr PrepareChooseRecipientBox( rpl::producer titleOverride, FnMut &&successCallback, InlineBots::PeerTypes typesRestriction, - Fn>)> sendMany) { + Fn>, + Api::SendOptions)> sendMany) { const auto weak = std::make_shared>(); const auto selectable = (sendMany != nullptr); class Controller final : public ChooseRecipientBoxController { @@ -1932,7 +1995,7 @@ object_ptr PrepareChooseRecipientBox( .session = session, .callback = std::move(callback), .filter = filter, - .premiumRequiredError = WritePremiumRequiredError, + .moneyRestrictionError = WriteMoneyRestrictionError, }) , _selectable(selectable) { } @@ -1950,8 +2013,7 @@ object_ptr PrepareChooseRecipientBox( ChooseRecipientBoxController::rowClicked(row); } else { delegate()->peerListSetRowChecked(row, !row->checked()); - _hasSelectedChanges.fire( - delegate()->peerListSelectedRowsCount() > 0); + _selectionChanges.fire({}); } } @@ -1969,16 +2031,18 @@ object_ptr PrepareChooseRecipientBox( st::popupMenuWithIcons); menu->addAction(tr::lng_bot_choose_chat(tr::now), [=] { delegate()->peerListSetRowChecked(row, true); - _hasSelectedChanges.fire( - delegate()->peerListSelectedRowsCount() > 0); + _selectionChanges.fire({}); }, &st::menuIconSelect); return menu; } return nullptr; } - [[nodiscard]] rpl::producer hasSelectedChanges() const { - return _hasSelectedChanges.events_starting_with(false); + [[nodiscard]] rpl::producer<> selectionChanges() const { + return _selectionChanges.events_starting_with({}); + } + [[nodiscard]] bool hasSelected() const { + return delegate()->peerListSelectedRowsCount() > 0; } [[nodiscard]] rpl::producer singleChosen() const { @@ -1987,7 +2051,7 @@ object_ptr PrepareChooseRecipientBox( private: rpl::event_stream _singleChosen; - rpl::event_stream _hasSelectedChanges; + rpl::event_stream<> _selectionChanges; bool _selectable = false; }; @@ -2029,20 +2093,99 @@ object_ptr PrepareChooseRecipientBox( std::move(filter), selectable); const auto raw = controller.get(); + + struct State { + Fn submit; + rpl::variable starsToSend; + Fn refreshStarsToSend; + rpl::lifetime submitLifetime; + }; + const auto state = std::make_shared(); auto initBox = [=](not_null box) { - raw->hasSelectedChanges( - ) | rpl::start_with_next([=](bool shown) { + state->refreshStarsToSend = [=] { + auto perMessage = 0; + for (const auto &peer : box->collectSelectedRows()) { + perMessage += peer->starsPerMessageChecked(); + } + state->starsToSend = perMessage; + }; + raw->selectionChanges( + ) | rpl::start_with_next([=] { box->clearButtons(); + state->refreshStarsToSend(); + const auto shown = raw->hasSelected(); if (shown) { - box->addButton(tr::lng_send_button(), [=] { + const auto weak = Ui::MakeWeak(box); + state->submit = [=](Api::SendOptions options) { + state->submitLifetime.destroy(); + const auto show = box->peerListUiShow(); const auto peers = box->collectSelectedRows(); + const auto withPaymentApproved = crl::guard(weak, [=]( + int approved) { + auto copy = options; + copy.starsApproved = approved; + if (const auto onstack = state->submit) { + onstack(copy); + } + }); + + const auto alreadyApproved = options.starsApproved; + auto paid = std::vector>(); + auto waiting = base::flat_set>(); + auto totalStars = 0; + for (const auto &peer : peers) { + const auto details = ComputePaymentDetails(peer, 1); + 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); + } + }, state->submitLifetime); + + if (!session->credits().loaded()) { + session->credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + withPaymentApproved(alreadyApproved); + }, state->submitLifetime); + } + return; + } else if (totalStars > alreadyApproved) { + ShowSendPaidConfirm(show, paid, SendPaymentDetails{ + .messages = 1, + .stars = totalStars, + }, [=] { withPaymentApproved(totalStars); }); + return; + } + state->submit = nullptr; + sendMany(ranges::views::all( peers ) | ranges::views::transform([&]( not_null peer) -> Controller::Chosen { return peer->owner().history(peer); - }) | ranges::to_vector); - }); + }) | ranges::to_vector, options); + }; + const auto send = box->addButton( + tr::lng_send_button(), + [=] { + if (const auto onstack = state->submit) { + onstack({}); + } + }); + send->setText(PaidSendButtonText( + state->starsToSend.value(), + tr::lng_send_button())); } box->addButton(tr::lng_cancel(), [=] { box->closeBox(); @@ -2153,7 +2296,7 @@ QPointer ShowForwardMessagesBox( .callback = [=](Chosen thread) { _singleChosen.fire_copy(thread); }, - .premiumRequiredError = WritePremiumRequiredError, + .moneyRestrictionError = WriteMoneyRestrictionError, }) { } @@ -2173,8 +2316,7 @@ QPointer ShowForwardMessagesBox( ChooseRecipientBoxController::rowClicked(row); } else if (count) { delegate()->peerListSetRowChecked(row, !row->checked()); - _hasSelectedChanges.fire( - delegate()->peerListSelectedRowsCount() > 0); + _selectionChanges.fire({}); } } @@ -2187,16 +2329,18 @@ QPointer ShowForwardMessagesBox( st::popupMenuWithIcons); menu->addAction(tr::lng_bot_choose_chat(tr::now), [=] { delegate()->peerListSetRowChecked(row, true); - _hasSelectedChanges.fire( - delegate()->peerListSelectedRowsCount() > 0); + _selectionChanges.fire({}); }, &st::menuIconSelect); return menu; } return nullptr; } - [[nodiscard]] rpl::producer hasSelectedChanges() const { - return _hasSelectedChanges.events_starting_with(false); + [[nodiscard]] rpl::producer<> selectionChanges() const { + return _selectionChanges.events_starting_with({}); + } + [[nodiscard]] bool hasSelected() const { + return delegate()->peerListSelectedRowsCount() > 0; } [[nodiscard]] rpl::producer singleChosen() const{ @@ -2205,7 +2349,7 @@ QPointer ShowForwardMessagesBox( private: rpl::event_stream _singleChosen; - rpl::event_stream _hasSelectedChanges; + rpl::event_stream<> _selectionChanges; }; @@ -2213,6 +2357,10 @@ QPointer ShowForwardMessagesBox( not_null box; not_null controller; base::unique_qptr menu; + Fn submit; + rpl::variable starsToSend; + Fn refreshStarsToSend; + rpl::lifetime submitLifetime; }; const auto applyFilter = [=](not_null box, FilterId id) { @@ -2352,13 +2500,74 @@ QPointer ShowForwardMessagesBox( tr::lng_photos_comment()), st::shareCommentPadding); + const auto history = session->data().message(msgIds.front())->history(); const auto send = ShareBox::DefaultForwardCallback( show, - session->data().message(msgIds.front())->history(), + history, + msgIds); + const auto countMessages = ShareBox::DefaultForwardCountMessages( + history, msgIds); - const auto submit = [=](Api::SendOptions options) { + const auto weak = Ui::MakeWeak(state->box); + const auto field = comment->entity(); + state->submit = [=](Api::SendOptions options) { const auto peers = state->box->collectSelectedRows(); + auto comment = field->getTextWithAppliedMarkdown(); + const auto checkPaid = [=] { + const auto withPaymentApproved = crl::guard(weak, [=]( + int approved) { + auto copy = options; + copy.starsApproved = approved; + if (const auto onstack = state->submit) { + onstack(copy); + } + }); + + const auto alreadyApproved = options.starsApproved; + const auto messagesCount = countMessages(comment); + auto paid = std::vector>(); + auto waiting = base::flat_set>(); + auto totalStars = 0; + for (const auto &peer : peers) { + 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); + } + }, state->submitLifetime); + + if (!session->credits().loaded()) { + session->credits().loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + withPaymentApproved(alreadyApproved); + }, state->submitLifetime); + } + return false; + } else if (totalStars > alreadyApproved) { + ShowSendPaidConfirm(show, paid, SendPaymentDetails{ + .messages = messagesCount, + .stars = totalStars, + }, [=] { withPaymentApproved(totalStars); }); + return false; + } + state->submit = nullptr; + return true; + }; send( ranges::views::all( peers @@ -2366,17 +2575,28 @@ QPointer ShowForwardMessagesBox( not_null peer) -> Controller::Chosen { return peer->owner().history(peer); }) | ranges::to_vector, - comment->entity()->getTextWithAppliedMarkdown(), + checkPaid, + std::move(comment), options, state->box->forwardOptionsData()); - if (successCallback) { + if (!state->submit && successCallback) { successCallback(); } }; const auto sendMenuType = [=] { const auto selected = state->box->collectSelectedRows(); - return ranges::all_of(selected, HistoryView::CanScheduleUntilOnline) + const auto hasPaid = [&] { + for (const auto peer : selected) { + if (peer->starsPerMessageChecked()) { + return true; + } + } + return false; + }(); + return hasPaid + ? SendMenu::Type::SilentOnly + : ranges::all_of(selected, HistoryView::CanScheduleUntilOnline) ? SendMenu::Type::ScheduledToUser : ((selected.size() == 1) && selected.front()->isSelf()) ? SendMenu::Type::Reminder @@ -2428,14 +2648,31 @@ QPointer ShowForwardMessagesBox( state->menu.get(), show, SendMenu::Details{ sendMenuType() }, - SendMenu::DefaultCallback(show, crl::guard(parent, submit))); + SendMenu::DefaultCallback(show, crl::guard(parent, [=]( + Api::SendOptions options) { + if (const auto onstack = state->submit) { + onstack(options); + } + }))); if (showForwardOptions || !state->menu->empty()) { state->menu->popup(QCursor::pos()); } }; + state->refreshStarsToSend = [=] { + auto perMessage = 0; + for (const auto &peer : state->box->collectSelectedRows()) { + perMessage += peer->starsPerMessageChecked(); + } + state->starsToSend = perMessage + * countMessages(field->getTextWithTags()); + }; + comment->hide(anim::type::instant); - comment->toggleOn(state->controller->hasSelectedChanges()); + comment->toggleOn(state->controller->selectionChanges( + ) | rpl::map([=] { + return state->controller->hasSelected(); + })); rpl::combine( state->box->sizeValue(), @@ -2447,10 +2684,12 @@ QPointer ShowForwardMessagesBox( state->box->setBottomSkip(comment->isHidden() ? 0 : commentHeight); }, comment->lifetime()); - const auto field = comment->entity(); - field->submits( - ) | rpl::start_with_next([=] { submit({}); }, field->lifetime()); + ) | rpl::start_with_next([=] { + if (const auto onstack = state->submit) { + onstack({}); + } + }, field->lifetime()); InitMessageFieldHandlers({ .session = session, .show = show, @@ -2460,6 +2699,9 @@ QPointer ShowForwardMessagesBox( }, }); field->setSubmitSettings(Core::App().settings().sendSubmitWay()); + field->changes() | rpl::start_with_next([=] { + state->refreshStarsToSend(); + }, field->lifetime()); Ui::SendPendingMoveResizeEvents(comment); @@ -2470,13 +2712,20 @@ QPointer ShowForwardMessagesBox( } }, comment->lifetime()); - state->controller->hasSelectedChanges( - ) | rpl::start_with_next([=](bool shown) { + state->controller->selectionChanges( + ) | rpl::start_with_next([=] { + const auto shown = state->controller->hasSelected(); + state->box->clearButtons(); + state->refreshStarsToSend(); if (shown) { const auto send = state->box->addButton( tr::lng_send_button(), - [=] { submit({}); }); + [=] { + if (const auto onstack = state->submit) { + onstack({}); + } + }); send->setAcceptBoth(); send->clicks( ) | rpl::start_with_next([=](Qt::MouseButton button) { @@ -2484,6 +2733,9 @@ QPointer ShowForwardMessagesBox( showMenu(send); } }, send->lifetime()); + send->setText(PaidSendButtonText( + state->starsToSend.value(), + tr::lng_send_button())); } state->box->addButton(tr::lng_cancel(), [=] { state->box->closeBox(); @@ -2563,7 +2815,7 @@ QPointer ShowShareGameBox( .session = &navigation->session(), .callback = std::move(chosen), .filter = std::move(filter), - .premiumRequiredError = WritePremiumRequiredError, + .moneyRestrictionError = WriteMoneyRestrictionError, }), std::move(initBox))); return weak->data(); diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 7e2314f59..97268366c 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace Api { +struct SendOptions; +} // namespace Api + namespace Ui { class RpWidget; class BoxContent; @@ -146,7 +150,9 @@ object_ptr PrepareChooseRecipientBox( rpl::producer titleOverride = nullptr, FnMut &&successCallback = nullptr, InlineBots::PeerTypes typesRestriction = 0, - Fn>)> sendMany = nullptr); + Fn>, + Api::SendOptions)> sendMany = nullptr); QPointer ShowChooseRecipientBox( not_null navigation, FnMut)> &&chosen, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index b5abfd6cb..f91148357 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -176,12 +176,6 @@ private: [[nodiscard]] Ui::CollectibleDetails PrepareCollectibleDetails( not_null session) { - const auto makeContext = [=] { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = [] {}, - }; - }; return { .tonEmoji = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( @@ -190,7 +184,7 @@ private: st::collectibleInfo.textFg->c), st::collectibleInfoTonMargins, true)), - .tonEmojiContext = makeContext, + .tonEmojiContext = Core::TextContext({ .session = session }), }; } diff --git a/Telegram/SourceFiles/window/window_slide_animation.cpp b/Telegram/SourceFiles/window/window_slide_animation.cpp index 54a843559..fced2ba81 100644 --- a/Telegram/SourceFiles/window/window_slide_animation.cpp +++ b/Telegram/SourceFiles/window/window_slide_animation.cpp @@ -194,11 +194,15 @@ void SlideAnimation::start() { fromLeft ? 0. : 1., st::slideDuration, transition()); - _repaintCallback(); + if (const auto onstack = _repaintCallback) { + onstack(); + } } void SlideAnimation::animationCallback() { - _repaintCallback(); + if (const auto onstack = _repaintCallback) { + onstack(); + } if (!_animation.animating()) { if (const auto onstack = _finishedCallback) { onstack(); diff --git a/Telegram/build/version b/Telegram/build/version index 2e287b997..eac8b2553 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5011001 -AppVersionStrMajor 5.11 -AppVersionStrSmall 5.11.1 -AppVersionStr 5.11.1 +AppVersion 5012001 +AppVersionStrMajor 5.12 +AppVersionStrSmall 5.12.1 +AppVersionStr 5.12.1 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.11.1 +AppVersionOriginal 5.12.1 diff --git a/Telegram/lib_base b/Telegram/lib_base index 90a358695..b28088164 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 90a358695a6188cd80f640e1b6b8faab40c9221b +Subproject commit b28088164b7a46c70ae2cfd9daf865f6425610b2 diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 1a700e5a0..3eb4a97f1 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 1a700e5a0d7c3e2f617530354ff2a47c5c72bb4a +Subproject commit 3eb4a97f1dd038bc4b6bd2884262242382a37e79 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index a0b1afdbd..f54696991 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit a0b1afdbdad2017075d05bf4c3054a399ea55e0b +Subproject commit f546969919a5946d49a504f8159041fa5b55c3df diff --git a/changelog.txt b/changelog.txt index 97a490c23..9793a1e23 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,15 @@ +5.12.1 (08.03.25) + +- Fix a crash in some chat switchings. +- Fix crashes in empty repaint callbacks. + +5.12 (07.03.25) + +- Set a fee for incoming messages from unknown users. +- Set a fee for messages in groups or channel comments. +- Show some information about who's messaging you. +- Pin gifts on your profile. + 5.11.1 (13.02.25) - Fix arbitrary cropping support in the image editor.