diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7d3841211..97c8501a7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2031,11 +2031,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_got_subtitle" = "Gift from {user}"; "lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star."; "lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars."; +"lng_action_gift_got_upgradable_text" = "Upgrade this gift to a unique collectible."; "lng_action_gift_got_gift_text" = "You can keep this gift on your page."; "lng_action_gift_can_remove_text" = "You can remove this gift from your page."; "lng_action_gift_sent_subtitle" = "Gift for {user}"; "lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star."; "lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars."; +"lng_action_gift_sent_upgradable" = "{user} can upgrade this gift to a unique collectible."; "lng_action_gift_premium_months#one" = "{count} Month Premium"; "lng_action_gift_premium_months#other" = "{count} Months Premium"; "lng_action_gift_premium_about" = "Subscription for exclusive Telegram features."; @@ -3227,6 +3229,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_send_message" = "Enter Message"; "lng_gift_send_anonymous" = "Hide My Name"; "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_unique" = "Make Unique for {price}"; +"lng_gift_send_unique_about" = "Enable this to let {user} turn your gift into a unique collectible. {link}"; +"lng_gift_send_unique_link" = "Learn More >"; "lng_gift_send_premium_about" = "Only {user} will see your message."; "lng_gift_send_button" = "Send a Gift for {cost}"; "lng_gift_sent_title" = "Gift Sent!"; @@ -3235,6 +3240,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; "lng_gift_price_unique" = "Unique"; +"lng_gift_view_unpack" = "Unpack"; "lng_gift_anonymous_hint" = "Only you can see the sender's name."; "lng_gift_hidden_hint" = "This gift is hidden. Only you can see it."; "lng_gift_visible_hint" = "This gift is visible to visitors of your page."; @@ -3286,6 +3292,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_sell_small#other" = "sell for {count} Stars"; "lng_gift_upgrade_title" = "Upgrade Gift"; "lng_gift_upgrade_about" = "Turn your gift into a unique collectible\nthat you can transfer or auction."; +"lng_gift_upgrade_preview_title" = "Make Unique"; +"lng_gift_upgrade_preview_about" = "Let {name} turn your gift into a unique collectible."; "lng_gift_upgrade_unique_title" = "Unique"; "lng_gift_upgrade_unique_about" = "Get a unique number, model, backdrop and symbol for your gift."; "lng_gift_upgrade_transferable_title" = "Transferable"; @@ -3293,7 +3301,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_upgrade_tradable_title" = "Tradable"; "lng_gift_upgrade_tradable_about" = "Sell or auction your gift on third-party NFT marketplaces."; "lng_gift_upgrade_button" = "Upgrade for {price}"; -"lng_gift_upgrade_add_sender" = "Add sender's name"; +"lng_gift_upgrade_free" = "Upgrade for Free"; +"lng_gift_upgrade_confirm" = "Confirm"; +"lng_gift_upgrade_add_my" = "Add my name to the gift"; +"lng_gift_upgrade_add_sender" = "Add sender's name to the gift"; "lng_gift_upgrade_add_comment" = "Add sender's name and comment"; "lng_gift_upgraded_title" = "Gift Upgraded"; "lng_gift_upgraded_about" = "Your gift {name} now has unique attributes and can be transferred to others"; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 88b46e126..0bed9cf1a 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -130,7 +130,8 @@ constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); not_null parent, not_null controller, PeerId id, - bool withSendGiftButton = false) { + rpl::producer button = nullptr, + Fn handler = nullptr) { auto result = object_ptr(parent); const auto raw = result.data(); @@ -141,19 +142,17 @@ constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); const auto userpic = Ui::CreateChild(raw, peer, st); const auto label = Ui::CreateChild( raw, - withSendGiftButton ? peer->shortName() : peer->name(), + (button && handler) ? peer->shortName() : peer->name(), st::giveawayGiftCodeValue); - const auto send = withSendGiftButton + const auto send = (button && handler) ? Ui::CreateChild( raw, - tr::lng_gift_send_small(), + std::move(button), st::starGiftSmallButton) : nullptr; if (send) { send->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - send->setClickedCallback([=] { - Ui::ShowStarGiftBox(controller->parentController(), peer); - }); + send->setClickedCallback(std::move(handler)); } rpl::combine( raw->widthValue(), @@ -1204,19 +1203,25 @@ void AddStarGiftTable( const auto session = &controller->session(); const auto unique = entry.uniqueGift.get(); if (unique) { + const auto ownerId = PeerId(entry.bareGiftOwnerId); + auto send = entry.in ? tr::lng_gift_unique_owner_change() : nullptr; + auto handler = entry.in ? Fn([=] {}) : nullptr; AddTableRow( table, tr::lng_gift_unique_owner(), - MakePeerTableValue(table, controller, peerId, false), + MakePeerTableValue(table, controller, ownerId, send, handler), st::giveawayGiftCodePeerMargin); - //"lng_gift_unique_owner_change" = "change"; } else if (peerId) { const auto user = session->data().peer(peerId)->asUser(); const auto withSendButton = entry.in && user && !user->isBot(); + auto send = withSendButton ? tr::lng_gift_send_small() : nullptr; + auto handler = send ? Fn([=] { + Ui::ShowStarGiftBox(controller->parentController(), user); + }) : nullptr; AddTableRow( table, tr::lng_credits_box_history_entry_peer_in(), - MakePeerTableValue(table, controller, peerId, withSendButton), + MakePeerTableValue(table, controller, peerId, send, handler), st::giveawayGiftCodePeerMargin); } else if (!entry.soldOutInfo) { AddTableRow( @@ -1390,6 +1395,12 @@ 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 @@ -1427,7 +1438,9 @@ void AddStarGiftTable( lt_text, rpl::single(original.message), Ui::Text::WithEntities))), - st::giveawayGiftMessage); + st::giveawayGiftMessage, + st::defaultPopupMenu, + makeContext); const auto showBoxLink = [=](not_null peer) { return std::make_shared([=] { controller->uiShow()->showBox( diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 4d661f1b1..bf8e025b6 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "history/admin_log/history_admin_log_item.h" #include "history/view/media/history_view_media_generic.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" @@ -67,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/shadow.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" @@ -92,6 +94,7 @@ constexpr auto kGiftMessageLimit = 255; 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); using namespace HistoryView; using namespace Info::PeerGifts; @@ -111,6 +114,7 @@ struct GiftDetails { TextWithEntities text; uint64 randomId = 0; bool anonymous = false; + bool upgraded = false; }; class PreviewDelegate final : public DefaultElementDelegate { @@ -250,11 +254,15 @@ auto GenerateGiftMedia( tr::now, Text::RichLangValue); }, [&](const GiftTypeStars &gift) { - return tr::lng_action_gift_got_stars_text( - tr::now, - lt_count, - gift.info.starsConverted, - Text::RichLangValue); + return data.upgraded + ? tr::lng_action_gift_got_upgradable_text( + tr::now, + Text::RichLangValue) + : tr::lng_action_gift_got_stars_text( + tr::now, + lt_count, + gift.info.starsConverted, + Text::RichLangValue); }); auto description = data.text.empty() ? std::move(textFallback) @@ -268,6 +276,14 @@ auto GenerateGiftMedia( .session = &parent->history()->session(), .customEmojiRepaint = [parent] { parent->repaint(); }, }); + + push(HistoryView::MakeGenericButtonPart( + (data.upgraded + ? tr::lng_gift_view_unpack(tr::now) + : tr::lng_sticker_premium_view(tr::now)), + st::giftBoxButtonMargin, + [parent] { parent->repaint(); }, + nullptr)); }; } @@ -401,7 +417,8 @@ PreviewWrap::PreviewWrap( void ShowSentToast( not_null window, - const GiftDescriptor &descriptor) { + const GiftDescriptor &descriptor, + const GiftDetails &details) { const auto &st = st::historyPremiumToast; const auto skip = st.padding.top(); const auto size = st.style.font->height * 2; @@ -414,10 +431,12 @@ void ShowSentToast( tr::now, Text::RichLangValue); }, [&](const GiftTypeStars &gift) { + const auto amount = gift.info.stars + + (details.upgraded ? gift.info.starsToUpgrade : 0); return tr::lng_gift_sent_about( tr::now, lt_count, - gift.info.stars, + amount, Text::RichLangValue); }); const auto strong = window->showToast({ @@ -475,7 +494,8 @@ void PreviewWrap::prepare(rpl::producer details) { const auto cost = v::match(descriptor, [&](GiftTypePremium data) { return FillAmountAndCurrency(data.cost, data.currency, true); }, [&](GiftTypeStars data) { - const auto stars = data.info.stars; + const auto stars = data.info.stars + + (details.upgraded ? data.info.starsToUpgrade : 0); return stars ? tr::lng_gift_stars_title(tr::now, lt_count, stars) : QString(); @@ -1063,6 +1083,7 @@ void SendGift( .user = peer->asUser(), .limitedCount = gift.info.limitedCount, .anonymous = details.anonymous, + .upgraded = details.upgraded, }, done, processNonPanelPaymentFormFactory); }); } @@ -1090,6 +1111,24 @@ void SendGift( return result; } +void ShowGiftUpgradedToast( + base::weak_ptr weak, + not_null session, + const MTPUpdates & result) { + const auto gift = FindUniqueGift(session, result); + if (const auto strong = gift ? weak.get() : nullptr) { + strong->showToast({ + .title = tr::lng_gift_upgraded_title(tr::now), + .text = tr::lng_gift_upgraded_about( + tr::now, + lt_name, + Text::Bold(Data::UniqueGiftName(*gift)), + Ui::Text::WithEntities), + .duration = kUpgradeDoneToastDuration, + }); + } +} + void SendUpgradeRequest( not_null controller, Settings::SmallBalanceResult result, @@ -1108,17 +1147,7 @@ void SendUpgradeRequest( )).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { session->api().applyUpdates(data.vupdates()); - const auto gift = FindUniqueGift(session, data.vupdates()); - if (const auto strong = gift ? weak.get() : nullptr) { - strong->showToast({ - .title = tr::lng_gift_upgraded_title(tr::now), - .text = tr::lng_gift_upgraded_about( - tr::now, - lt_name, - Text::Bold(Data::UniqueGiftName(*gift)), - Ui::Text::WithEntities), - }); - } + ShowGiftUpgradedToast(weak, session, data.vupdates()); }, [](const MTPDpayments_paymentVerificationNeeded &data) { }); done(Payments::CheckoutResult::Paid); @@ -1150,17 +1179,8 @@ void UpgradeGift( MTP_int(messageId.bare) )).done([=](const MTPUpdates &result) { session->api().applyUpdates(result); - const auto gift = FindUniqueGift(session, result); - if (const auto strong = gift ? weak.get() : nullptr) { - strong->showToast({ - .title = tr::lng_gift_upgraded_title(tr::now), - .text = tr::lng_gift_upgraded_about( - tr::now, - lt_name, - Text::Bold(Data::UniqueGiftName(*gift)), - Ui::Text::WithEntities), - }); - } + ShowGiftUpgradedToast(weak, session, result); + done(Payments::CheckoutResult::Paid); }).fail([=](const MTP::Error &error) { if (const auto strong = weak.get()) { strong->showToast(error.type()); @@ -1233,6 +1253,69 @@ void SoldOutBox( Data::SubscriptionEntry()); } +void AddUpgradeButton( + not_null container, + not_null session, + int cost, + QString name, + Fn toggled, + Fn preview) { + const auto button = container->add( + object_ptr( + container, + rpl::single(QString()), + st::settingsButtonNoIcon)); + 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, + tr::lng_gift_send_unique( + lt_price, + rpl::single(star.append(' ' + + Lang::FormatStarsAmountDecimal( + StarsAmount{ cost }))), + Text::WithEntities), + st::boxLabel, + st::defaultPopupMenu, + std::move(makeContext)); + label->show(); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + button->widthValue() | rpl::start_with_next([=](int outer) { + const auto padding = st::settingsButtonNoIcon.padding; + const auto inner = outer + - padding.left() + - padding.right() + - st::settingsButtonNoIcon.toggleSkip + - 2 * st::settingsButtonNoIcon.toggle.border + - 2 * st::settingsButtonNoIcon.toggle.diameter + - 2 * st::settingsButtonNoIcon.toggle.width; + label->resizeToWidth(inner); + label->moveToLeft(padding.left(), padding.top(), outer); + }, label->lifetime()); + + AddSkip(container); + const auto about = AddDividerText( + container, + tr::lng_gift_send_unique_about( + lt_user, + rpl::single(TextWithEntities{ name }), + lt_link, + tr::lng_gift_send_unique_link() | Text::ToLink(), + Text::WithEntities)); + about->setClickHandlerFilter([=](const auto &...) { + preview(); + return false; + }); +} + void SendGiftBox( not_null box, not_null window, @@ -1247,20 +1330,6 @@ void SendGiftBox( }); const auto session = &window->session(); - auto cost = rpl::single([&] { - return v::match(descriptor, [&](const GiftTypePremium &data) { - if (data.currency == kCreditsCurrency) { - return CreditsEmojiSmall(session).append( - Lang::FormatCountDecimal(std::abs(data.cost))); - } - return TextWithEntities{ - FillAmountAndCurrency(data.cost, data.currency), - }; - }, [&](const GiftTypeStars &data) { - return CreditsEmojiSmall(session).append( - Lang::FormatCountDecimal(std::abs(data.info.stars))); - }); - }()); struct State { rpl::variable details; @@ -1272,7 +1341,26 @@ void SendGiftBox( .descriptor = descriptor, .randomId = base::RandomValue(), }; - const auto document = LookupGiftSticker(&window->session(), descriptor); + + auto cost = state->details.value( + ) | rpl::map([session](const GiftDetails &details) { + return v::match(details.descriptor, [&](const GiftTypePremium &data) { + if (data.currency == kCreditsCurrency) { + return CreditsEmojiSmall(session).append( + Lang::FormatCountDecimal(std::abs(data.cost))); + } + return TextWithEntities{ + FillAmountAndCurrency(data.cost, data.currency), + }; + }, [&](const GiftTypeStars &data) { + const auto amount = std::abs(data.info.stars) + + (details.upgraded ? data.info.starsToUpgrade : 0); + return CreditsEmojiSmall(session).append( + Lang::FormatCountDecimal(amount)); + }); + }); + + const auto document = LookupGiftSticker(session, descriptor); if ((state->media = document ? document->createMediaView() : nullptr)) { state->media->checkStickerLarge(); } @@ -1283,7 +1371,7 @@ void SendGiftBox( session, state->details.value())); - const auto limit = StarGiftMessageLimit(&window->session()); + const auto limit = StarGiftMessageLimit(session); const auto text = AddPartInput( window, container, @@ -1309,7 +1397,7 @@ void SendGiftBox( return true; }; InitMessageFieldHandlers({ - .session = &window->session(), + .session = session, .show = window->uiShow(), .field = text, .customEmojiPaused = [=] { @@ -1328,11 +1416,39 @@ void SendGiftBox( Emoji::SuggestionsController::Init( box->getDelegate()->outerContainer(), text, - &window->session(), + session, { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); - if (v::is(descriptor)) { - AddDivider(container); + if (const auto stars = std::get_if(&descriptor)) { + if (const auto cost = stars->info.starsToUpgrade; cost > 0) { + const auto user = peer->asUser(); + Assert(user != nullptr); + + const auto id = stars->info.id; + const auto name = user->shortName(); + const auto showing = std::make_shared(); + AddDivider(container); + AddSkip(container); + AddUpgradeButton(container, session, cost, name, [=](bool on) { + auto now = state->details.current(); + now.upgraded = on; + state->details = std::move(now); + }, [=] { + if (*showing) { + return; + } + *showing = true; + ShowStarGiftUpgradeBox({ + .controller = window, + .stargiftId = id, + .ready = [=](bool) { *showing = false; }, + .user = user, + .cost = int(cost), + }); + }); + } else { + AddDivider(container); + } AddSkip(container); container->add( object_ptr( @@ -1373,7 +1489,7 @@ void SendGiftBox( if (result == Payments::CheckoutResult::Paid) { const auto copy = state->media; window->showPeerHistory(peer); - ShowSentToast(window, descriptor); + ShowSentToast(window, descriptor, details); } if (const auto strong = weak.data()) { box->closeBox(); @@ -1894,13 +2010,10 @@ void AddUniqueGiftCover( }, cover->lifetime()); } -struct UpgradeArgs { +struct UpgradeArgs : StarGiftUpgradeArgs { std::vector models; std::vector patterns; std::vector backdrops; - not_null user; - MsgId itemId = 0; - int stars = 0; }; [[nodiscard]] rpl::producer MakeUpgradeGiftStream( @@ -1925,22 +2038,31 @@ struct UpgradeArgs { const auto put = [=] { const auto index = [](std::vector &indices, const auto &v) { - if (indices.empty()) { + const auto fill = [&] { + if (!indices.empty()) { + return; + } indices = ranges::views::ints(0) | ranges::views::take( v.size() ) | ranges::to_vector; + ranges::shuffle(indices); + }; + fill(); + const auto result = indices.back(); + indices.pop_back(); + fill(); + if (indices.back() == result) { + std::swap(indices.front(), indices.back()); } - const auto index = base::RandomIndex(indices.size()); - const auto i = begin(indices) + index; - const auto result = *i; - indices.erase(i); return result; }; auto &models = state->data.models; auto &patterns = state->data.patterns; auto &backdrops = state->data.backdrops; consumer.put_next(Data::UniqueGift{ - .title = tr::lng_gift_upgrade_title(tr::now), + .title = (state->data.itemId + ? tr::lng_gift_upgrade_title(tr::now) + : tr::lng_gift_upgrade_preview_title(tr::now)), .model = models[index(state->modelIndices, models)], .pattern = patterns[index(state->patternIndices, patterns)], .backdrop = backdrops[index(state->backdropIndices, backdrops)], @@ -1962,7 +2084,11 @@ void AddUpgradeGiftCover( AddUniqueGiftCover( container, MakeUpgradeGiftStream(args), - tr::lng_gift_upgrade_about()); + (args.itemId + ? tr::lng_gift_upgrade_about() + : tr::lng_gift_upgrade_preview_about( + lt_name, + rpl::single(args.user->shortName())))); } void UpgradeBox( @@ -2025,45 +2151,75 @@ void UpgradeBox( &st::menuIconReplace, true); - container->add( - object_ptr(container), - st::boxRowPadding + QMargins(0, st::defaultVerticalListSkip, 0, 0)); - - box->setStyle(st::giftBox); - struct State { bool sent = false; + bool preserveDetails = false; }; - const auto stars = args.stars; - const auto session = &controller->session(); const auto state = std::make_shared(); - const auto button = box->addButton(rpl::single(QString()), [=] { - if (state->sent) { + const auto preview = !args.itemId; + + if (!preview) { + const auto skip = st::defaultVerticalListSkip; + container->add( + object_ptr(container), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + const auto checkbox = container->add( + object_ptr>( + container, + object_ptr(container, args.canAddComment + ? tr::lng_gift_upgrade_add_comment(tr::now) + : args.canAddSender + ? tr::lng_gift_upgrade_add_sender(tr::now) + : tr::lng_gift_upgrade_add_my(tr::now))), + st::defaultCheckbox.margin)->entity(); + checkbox->checkedChanges() | rpl::start_with_next([=](bool checked) { + state->preserveDetails = checked; + }, checkbox->lifetime()); + } + + box->setStyle(preview ? st::giftBox : st::upgradeGiftBox); + + const auto cost = args.cost; + const auto session = &controller->session(); + auto buttonText = preview ? tr::lng_box_ok() : rpl::single(QString()); + const auto button = box->addButton(std::move(buttonText), [=] { + if (preview) { + box->closeBox(); + return; + } else if (state->sent) { return; } state->sent = true; - const auto keepDetails = true; + const auto keepDetails = state->preserveDetails; const auto weak = Ui::MakeWeak(box); const auto done = [=](Payments::CheckoutResult result) { if (result != Payments::CheckoutResult::Paid) { state->sent = false; - } else if (const auto strong = weak.data()) { - strong->closeBox(); + } else { + controller->showPeerHistory(args.user); + if (const auto strong = weak.data()) { + strong->closeBox(); + } } }; - UpgradeGift(controller, args.itemId, keepDetails, stars, done); + UpgradeGift(controller, args.itemId, keepDetails, cost, done); }); - auto star = session->data().customEmojiManager().creditsEmoji(); - SetButtonMarkedLabel( - button, - tr::lng_gift_upgrade_button( - lt_price, - rpl::single(star.append( - ' ' + Lang::FormatStarsAmountDecimal(StarsAmount{ 25 }))), - Ui::Text::WithEntities), - &controller->session(), - st::creditsBoxButtonLabel, - &st::giftBox.button.textFg); + if (!preview) { + auto star = session->data().customEmojiManager().creditsEmoji(); + SetButtonMarkedLabel( + button, + (cost + ? tr::lng_gift_upgrade_button( + lt_price, + rpl::single(star.append( + ' ' + Lang::FormatStarsAmountDecimal( + StarsAmount{ cost }))), + Ui::Text::WithEntities) + : tr::lng_gift_upgrade_confirm(Ui::Text::WithEntities)), + &controller->session(), + st::creditsBoxButtonLabel, + &st::giftBox.button.textFg); + } rpl::combine( box->widthValue(), button->widthValue() @@ -2118,45 +2274,41 @@ void PaintPoints( } } -void ShowStarGiftUpgradeBox( - not_null controller, - uint64 stargiftId, - not_null user, - MsgId itemId, - int stars, - Fn ready) { - const auto weak = base::make_weak(controller); - user->session().api().request(MTPpayments_GetStarGiftUpgradePreview( - MTP_long(stargiftId) +void ShowStarGiftUpgradeBox(StarGiftUpgradeArgs &&args) { + const auto weak = base::make_weak(args.controller); + const auto session = &args.user->session(); + session->api().request(MTPpayments_GetStarGiftUpgradePreview( + MTP_long(args.stargiftId) )).done([=](const MTPpayments_StarGiftUpgradePreview &result) { const auto strong = weak.get(); if (!strong) { - ready(false); + if (const auto onstack = args.ready) { + onstack(false); + } return; } const auto &data = result.data(); - const auto session = &user->session(); - auto args = UpgradeArgs{ - .user = user, - .itemId = itemId, - .stars = stars, - }; + auto upgrade = UpgradeArgs{ args }; for (const auto &attribute : data.vsample_attributes().v) { attribute.match([&](const MTPDstarGiftAttributeModel &data) { - args.models.push_back(Api::FromTL(session, data)); + upgrade.models.push_back(Api::FromTL(session, data)); }, [&](const MTPDstarGiftAttributePattern &data) { - args.patterns.push_back(Api::FromTL(session, data)); + upgrade.patterns.push_back(Api::FromTL(session, data)); }, [&](const MTPDstarGiftAttributeBackdrop &data) { - args.backdrops.push_back(Api::FromTL(data)); + upgrade.backdrops.push_back(Api::FromTL(data)); }, [](const auto &) {}); } - controller->show(Box(UpgradeBox, controller, std::move(args))); - ready(true); + strong->show(Box(UpgradeBox, strong, std::move(upgrade))); + if (const auto onstack = args.ready) { + onstack(true); + } }).fail([=](const MTP::Error &error) { if (const auto strong = weak.get()) { strong->showToast(error.type()); } - ready(false); + if (const auto onstack = args.ready) { + onstack(false); + } }).send(); } diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 4ae5f7704..70d1ae8bf 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -46,13 +46,17 @@ void PaintPoints( const QRect &rect, float64 shown = 1.); -void ShowStarGiftUpgradeBox( - not_null controller, - uint64 stargiftId, - not_null user, - MsgId itemId, - int stars, - Fn ready); +struct StarGiftUpgradeArgs { + not_null controller; + base::required stargiftId; + Fn ready; + not_null user; + MsgId itemId = 0; + int cost = 0; + bool canAddSender = false; + bool canAddComment = false; +}; +void ShowStarGiftUpgradeBox(StarGiftUpgradeArgs &&args); void AddUniqueCloseButton(not_null box); diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index c84f55f0a..5ab71ef7b 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -64,6 +64,7 @@ struct CreditsHistoryEntry final { uint64 barePeerId = 0; uint64 bareGiveawayMsgId = 0; uint64 bareGiftStickerId = 0; + uint64 bareGiftOwnerId = 0; uint64 bareActorId = 0; uint64 stargiftId = 0; std::shared_ptr uniqueGift; @@ -87,6 +88,7 @@ struct CreditsHistoryEntry final { bool fromGiftsList : 1 = false; bool soldOutInfo : 1 = false; bool canUpgradeGift : 1 = false; + bool hasGiftComment : 1 = false; bool reaction : 1 = false; bool refunded : 1 = false; bool pending : 1 = false; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 8ba9b36c2..2306eb231 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -5407,7 +5407,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { const auto peer = isSelf ? _history->peer : _from; const auto stars = action.vgift().match([&]( const MTPDstarGift &data) { - return uint64(data.vstars().v); + return uint64(data.vstars().v) + + uint64(data.vupgrade_stars().value_or_empty()); }, [](const MTPDstarGiftUnique &) { return uint64(); }); diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index a854b4108..950ab283b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -78,14 +78,24 @@ TextWithEntities PremiumGift::subtitle() { return !_data.message.empty() ? _data.message : outgoingGift() - ? tr::lng_action_gift_sent_text( + ? (_data.starsUpgradedBySender + ? tr::lng_action_gift_sent_upgradable( + tr::now, + lt_user, + Ui::Text::Bold(_parent->history()->peer->shortName()), + Ui::Text::RichLangValue) + : tr::lng_action_gift_sent_text( + tr::now, + lt_count, + _data.starsConverted, + lt_user, + Ui::Text::Bold(_parent->history()->peer->shortName()), + Ui::Text::RichLangValue)) + : _data.starsUpgradedBySender + ? tr::lng_action_gift_got_upgradable_text( tr::now, - lt_count, - _data.starsConverted, - lt_user, - Ui::Text::Bold(_parent->history()->peer->shortName()), Ui::Text::RichLangValue) - : (_data.converted + : ((_data.converted || !_data.starsConverted) ? tr::lng_gift_got_stars : tr::lng_action_gift_got_stars_text)( tr::now, @@ -147,6 +157,8 @@ rpl::producer PremiumGift::button() { ? nullptr : creditsPrize() ? tr::lng_view_button_giftcode() + : (starGift() && _data.starsUpgradedBySender && !_data.upgraded) + ? tr::lng_gift_view_unpack() : (gift() && (outgoingGift() || !_data.unclaimed)) ? tr::lng_sticker_premium_view() : tr::lng_prize_open(); 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 d2f166c43..eeea9b166 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -96,9 +96,9 @@ public: ButtonPart( const QString &text, QMargins margins, - QColor bg, Fn repaint, - ClickHandlerPtr link); + ClickHandlerPtr link, + QColor bg = QColor(0, 0, 0, 0)); void draw( Painter &p, @@ -126,6 +126,7 @@ private: ClickHandlerPtr _link; std::unique_ptr _ripple; mutable Ui::Premium::ColoredMiniStars _stars; + mutable std::optional _starsLastColor; Fn _repaint; mutable QPoint _lastPoint; @@ -135,9 +136,9 @@ private: ButtonPart::ButtonPart( const QString &text, QMargins margins, - QColor bg, Fn repaint, - ClickHandlerPtr link) + ClickHandlerPtr link, + QColor bg) : _text(st::semiboldTextStyle, text) , _margins(margins) , _bg(bg) @@ -152,13 +153,6 @@ ButtonPart::ButtonPart( repaint(); }, Ui::Premium::MiniStars::Type::SlowStars) , _repaint(std::move(repaint)) { - _stars.setColorOverride(QGradientStops{ - { 0., QColor(255, 255, 255, 255 * .3) }, - { 1., QColor(255, 255, 255) }, - }); - const auto padding = _size.height() / 2; - _stars.setCenter( - Rect(_size) - QMargins(padding, 0, padding, 0)); } void ButtonPart::draw( @@ -168,17 +162,32 @@ void ButtonPart::draw( int outerWidth) const { PainterHighQualityEnabler hq(p); - const auto position = QPoint( + const auto customColors = (_bg.alpha() > 0); + + const auto position = QPoint( (outerWidth - width()) / 2 + _margins.left(), _margins.top()); p.translate(position); p.setPen(Qt::NoPen); - p.setBrush(_bg); + p.setBrush(customColors ? QBrush(_bg) : context.st->msgServiceBg()); const auto radius = _size.height() / 2.; const auto r = Rect(_size); p.drawRoundedRect(r, radius, radius); + auto white = QColor(255, 255, 255); + const auto fg = customColors ? white : context.st->msgServiceFg()->c; + if (!_starsLastColor || *_starsLastColor != fg) { + _starsLastColor = fg; + _stars.setColorOverride(QGradientStops{ + { 0., anim::with_alpha(fg, .3) }, + { 1., fg }, + }); + const auto padding = _size.height() / 2; + _stars.setCenter( + Rect(_size) - QMargins(padding, 0, padding, 0)); + } + auto clipPath = QPainterPath(); clipPath.addRoundedRect(r, radius, radius); p.setClipPath(clipPath); @@ -186,20 +195,22 @@ void ButtonPart::draw( _stars.paint(p); p.setClipping(false); - auto white = QColor(255, 255, 255); - p.setPen(white); if (_ripple) { const auto opacity = p.opacity(); + const auto ripple = customColors + ? anim::with_alpha(fg, .3) + : context.messageStyle()->msgWaveformInactive->c; p.setOpacity(st::historyPollRippleOpacity); - white.setAlphaF(0.3); _ripple->paint( p, 0, 0, width(), - &white); + &ripple); p.setOpacity(opacity); } + + p.setPen(fg); _text.draw( p, 0, @@ -456,9 +467,9 @@ auto GenerateUniqueGiftMedia( push(std::make_unique( tr::lng_sticker_premium_view(tr::now), st::chatUniqueButtonPadding, - gift->backdrop.patternColor, [=] { parent->repaint(); }, - std::move(link))); + std::move(link), + gift->backdrop.patternColor)); } }; } @@ -537,4 +548,13 @@ Fn UniqueGiftBg( }; } +std::unique_ptr MakeGenericButtonPart( + const QString &text, + QMargins margins, + Fn repaint, + ClickHandlerPtr link, + QColor bg) { + return std::make_unique(text, margins, repaint, link, bg); +} + } // 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 aa6d661b0..32061081c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h @@ -29,8 +29,15 @@ auto GenerateUniqueGiftMedia( not_null gift) -> Fn)>)>; -Fn UniqueGiftBg( +[[nodiscard]] Fn UniqueGiftBg( not_null view, not_null gift); +[[nodiscard]] std::unique_ptr MakeGenericButtonPart( + const QString &text, + QMargins margins, + Fn repaint, + ClickHandlerPtr link, + QColor bg = QColor(0, 0, 0, 0)); + } // namespace HistoryView 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 b29e937fc..024901ed2 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -326,6 +326,7 @@ void InnerWidget::showGift(int index) { _window->show(Box( ::Settings::UserStarGiftBox, _window, + _user, _entries[index].gift)); } diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 6431796e0..741bf12cc 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -383,7 +383,8 @@ MTPInputInvoice Form::inputInvoice() const { using Flag = MTPDinputInvoiceStarGift::Flag; return MTP_inputInvoiceStarGift( MTP_flags((gift->anonymous ? Flag::f_hide_name : Flag(0)) - | (gift->message.empty() ? Flag(0) : Flag::f_message)), + | (gift->message.empty() ? Flag(0) : Flag::f_message) + | (gift->upgraded ? Flag::f_include_upgrade : Flag(0))), gift->user->inputUser, MTP_long(gift->giftId), MTP_textWithEntities( diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index a2106cb53..e2df89143 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -180,6 +180,7 @@ struct InvoiceStarGift { not_null user; int limitedCount = 0; bool anonymous = false; + bool upgraded = false; }; struct InvoiceId { diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index bc8c7ecbe..0ef0c2ffa 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1182,25 +1182,28 @@ void ReceiptCreditsBox( box, object_ptr( box, - ((couldConvert || nonConvertible) - ? (e.savedToProfile - ? tr::lng_action_gift_can_remove_text - : tr::lng_action_gift_got_gift_text)( - Ui::Text::WithEntities) - : rpl::combine( - (canConvert - ? tr::lng_action_gift_got_stars_text - : tr::lng_gift_got_stars)( - lt_count, - rpl::single(e.starsConverted * 1.), - Ui::Text::RichLangValue), - tr::lng_paid_about_link() - ) | rpl::map([]( - TextWithEntities text, - QString link) { - return text.append(' ').append( - Ui::Text::Link(link)); - })), + (e.starsUpgradedBySender + ? tr::lng_action_gift_got_upgradable_text( + Ui::Text::RichLangValue) + : ((couldConvert || nonConvertible) + ? (e.savedToProfile + ? tr::lng_action_gift_can_remove_text + : tr::lng_action_gift_got_gift_text)( + Ui::Text::WithEntities) + : rpl::combine( + (canConvert + ? tr::lng_action_gift_got_stars_text + : tr::lng_gift_got_stars)( + lt_count, + rpl::single(e.starsConverted * 1.), + Ui::Text::RichLangValue), + tr::lng_paid_about_link() + ) | rpl::map([]( + TextWithEntities text, + QString link) { + return text.append(' ').append( + Ui::Text::Link(link)); + }))), st::creditsBoxAbout)))->entity(); about->setClickHandlerFilter([=](const auto &...) { Core::App().iv().openWithIvPreferred( @@ -1297,6 +1300,32 @@ void ReceiptCreditsBox( done); }; + const auto upgradeGuard = std::make_shared(); + const auto upgrade = [=] { + if (const auto window = weakWindow.get()) { + const auto itemId = MsgId(e.bareMsgId); + if (*upgradeGuard) { + return; + } + *upgradeGuard = true; + using namespace Ui; + ShowStarGiftUpgradeBox({ + .controller = window, + .stargiftId = e.stargiftId, + .ready = [=](bool) { *upgradeGuard = false; }, + .user = starGiftSender, + .itemId = itemId, + .cost = e.starsUpgradedBySender ? 0 : e.starsToUpgrade, + .canAddSender = !e.anonymous, + .canAddComment = !e.anonymous && e.hasGiftComment, + }); + } + }; + const auto canUpgrade = e.stargiftId + && e.canUpgradeGift + && !e.uniqueGift; + const auto canUpgradeFree = canUpgrade && (e.starsUpgradedBySender > 0); + if (isStarGift && e.id.isEmpty()) { const auto convert = [=, weak = Ui::MakeWeak(box)] { const auto stars = e.starsConverted; @@ -1340,28 +1369,7 @@ void ReceiptCreditsBox( } }); }; - const auto upgradeGuard = std::make_shared(); - const auto upgrade = [=] { - if (const auto window = weakWindow.get()) { - const auto itemId = MsgId(e.bareMsgId); - if (*upgradeGuard) { - return; - } - *upgradeGuard = true; - using namespace Ui; - ShowStarGiftUpgradeBox( - window, - e.stargiftId, - starGiftSender, - itemId, - e.starsUpgradedBySender ? 0 : e.starsToUpgrade, - [=](bool) { *upgradeGuard = false; }); - } - }; const auto canToggle = canConvert || couldConvert || nonConvertible; - const auto canUpgrade = e.stargiftId - && e.canUpgradeGift - && !e.uniqueGift; AddStarGiftTable( controller, @@ -1479,6 +1487,8 @@ void ReceiptCreditsBox( ? tr::lng_credits_subscription_off_button() : toRejoin ? tr::lng_credits_subscription_off_rejoin_button() + : canUpgradeFree + ? tr::lng_gift_upgrade_free() : tr::lng_box_ok())); const auto send = [=, weak = Ui::MakeWeak(box)] { if (toRejoin) { @@ -1531,6 +1541,8 @@ void ReceiptCreditsBox( if (willBusy) { state->confirmButtonBusy = true; send(); + } else if (canUpgradeFree) { + upgrade(); } else { box->closeBox(); } @@ -1607,6 +1619,7 @@ void CreditsPrizeBox( void UserStarGiftBox( not_null box, not_null controller, + not_null owner, const Data::UserStarGift &data) { Settings::ReceiptCreditsBox( box, @@ -1618,7 +1631,9 @@ void UserStarGiftBox( .bareMsgId = uint64(data.messageId.bare), .barePeerId = data.fromId.value, .bareGiftStickerId = data.info.document->id, + .bareGiftOwnerId = owner->id.value, .stargiftId = data.info.id, + .uniqueGift = data.info.unique, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = data.info.limitedCount, .limitedLeft = data.info.limitedLeft, @@ -1650,6 +1665,7 @@ void StarGiftViewBox( .bareMsgId = uint64(item->id.bare), .barePeerId = item->history()->peer->id.value, .bareGiftStickerId = data.document ? data.document->id : 0, + .bareGiftOwnerId = item->history()->session().userPeerId().value, .stargiftId = data.stargiftId, .uniqueGift = data.unique, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, @@ -1663,6 +1679,7 @@ void StarGiftViewBox( .stargift = true, .savedToProfile = data.saved, .canUpgradeGift = data.upgradable, + .hasGiftComment = !data.message.empty(), .in = true, .gift = true, }; diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index a4df9f73b..3d042a082 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -100,6 +100,7 @@ void CreditsPrizeBox( void UserStarGiftBox( not_null box, not_null controller, + not_null owner, const Data::UserStarGift &data); void StarGiftViewBox( not_null box, diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index a8d480369..44d2cd554 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -120,7 +120,8 @@ giftBoxButtonBottom: 12px; giftBoxButtonPadding: margins(8px, 4px, 8px, 4px); giftBoxPreviewStickerPadding: margins(10px, 12px, 10px, 16px); giftBoxPreviewTitlePadding: margins(12px, 4px, 12px, 4px); -giftBoxPreviewTextPadding: margins(12px, 4px, 12px, 24px); +giftBoxPreviewTextPadding: margins(12px, 4px, 12px, 4px); +giftBoxButtonMargin: margins(12px, 8px, 12px, 12px); giftBoxStickerTop: 0px; giftBoxStickerStarTop: 24px; giftBoxStickerSize: size(80px, 80px); @@ -192,3 +193,6 @@ uniqueCloseButton: IconButton(boxTitleClose) { color: shadowFg; } } +upgradeGiftBox: Box(giftBox) { + buttonPadding: margins(22px, 3px, 22px, 22px); +}