diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d5c64f9e7..9632680e4 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2023,6 +2023,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_unique_received" = "{user} sent you a unique collectible item"; "lng_action_gift_sent" = "You sent a gift for {cost}"; "lng_action_gift_unique_sent" = "You sent a unique collectible item"; +"lng_action_gift_upgraded" = "{user} turned the gift from you to a unique collectible"; +"lng_action_gift_upgraded_mine" = "You turned the gift from {user} to a unique collectible"; "lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}"; "lng_action_gift_for_stars#one" = "{count} Star"; "lng_action_gift_for_stars#other" = "{count} Stars"; @@ -2440,6 +2442,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_button" = "Subscribe for {cost} per month"; "lng_premium_summary_new_badge" = "NEW"; +"lng_soon_badge" = "Soon"; "lng_premium_success" = "You've successfully subscribed to Telegram Premium!"; "lng_premium_unavailable" = "This feature requires subscription to **Telegram Premium**.\n\nUnfortunately, **Telegram Premium** is not available in your region."; @@ -3279,7 +3282,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_sell_small#one" = "sell for {count} Star"; "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 that you can transfer or auction."; +"lng_gift_upgrade_about" = "Turn your gift into a unique collectible\nthat you can transfer or auction."; "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"; diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index 11ded38e0..2bd58fc2f 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -94,7 +94,7 @@ constexpr auto kTransactionsLimit = 100; const auto parsedGift = stargift ? FromTL(&peer->session(), *stargift) : std::optional(); - const auto giftStickerId = parsedGift ? parsedGift->stickerId : 0; + const auto giftStickerId = parsedGift ? parsedGift->document->id : 0; return Data::CreditsHistoryEntry{ .id = qs(tl.data().vid()), .title = qs(tl.data().vtitle().value_or_empty()), diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 2e06915aa..772f46e98 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -774,58 +774,58 @@ std::optional FromTL( .id = uint64(data.vid().v), .stars = int64(data.vstars().v), .starsConverted = int64(data.vconvert_stars().v), - .stickerId = document->id, + .starsUpgraded = int64(data.vupgrade_stars().value_or_empty()), + .document = document, .limitedLeft = remaining.value_or_empty(), .limitedCount = total.value_or_empty(), .firstSaleDate = data.vfirst_sale_date().value_or_empty(), .lastSaleDate = data.vlast_sale_date().value_or_empty(), + .upgradable = data.vupgrade_stars().has_value(), .birthday = data.is_birthday(), }); }, [&](const MTPDstarGiftUnique &data) { const auto total = data.vavailability_total().v; + auto model = std::optional(); + auto pattern = std::optional(); + for (const auto &attribute : data.vattributes().v) { + attribute.match([&](const MTPDstarGiftAttributeModel &data) { + model = FromTL(session, data); + }, [&](const MTPDstarGiftAttributePattern &data) { + pattern = FromTL(session, data); + }, [&](const MTPDstarGiftAttributeBackdrop &data) { + }, [&](const MTPDstarGiftAttributeOriginalDetails &data) { + }); + } + if (!model + || !model->document->sticker() + || !pattern + || !pattern->document->sticker()) { + return std::optional(); + } auto result = Data::StarGift{ .id = uint64(data.vid().v), .unique = std::make_shared(Data::UniqueGift{ .title = qs(data.vtitle()), .number = data.vnum().v, .ownerId = peerFromUser(UserId(data.vowner_id().v)), + .model = *model, + .pattern = *pattern, }), + .document = model->document, .limitedLeft = (total - data.vavailability_issued().v), .limitedCount = total, }; const auto unique = result.unique.get(); for (const auto &attribute : data.vattributes().v) { attribute.match([&](const MTPDstarGiftAttributeModel &data) { - unique->model.name = qs(data.vname()); - unique->model.rarityPermille = data.vrarity_permille().v; - result.stickerId = data.vdocument_id().v; }, [&](const MTPDstarGiftAttributePattern &data) { - unique->pattern.name = qs(data.vname()); - unique->pattern.rarityPermille = data.vrarity_permille().v; - unique->pattern.documentId = data.vdocument_id().v; }, [&](const MTPDstarGiftAttributeBackdrop &data) { - unique->backdrop.name = qs(data.vname()); - unique->backdrop.rarityPermille = data.vrarity_permille().v; - unique->backdrop.centerColor = Ui::ColorFromSerialized( - data.vcenter_color()); - unique->backdrop.edgeColor = Ui::ColorFromSerialized( - data.vedge_color()); - unique->backdrop.patternColor = Ui::ColorFromSerialized( - data.vpattern_color()); - unique->backdrop.textColor = Ui::ColorFromSerialized( - data.vtext_color()); + unique->backdrop = FromTL(data); }, [&](const MTPDstarGiftAttributeOriginalDetails &data) { - unique->originalDetails.date = data.vdate().v; - unique->originalDetails.senderId = peerFromUser( - UserId(data.vsender_id().value_or_empty())); - unique->originalDetails.recipientId = peerFromUser( - UserId(data.vrecipient_id().v)); - unique->originalDetails.message = data.vmessage() - ? Api::ParseTextWithEntities(session, *data.vmessage()) - : TextWithEntities(); + unique->originalDetails = FromTL(session, data); }); } - return result.stickerId ? result : std::optional(); + return std::make_optional(result); }); } @@ -849,15 +849,70 @@ std::optional FromTL( } : TextWithEntities()), .starsConverted = int64(data.vconvert_stars().value_or_empty()), + .starsUpgraded = int64(data.vupgrade_stars().value_or_empty()), .fromId = (data.vfrom_id() ? peerFromUser(data.vfrom_id()->v) : PeerId()), .messageId = data.vmsg_id().value_or_empty(), .date = data.vdate().v, + .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), .hidden = data.is_unsaved(), .mine = to->isSelf(), }; } +Data::UniqueGiftModel FromTL( + not_null session, + const MTPDstarGiftAttributeModel &data) { + auto result = Data::UniqueGiftModel{ + .document = session->data().processDocument(data.vdocument()), + }; + result.name = qs(data.vname()); + result.rarityPermille = data.vrarity_permille().v; + return result; +} + +Data::UniqueGiftPattern FromTL( + not_null session, + const MTPDstarGiftAttributePattern &data) { + auto result = Data::UniqueGiftPattern{ + .document = session->data().processDocument(data.vdocument()), + }; + result.document->overrideEmojiUsesTextColor(true); + result.name = qs(data.vname()); + result.rarityPermille = data.vrarity_permille().v; + return result; +} + +Data::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) { + auto result = Data::UniqueGiftBackdrop(); + result.name = qs(data.vname()); + result.rarityPermille = data.vrarity_permille().v; + result.centerColor = Ui::ColorFromSerialized( + data.vcenter_color()); + result.edgeColor = Ui::ColorFromSerialized( + data.vedge_color()); + result.patternColor = Ui::ColorFromSerialized( + data.vpattern_color()); + result.textColor = Ui::ColorFromSerialized( + data.vtext_color()); + return result; +} + +Data::UniqueGiftOriginalDetails FromTL( + not_null session, + const MTPDstarGiftAttributeOriginalDetails &data) { + auto result = Data::UniqueGiftOriginalDetails(); + result.date = data.vdate().v; + result.senderId = peerFromUser( + UserId(data.vsender_id().value_or_empty())); + result.recipientId = peerFromUser( + UserId(data.vrecipient_id().v)); + result.message = data.vmessage() + ? ParseTextWithEntities(session, *data.vmessage()) + : TextWithEntities(); + return result; +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 252a32841..4617da755 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -263,4 +263,16 @@ enum class RequirePremiumState { not_null to, const MTPuserStarGift &gift); +[[nodiscard]] Data::UniqueGiftModel FromTL( + not_null session, + const MTPDstarGiftAttributeModel &data); +[[nodiscard]] Data::UniqueGiftPattern FromTL( + not_null session, + const MTPDstarGiftAttributePattern &data); +[[nodiscard]] Data::UniqueGiftBackdrop FromTL( + const MTPDstarGiftAttributeBackdrop &data); +[[nodiscard]] Data::UniqueGiftOriginalDetails FromTL( + not_null session, + const MTPDstarGiftAttributeOriginalDetails &data); + } // namespace Api diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 90e4b8649..ee98ff717 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -338,13 +338,58 @@ void AddTableRow( : 0; label->resizeToNaturalWidth(width - toggleSkip); label->moveToLeft(0, 0, width); - if (toggle) { - toggle->moveToLeft( - label->width() + st::normalFont->spacew, - (st::giveawayGiftCodeValue.style.font->ascent - - st::starGiftSmallButton.style.font->ascent), - width); - } + toggle->moveToLeft( + label->width() + st::normalFont->spacew, + (st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); + }, label->lifetime()); + + label->heightValue() | rpl::start_with_next([=](int height) { + raw->resize( + raw->width(), + height + st::giveawayGiftCodeValueMargin.bottom()); + }, raw->lifetime()); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + return result; +} + +[[nodiscard]] object_ptr MakeNonUniqueStatusTableValue( + not_null parent, + not_null controller, + Fn startUpgrade) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto label = Ui::CreateChild( + raw, + tr::lng_gift_unique_status_non(), + st::giveawayGiftCodeValue, + st::defaultPopupMenu); + + const auto upgrade = Ui::CreateChild( + raw, + tr::lng_gift_unique_status_upgrade(), + st::starGiftSmallButton); + upgrade->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + upgrade->setClickedCallback(startUpgrade); + + rpl::combine( + raw->widthValue(), + upgrade->widthValue() + ) | rpl::start_with_next([=](int width, int toggleWidth) { + const auto toggleSkip = toggleWidth + ? (st::normalFont->spacew + toggleWidth) + : 0; + label->resizeToNaturalWidth(width - toggleSkip); + label->moveToLeft(0, 0, width); + upgrade->moveToLeft( + label->width() + st::normalFont->spacew, + (st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); }, label->lifetime()); label->heightValue() | rpl::start_with_next([=](int height) { @@ -1092,7 +1137,8 @@ void AddStarGiftTable( not_null container, const Data::CreditsHistoryEntry &entry, Fn toggleVisibility, - Fn convertToStars) { + Fn convertToStars, + Fn startUpgrade) { auto table = container->add( object_ptr( container, @@ -1178,6 +1224,16 @@ void AddStarGiftTable( std::move(amount), Ui::Text::WithEntities))); } + if (!entry.uniqueGift && startUpgrade) { + AddTableRow( + table, + tr::lng_gift_unique_status(), + MakeNonUniqueStatusTableValue( + table, + controller, + std::move(startUpgrade)), + marginWithButton); + } if (!entry.description.empty()) { const auto makeContext = [=](Fn update) { return Core::MarkedTextContext{ diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index 3974d85ab..d29c1fca4 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -59,7 +59,8 @@ void AddStarGiftTable( not_null container, const Data::CreditsHistoryEntry &entry, Fn toggleVisibility, - Fn convertToStars); + Fn convertToStars, + Fn startUpgrade); void AddCreditsHistoryEntryTable( not_null controller, not_null container, diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index e42151d75..204afadac 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -463,7 +463,7 @@ void SendCreditsBox( }), session, st::creditsBoxButtonLabel, - box->getDelegate()->style().button.textFg->c); + &box->getDelegate()->style().button.textFg); const auto buttonWidth = st::boxWidth - rect::m::sum::h(stBox.buttonPadding); @@ -524,7 +524,7 @@ not_null SetButtonMarkedLabel( rpl::producer text, Fn update)> context, const style::FlatLabel &st, - std::optional textFg) { + const style::color *textFg) { const auto buttonLabel = Ui::CreateChild( button, rpl::single(QString()), @@ -539,7 +539,10 @@ not_null SetButtonMarkedLabel( context([=] { buttonLabel->update(); })); }, buttonLabel->lifetime()); if (textFg) { - buttonLabel->setTextColorOverride(textFg); + buttonLabel->setTextColorOverride((*textFg)->c); + style::PaletteChanged() | rpl::start_with_next([=] { + buttonLabel->setTextColorOverride((*textFg)->c); + }, buttonLabel->lifetime()); } button->sizeValue( ) | rpl::start_with_next([=](const QSize &size) { @@ -561,7 +564,7 @@ not_null SetButtonMarkedLabel( rpl::producer text, not_null session, const style::FlatLabel &st, - std::optional textFg) { + const style::color *textFg) { return SetButtonMarkedLabel(button, text, [=](Fn update) { return Core::MarkedTextContext{ .session = session, diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h index 08188ae22..cc84b379e 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.h +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -43,14 +43,14 @@ not_null SetButtonMarkedLabel( rpl::producer text, Fn update)> context, const style::FlatLabel &st, - std::optional textFg = {}); + const style::color *textFg = nullptr); not_null SetButtonMarkedLabel( not_null button, rpl::producer text, not_null session, const style::FlatLabel &st, - std::optional textFg = {}); + const style::color *textFg = nullptr); void SendStarGift( not_null session, diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index b4a7cb2d3..000c4749f 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -7,8 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/star_gift_box.h" +#include "apiwrap.h" #include "base/event_filter.h" #include "base/random.h" +#include "base/timer_rpl.h" #include "base/unixtime.h" #include "api/api_premium.h" #include "boxes/peer_list_controllers.h" @@ -23,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_credits.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_file_origin.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" @@ -33,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_helpers.h" #include "info/peer_gifts/info_peer_gifts_common.h" +#include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "lottie/lottie_common.h" #include "lottie/lottie_single_player.h" @@ -52,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/layers/generic_box.h" +#include "ui/new_badges.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/format_values.h" @@ -61,6 +66,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/shadow.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_session_controller.h" @@ -69,6 +75,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_premium.h" #include "styles/style_settings.h" @@ -82,6 +89,8 @@ constexpr auto kPriceTabLimited = -1; constexpr auto kPriceTabInStock = -2; constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); +constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); +constexpr auto kCrossfadeDuration = crl::time(400); using namespace HistoryView; using namespace Info::PeerGifts; @@ -107,20 +116,20 @@ class PreviewDelegate final : public DefaultElementDelegate { public: PreviewDelegate( not_null parent, - not_null st, + not_null st, Fn update); bool elementAnimationsPaused() override; - not_null elementPathShiftGradient() override; + not_null elementPathShiftGradient() override; Context elementContext() override; private: const not_null _parent; - const std::unique_ptr _pathGradient; + const std::unique_ptr _pathGradient; }; -class PreviewWrap final : public Ui::RpWidget { +class PreviewWrap final : public RpWidget { public: PreviewWrap( not_null parent, @@ -135,11 +144,10 @@ private: void prepare(rpl::producer details); const not_null _history; - const std::unique_ptr _theme; - const std::unique_ptr _style; + const std::unique_ptr _theme; + const std::unique_ptr _style; const std::unique_ptr _delegate; AdminLog::OwnedItem _item; - rpl::lifetime _itemLifetime; QPoint _position; }; @@ -167,7 +175,7 @@ private: PreviewDelegate::PreviewDelegate( not_null parent, - not_null st, + not_null st, Fn update) : _parent(parent) , _pathGradient(MakePathShiftGradient(st, update)) { @@ -178,7 +186,7 @@ bool PreviewDelegate::elementAnimationsPaused() { } auto PreviewDelegate::elementPathShiftGradient() --> not_null { +-> not_null { return _pathGradient.get(); } @@ -189,9 +197,7 @@ Context PreviewDelegate::elementContext() { auto GenerateGiftMedia( not_null parent, Element *replacing, - const GiftDetails &data, - Fn requestResize, - not_null onLifetime) + const GiftDetails &data) -> Fn)>)> { return [=](Fn)> push) { const auto &descriptor = data.descriptor; @@ -210,19 +216,12 @@ auto GenerateGiftMedia( context)); }; - const auto resolved = onLifetime->make_state(nullptr); - GiftStickerValue( - &parent->history()->session(), - descriptor - ) | rpl::start_with_next([=](not_null document) { - *resolved = document; - requestResize(); - }, *onLifetime); - const auto sticker = [=] { using Tag = ChatHelpers::StickerLottieSize; + const auto session = &parent->history()->session(); + const auto sticker = LookupGiftSticker(session, descriptor); return StickerInBubblePart::Data{ - .sticker = *resolved, + .sticker = sticker, .size = st::chatIntroStickerSize, .cacheTag = Tag::ChatIntroHelloSticker, .singleTimePlayback = v::is(descriptor), @@ -247,18 +246,18 @@ auto GenerateGiftMedia( auto textFallback = v::match(descriptor, [&](GiftTypePremium gift) { return tr::lng_action_gift_premium_about( tr::now, - Ui::Text::RichLangValue); + Text::RichLangValue); }, [&](const GiftTypeStars &gift) { return tr::lng_action_gift_got_stars_text( tr::now, lt_count, gift.info.starsConverted, - Ui::Text::RichLangValue); + Text::RichLangValue); }); auto description = data.text.empty() ? std::move(textFallback) : data.text; - pushText(Ui::Text::Bold(title), st::giftBoxPreviewTitlePadding); + pushText(Text::Bold(title), st::giftBoxPreviewTitlePadding); pushText( std::move(description), st::giftBoxPreviewTextPadding, @@ -270,6 +269,103 @@ auto GenerateGiftMedia( }; } +struct PatternPoint { + QPointF position; + float64 scale = 1.; + float64 opacity = 1.; +}; +[[nodiscard]] const std::vector &PatternPoints() { + static const auto kSmall = 0.7; + static const auto kFaded = 0.7; + static const auto kLarge = 0.85; + static const auto kOpaque = 0.9; + static const auto result = std::vector{ + { { 0.5, 0.066 }, kSmall, kFaded }, + + { { 0.177, 0.168 }, kSmall, kFaded }, + { { 0.822, 0.168 }, kSmall, kFaded }, + + { { 0.37, 0.168 }, kLarge, kOpaque }, + { { 0.63, 0.168 }, kLarge, kOpaque }, + + { { 0.277, 0.308 }, kSmall, kOpaque }, + { { 0.723, 0.308 }, kSmall, kOpaque }, + + { { 0.13, 0.42 }, kSmall, kFaded }, + { { 0.87, 0.42 }, kSmall, kFaded }, + + { { 0.27, 0.533 }, kLarge, kOpaque }, + { { 0.73, 0.533 }, kLarge, kOpaque }, + + { { 0.2, 0.73 }, kSmall, kFaded }, + { { 0.8, 0.73 }, kSmall, kFaded }, + + { { 0.302, 0.825 }, kLarge, kOpaque }, + { { 0.698, 0.825 }, kLarge, kOpaque }, + + { { 0.5, 0.876 }, kLarge, kFaded }, + + { { 0.144, 0.936 }, kSmall, kFaded }, + { { 0.856, 0.936 }, kSmall, kFaded }, + }; + return result; +} + +[[nodiscard]] QImage CreateGradient( + QSize size, + const Data::UniqueGift &gift) { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + auto gradient = QRadialGradient( + QRect(QPoint(), size).center(), + size.height() / 2); + gradient.setStops({ + { 0., gift.backdrop.centerColor }, + { 1., gift.backdrop.edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRect(QRect(QPoint(), size)); + p.end(); + + const auto mask = Images::CornersMask(st::boxRadius); + return Images::Round(std::move(result), mask, RectPart::FullTop); +} + +void PrepareImage( + QImage &image, + not_null emoji, + const PatternPoint &point, + const Data::UniqueGift &gift) { + if (!image.isNull() || !emoji->ready()) { + return; + } + const auto ratio = style::DevicePixelRatio(); + const auto size = Emoji::GetSizeNormal() / ratio; + image = QImage( + 2 * QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(ratio); + image.fill(Qt::transparent); + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + p.setOpacity(point.opacity); + if (point.scale < 1.) { + p.translate(size, size); + p.scale(point.scale, point.scale); + p.translate(-size, -size); + } + const auto shift = (2 * size - (Emoji::GetSizeLarge() / ratio)) / 2; + emoji->paint(p, { + .textColor = gift.backdrop.patternColor, + .position = QPoint(shift, shift), + }); +} + PreviewWrap::PreviewWrap( not_null parent, not_null session, @@ -277,7 +373,7 @@ PreviewWrap::PreviewWrap( : RpWidget(parent) , _history(session->data().history(session->userPeerId())) , _theme(Window::Theme::DefaultChatThemeOn(lifetime())) -, _style(std::make_unique( +, _style(std::make_unique( _history->session().colorIndicesValue())) , _delegate(std::make_unique( parent, @@ -307,21 +403,20 @@ void ShowSentToast( const auto &st = st::historyPremiumToast; const auto skip = st.padding.top(); const auto size = st.style.font->height * 2; - const auto stickerId = GiftStickerId(&window->session(), descriptor); - const auto stickerSize = skip + size + skip; - const auto leftSkip = stickerId - ? (stickerSize - st.padding.left()) + const auto document = LookupGiftSticker(&window->session(), descriptor); + const auto leftSkip = document + ? (skip + size + skip - st.padding.left()) : 0; auto text = v::match(descriptor, [&](const GiftTypePremium &gift) { return tr::lng_action_gift_premium_about( tr::now, - Ui::Text::RichLangValue); + Text::RichLangValue); }, [&](const GiftTypeStars &gift) { return tr::lng_gift_sent_about( tr::now, lt_count, gift.info.stars, - Ui::Text::RichLangValue); + Text::RichLangValue); }); const auto strong = window->showToast({ .title = tr::lng_gift_sent_title(tr::now), @@ -331,31 +426,40 @@ void ShowSentToast( .attach = RectPart::Top, .duration = kSentToastDuration, }).get(); - if (!strong || !stickerId) { + if (!strong || !document) { return; } const auto widget = strong->widget(); - const auto preview = Ui::CreateChild(widget.get()); - preview->moveToLeft(0, 0); - preview->resize(stickerSize, stickerSize); + const auto preview = CreateChild(widget.get()); + preview->moveToLeft(skip, skip); + preview->resize(size, size); preview->show(); - const auto tag = Data::CustomEmojiManager::SizeTag::Isolated; - const auto manager = &window->session().data().customEmojiManager(); - const auto emoji = std::shared_ptr( - manager->create(stickerId, [=] { preview->update(); }, tag)); + const auto bytes = document->createMediaView()->bytes(); + const auto filepath = document->filepath(); + const auto ratio = style::DevicePixelRatio(); + const auto player = preview->lifetime().make_state( + Lottie::ReadContent(bytes, filepath), + Lottie::FrameRequest{ QSize(size, size) * ratio }, + Lottie::Quality::Default); preview->paintRequest( ) | rpl::start_with_next([=] { - auto p = Painter(preview); - const auto frame = Data::FrameSizeFromTag(tag) - / style::DevicePixelRatio(); - const auto delta = (stickerSize - frame) / 2; - emoji->paint(p, { - .textColor = st::toastFg->c, - .now = crl::now(), - .position = QPoint(delta, delta), - }); + if (!player->ready()) { + return; + } + const auto image = player->frame(); + QPainter(preview).drawImage( + QRect(QPoint(), image.size() / ratio), + image); + if (player->frameIndex() + 1 != player->framesCount()) { + player->markFrameShown(); + } + }, preview->lifetime()); + + player->updates( + ) | rpl::start_with_next([=] { + preview->update(); }, preview->lifetime()); } @@ -365,8 +469,6 @@ PreviewWrap::~PreviewWrap() { void PreviewWrap::prepare(rpl::producer details) { std::move(details) | rpl::start_with_next([=](GiftDetails details) { - _itemLifetime.destroy(); - const auto &descriptor = details.descriptor; const auto cost = v::match(descriptor, [&](GiftTypePremium data) { return FillAmountAndCurrency(data.cost, data.currency, true); @@ -396,12 +498,7 @@ void PreviewWrap::prepare(rpl::producer details) { auto owned = AdminLog::OwnedItem(_delegate.get(), item); owned->overrideMedia(std::make_unique( owned.get(), - GenerateGiftMedia( - owned.get(), - _item.get(), - details, - [=] { item->history()->owner().requestItemResize(item); }, - &_itemLifetime), + GenerateGiftMedia(owned.get(), _item.get(), details), MediaGenericDescriptor{ .maxWidth = st::chatIntroWidth, .service = true, @@ -786,7 +883,7 @@ struct GiftPriceTabs { case QEvent::Wheel: { const auto me = static_cast(e.get()); state->scroll = std::clamp( - state->scroll - Ui::ScrollDeltaF(me).x(), + state->scroll - ScrollDeltaF(me).x(), 0., state->scrollMax * 1.); raw->update(); @@ -857,25 +954,25 @@ struct GiftPriceTabs { 255); } -[[nodiscard]] not_null AddPartInput( +[[nodiscard]] not_null AddPartInput( not_null controller, - not_null container, + not_null container, not_null outer, rpl::producer placeholder, QString current, int limit) { const auto field = container->add( - object_ptr( + object_ptr( container, st::giftBoxTextField, - Ui::InputField::Mode::NoNewlines, + InputField::Mode::NoNewlines, std::move(placeholder), current), st::giftBoxTextPadding); field->setMaxLength(limit); - Ui::AddLengthLimitLabel(field, limit, std::nullopt, st::giftBoxLimitTop); + AddLengthLimitLabel(field, limit, std::nullopt, st::giftBoxLimitTop); - const auto toggle = Ui::CreateChild( + const auto toggle = CreateChild( container, st::defaultComposeFiles.emoji); toggle->show(); @@ -902,7 +999,7 @@ struct GiftPriceTabs { panel->selector()->setAllowEmojiWithoutPremium(true); panel->selector()->emojiChosen( ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { - Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji); + InsertEmojiAtCursor(field->textCursor(), data.emoji); }, field->lifetime()); panel->selector()->customEmojiChosen( ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { @@ -968,8 +1065,157 @@ void SendGift( }); } +[[nodiscard]] std::shared_ptr FindUniqueGift( + not_null session, + const MTPUpdates &updates) { + auto result = std::shared_ptr(); + const auto checkAction = [&](const MTPMessageAction &action) { + action.match([&](const MTPDmessageActionStarGiftUnique &data) { + if (const auto gift = Api::FromTL(session, data.vgift())) { + result = gift->unique; + } + }, [](const auto &) {}); + }; + updates.match([&](const MTPDupdates &data) { + for (const auto &update : data.vupdates().v) { + update.match([&](const MTPDupdateNewMessage &data) { + data.vmessage().match([&](const MTPDmessageService &data) { + checkAction(data.vaction()); + }, [](const auto &) {}); + }, [](const auto &) {}); + } + }, [](const auto &) {}); + return result; +} + +[[nodiscard]] QString ComputeTitle(const Data::UniqueGift &gift) { + return gift.title + u" #"_q + QString::number(gift.number); +} + +void SendUpgradeRequest( + not_null controller, + Settings::SmallBalanceResult result, + uint64 formId, + int stars, + MTPInputInvoice invoice, + Fn done) { + using BalanceResult = Settings::SmallBalanceResult; + const auto session = &controller->session(); + if (result == BalanceResult::Success + || result == BalanceResult::Already) { + const auto weak = base::make_weak(controller); + session->api().request(MTPpayments_SendStarsForm( + MTP_long(formId), + invoice + )).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(ComputeTitle(*gift)), + Ui::Text::WithEntities), + }); + } + }, [](const MTPDpayments_paymentVerificationNeeded &data) { + }); + done(Payments::CheckoutResult::Paid); + }).fail([=](const MTP::Error &error) { + if (const auto strong = weak.get()) { + strong->showToast(error.type()); + } + done(Payments::CheckoutResult::Failed); + }).send(); + } else if (result == BalanceResult::Cancelled) { + done(Payments::CheckoutResult::Cancelled); + } else { + done(Payments::CheckoutResult::Failed); + } +} + +void UpgradeGift( + not_null window, + MsgId messageId, + bool keepDetails, + int stars, + Fn done) { + const auto session = &window->session(); + if (stars <= 0) { + using Flag = MTPpayments_UpgradeStarGift::Flag; + const auto weak = base::make_weak(window); + session->api().request(MTPpayments_UpgradeStarGift( + MTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()), + 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(ComputeTitle(*gift)), + Ui::Text::WithEntities), + }); + } + }).fail([=](const MTP::Error &error) { + if (const auto strong = weak.get()) { + strong->showToast(error.type()); + } + done(Payments::CheckoutResult::Failed); + }).send(); + return; + } + using Flag = MTPDinputInvoiceStarGiftUpgrade::Flag; + const auto weak = base::make_weak(window); + const auto invoice = MTP_inputInvoiceStarGiftUpgrade( + MTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()), + MTP_int(messageId.bare)); + session->api().request(MTPpayments_GetPaymentForm( + MTP_flags(0), + invoice, + MTPDataJSON() // theme_params + )).done([=](const MTPpayments_PaymentForm &result) { + result.match([&](const MTPDpayments_paymentFormStarGift &data) { + const auto formId = data.vform_id().v; + const auto prices = data.vinvoice().data().vprices().v; + const auto strong = weak.get(); + if (!strong) { + done(Payments::CheckoutResult::Failed); + return; + } + const auto ready = [=](Settings::SmallBalanceResult result) { + SendUpgradeRequest( + strong, + result, + formId, + stars, + invoice, + done); + }; + Settings::MaybeRequestBalanceIncrease( + Main::MakeSessionShow(strong->uiShow(), session), + prices.front().data().vamount().v, + Settings::SmallBalanceDeepLink{}, + ready); + }, [&](const auto &) { + done(Payments::CheckoutResult::Failed); + }); + }).fail([=](const MTP::Error &error) { + if (const auto strong = weak.get()) { + strong->showToast(error.type()); + } + done(Payments::CheckoutResult::Failed); + }).send(); +} + void SoldOutBox( - not_null box, + not_null box, not_null window, const GiftTypeStars &gift) { Settings::ReceiptCreditsBox( @@ -979,7 +1225,7 @@ void SoldOutBox( .firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate), .lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate), .credits = StarsAmount(gift.info.stars), - .bareGiftStickerId = gift.info.stickerId, + .bareGiftStickerId = gift.info.document->id, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = gift.info.limitedCount, .limitedLeft = gift.info.limitedLeft, @@ -990,7 +1236,7 @@ void SoldOutBox( } void SendGiftBox( - not_null box, + not_null box, not_null window, not_null peer, std::shared_ptr api, @@ -1005,15 +1251,15 @@ void SendGiftBox( const auto session = &window->session(); auto cost = rpl::single([&] { return v::match(descriptor, [&](const GiftTypePremium &data) { - if (data.currency == Ui::kCreditsCurrency) { - return Ui::CreditsEmojiSmall(session).append( + 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 Ui::CreditsEmojiSmall(session).append( + return CreditsEmojiSmall(session).append( Lang::FormatCountDecimal(std::abs(data.info.stars))); }); }()); @@ -1028,6 +1274,10 @@ void SendGiftBox( .descriptor = descriptor, .randomId = base::RandomValue(), }; + const auto document = LookupGiftSticker(&window->session(), descriptor); + if ((state->media = document ? document->createMediaView() : nullptr)) { + state->media->checkStickerLarge(); + } const auto container = box->verticalLayout(); container->add(object_ptr( @@ -1070,14 +1320,14 @@ void SendGiftBox( }, .allowPremiumEmoji = allow, .allowMarkdownTags = { - Ui::InputField::kTagBold, - Ui::InputField::kTagItalic, - Ui::InputField::kTagUnderline, - Ui::InputField::kTagStrikeOut, - Ui::InputField::kTagSpoiler, + InputField::kTagBold, + InputField::kTagItalic, + InputField::kTagUnderline, + InputField::kTagStrikeOut, + InputField::kTagSpoiler, } }); - Ui::Emoji::SuggestionsController::Init( + Emoji::SuggestionsController::Init( box->getDelegate()->outerContainer(), text, &window->session(), @@ -1087,7 +1337,7 @@ void SendGiftBox( AddDivider(container); AddSkip(container); container->add( - object_ptr( + object_ptr( container, tr::lng_gift_send_anonymous(), st::settingsButtonNoIcon) @@ -1120,7 +1370,7 @@ void SendGiftBox( } state->submitting = true; const auto details = state->details.current(); - const auto weak = Ui::MakeWeak(box); + const auto weak = MakeWeak(box); const auto done = [=](Payments::CheckoutResult result) { if (result == Payments::CheckoutResult::Paid) { const auto copy = state->media; @@ -1138,10 +1388,10 @@ void SendGiftBox( tr::lng_gift_send_button( lt_cost, std::move(cost), - Ui::Text::WithEntities), + Text::WithEntities), session, st::creditsBoxButtonLabel, - st::giftBox.button.textFg->c); + &st::giftBox.button.textFg); button->resizeToWidth(buttonWidth); button->widthValue() | rpl::start_with_next([=](int width) { if (width != buttonWidth) { @@ -1370,7 +1620,7 @@ void GiftBox( Settings::AddMiniStars( content, - Ui::CreateChild(content), + CreateChild(content), stUser.photoSize, box->width(), 2.); @@ -1463,4 +1713,428 @@ void ShowStarGiftBox( controller->show(Box(GiftBox, controller, peer)); } +void AddUniqueGiftCover( + not_null container, + rpl::producer data, + rpl::producer subtitleOverride) { + const auto cover = container->add(object_ptr(container)); + + const auto title = CreateChild( + cover, + tr::lng_gift_upgrade_title(tr::now), + st::uniqueGiftTitle); + title->setTextColorOverride(QColor(255, 255, 255)); + auto subtitleText = subtitleOverride + ? std::move(subtitleOverride) + : rpl::duplicate(data) | rpl::map([](const Data::UniqueGift &gift) { + return tr::lng_gift_unique_number( + tr::now, + lt_index, + QString::number(gift.number)); + }); + const auto subtitle = CreateChild( + cover, + std::move(subtitleText), + st::uniqueGiftSubtitle); + + struct GiftView { + QImage gradient; + std::optional gift; + std::shared_ptr media; + std::unique_ptr lottie; + std::unique_ptr emoji; + base::flat_map emojis; + rpl::lifetime lifetime; + }; + struct State { + GiftView now; + GiftView next; + Animations::Simple crossfade; + bool animating = false; + }; + const auto state = cover->lifetime().make_state(); + const auto lottieSize = st::creditsHistoryEntryStarGiftSize; + const auto updateColors = [=](float64 progress) { + subtitle->setTextColorOverride((progress == 0.) + ? state->now.gift->backdrop.textColor + : (progress == 1.) + ? state->next.gift->backdrop.textColor + : anim::color( + state->now.gift->backdrop.textColor, + state->next.gift->backdrop.textColor, + progress)); + }; + std::move( + data + ) | rpl::start_with_next([=](const Data::UniqueGift &gift) { + const auto setup = [&](GiftView &to) { + to.gift = gift; + const auto document = gift.model.document; + to.media = document->createMediaView(); + to.media->automaticLoad({}, nullptr); + rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([&to] { + return to.media->loaded(); + }) | rpl::start_with_next([=, &to] { + const auto lottieSize = st::creditsHistoryEntryStarGiftSize; + to.lottie = ChatHelpers::LottiePlayerFromDocument( + to.media.get(), + ChatHelpers::StickerLottieSize::MessageHistory, + QSize(lottieSize, lottieSize), + Lottie::Quality::High); + + to.lifetime.destroy(); + const auto lottie = to.lottie.get(); + lottie->updates() | rpl::start_with_next([=] { + if (state->now.lottie.get() == lottie + || state->crossfade.animating()) { + cover->update(); + } + }, to.lifetime); + }, to.lifetime); + to.emoji = document->owner().customEmojiManager().create( + gift.pattern.document, + [=] { cover->update(); }, + Data::CustomEmojiSizeTag::Large); + [[maybe_unused]] const auto preload = to.emoji->ready(); + }; + + if (!state->now.gift) { + setup(state->now); + cover->update(); + updateColors(0.); + } else if (!state->next.gift) { + setup(state->next); + } + }, cover->lifetime()); + + cover->widthValue() | rpl::start_with_next([=](int width) { + const auto skip = st::uniqueGiftBottom; + if (width <= 3 * skip) { + return; + } + const auto available = width - 2 * skip; + title->resizeToWidth(available); + title->moveToLeft(skip, st::uniqueGiftTitleTop); + + subtitle->resizeToWidth(available); + subtitle->moveToLeft(skip, st::uniqueGiftSubtitleTop); + + cover->resize(width, subtitle->y() + subtitle->height() + skip); + }, cover->lifetime()); + + cover->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(cover); + + auto progress = state->crossfade.value(state->animating ? 1. : 0.); + if (state->animating) { + updateColors(progress); + } + if (progress == 1.) { + state->animating = false; + state->now = base::take(state->next); + progress = 0.; + } + const auto paint = [&](GiftView &gift, float64 shown) { + Expects(gift.gift.has_value()); + + const auto width = cover->width(); + const auto pointsHeight = st::uniqueGiftSubtitleTop; + const auto ratio = style::DevicePixelRatio(); + if (gift.gradient.size() != cover->size() * ratio) { + gift.gradient = CreateGradient(cover->size(), *gift.gift); + } + p.drawImage(0, 0, gift.gradient); + const auto paintPoint = [&](const PatternPoint &point) { + const auto key = (1. + point.opacity) * 10. + point.scale; + auto &image = gift.emojis[key]; + PrepareImage(image, gift.emoji.get(), point, *gift.gift); + if (!image.isNull()) { + const auto x = int(point.position.x() * width); + const auto y = int(point.position.y() * pointsHeight); + if (shown < 1.) { + p.save(); + p.translate(x, y); + p.scale(shown, shown); + p.translate(-x, -y); + } + const auto size = image.size() / ratio; + p.drawImage( + x - size.width() / 2, + y - size.height() / 2, + image); + if (shown < 1.) { + p.restore(); + } + } + }; + for (const auto point : PatternPoints()) { + paintPoint(point); + } + + const auto lottie = gift.lottie.get(); + const auto factor = style::DevicePixelRatio(); + const auto request = Lottie::FrameRequest{ + .box = Size(lottieSize) * factor, + }; + const auto frame = (lottie && lottie->ready()) + ? lottie->frameInfo(request) + : Lottie::Animation::FrameInfo(); + if (frame.image.isNull()) { + return false; + } + const auto size = frame.image.size() / factor; + const auto left = (width - size.width()) / 2; + p.drawImage( + QRect(QPoint(left, st::uniqueGiftModelTop), size), + frame.image); + const auto count = lottie->framesCount(); + const auto finished = lottie->frameIndex() == (count - 1); + lottie->markFrameShown(); + return finished; + }; + + if (progress < 1.) { + const auto finished = paint(state->now, 1. - progress); + const auto next = finished ? state->next.lottie.get() : nullptr; + if (next && next->ready()) { + state->animating = true; + state->crossfade.start([=] { + cover->update(); + }, 0., 1., kCrossfadeDuration); + } + } + if (progress > 0.) { + p.setOpacity(progress); + paint(state->next, progress); + } + }, cover->lifetime()); +} + +struct UpgradeArgs { + std::vector models; + std::vector patterns; + std::vector backdrops; + not_null user; + MsgId itemId = 0; + int stars = 0; +}; + +[[nodiscard]] rpl::producer MakeUpgradeGiftStream( + const UpgradeArgs &args) { + if (args.models.empty() + || args.patterns.empty() + || args.backdrops.empty()) { + return rpl::never(); + } + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + UpgradeArgs data; + std::vector modelIndices; + std::vector patternIndices; + std::vector backdropIndices; + }; + const auto state = lifetime.make_state(State{ + .data = args, + }); + + const auto put = [=] { + const auto index = [](std::vector &indices, const auto &v) { + if (indices.empty()) { + indices = ranges::views::ints(0) | ranges::views::take( + v.size() + ) | ranges::to_vector; + } + 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), + .model = models[index(state->modelIndices, models)], + .pattern = patterns[index(state->patternIndices, patterns)], + .backdrop = backdrops[index(state->backdropIndices, backdrops)], + }); + }; + + put(); + base::timer_each( + kSwitchUpgradeCoverInterval / 3 + ) | rpl::start_with_next(put, lifetime); + + return lifetime; + }; +} + +void AddUpgradeGiftCover( + not_null container, + const UpgradeArgs &args) { + AddUniqueGiftCover( + container, + MakeUpgradeGiftStream(args), + tr::lng_gift_upgrade_about()); +} + +void UpgradeBox( + not_null box, + not_null controller, + UpgradeArgs &&args) { + box->setNoContentMargin(true); + + const auto container = box->verticalLayout(); + AddUpgradeGiftCover(container, args); + + AddSkip(container, st::defaultVerticalListSkip * 2); + + const auto infoRow = [&]( + rpl::producer title, + rpl::producer text, + not_null icon, + bool newBadge = false) { + auto raw = container->add( + object_ptr(container)); + const auto widget = raw->add( + object_ptr( + raw, + std::move(title) | Ui::Text::ToBold(), + st::defaultFlatLabel), + st::settingsPremiumRowTitlePadding); + if (newBadge) { + const auto badge = NewBadge::CreateNewBadge( + raw, + tr::lng_soon_badge(Ui::Text::Upper)); + widget->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + badge->move(st::settingsPremiumNewBadgePosition + + QPoint(widget->x() + widget->width(), widget->y())); + }, badge->lifetime()); + } + raw->add( + object_ptr( + raw, + std::move(text), + st::boxDividerLabel), + st::settingsPremiumRowAboutPadding); + object_ptr( + raw, + *icon, + st::starrefInfoIconPosition); + }; + + infoRow( + tr::lng_gift_upgrade_unique_title(), + tr::lng_gift_upgrade_unique_about(), + &st::menuIconReplace); + infoRow( + tr::lng_gift_upgrade_transferable_title(), + tr::lng_gift_upgrade_transferable_about(), + &st::menuIconReplace); + infoRow( + tr::lng_gift_upgrade_tradable_title(), + tr::lng_gift_upgrade_tradable_about(), + &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; + }; + 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) { + return; + } + state->sent = true; + const auto keepDetails = true; + 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(); + } + }; + UpgradeGift(controller, args.itemId, keepDetails, stars, 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); + rpl::combine( + box->widthValue(), + button->widthValue() + ) | rpl::start_with_next([=](int outer, int inner) { + const auto padding = st::giftBox.buttonPadding; + const auto wanted = outer - padding.left() - padding.right(); + if (inner != wanted) { + button->resizeToWidth(wanted); + button->moveToLeft(padding.left(), padding.top()); + } + }, box->lifetime()); +} + +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) + )).done([=](const MTPpayments_StarGiftUpgradePreview &result) { + const auto strong = weak.get(); + if (!strong) { + ready(false); + return; + } + const auto &data = result.data(); + const auto session = &user->session(); + auto args = UpgradeArgs{ + .user = user, + .itemId = itemId, + .stars = stars, + }; + for (const auto &attribute : data.vsample_attributes().v) { + attribute.match([&](const MTPDstarGiftAttributeModel &data) { + args.models.push_back(Api::FromTL(session, data)); + }, [&](const MTPDstarGiftAttributePattern &data) { + args.patterns.push_back(Api::FromTL(session, data)); + }, [&](const MTPDstarGiftAttributeBackdrop &data) { + args.backdrops.push_back(Api::FromTL(data)); + }, [](const auto &) {}); + } + controller->show(Box(UpgradeBox, controller, std::move(args))); + ready(true); + }).fail([=](const MTP::Error &error) { + if (const auto strong = weak.get()) { + strong->showToast(error.type()); + } + ready(false); + }).send(); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index e1ae249a3..a3b30112b 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -7,12 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace Data { +struct UniqueGift; +} // namespace Data + namespace Window { class SessionController; } // namespace Window namespace Ui { +class VerticalLayout; + void ChooseStarGiftRecipient( not_null controller); @@ -20,4 +26,17 @@ void ShowStarGiftBox( not_null controller, not_null peer); +void AddUniqueGiftCover( + not_null container, + rpl::producer data, + rpl::producer subtitleOverride = nullptr); + +void ShowStarGiftUpgradeBox( + not_null controller, + uint64 stargiftId, + not_null user, + MsgId itemId, + int stars, + Fn ready); + } // namespace Ui diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 2e3ed7ab8..ccf1a86ca 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -65,6 +65,7 @@ struct CreditsHistoryEntry final { uint64 bareGiveawayMsgId = 0; uint64 bareGiftStickerId = 0; uint64 bareActorId = 0; + uint64 stargiftId = 0; std::shared_ptr uniqueGift; StarsAmount starrefAmount; int starrefCommission = 0; @@ -76,6 +77,7 @@ struct CreditsHistoryEntry final { int limitedCount = 0; int limitedLeft = 0; int starsConverted = 0; + int starsUpgraded = 0; int floodSkip = 0; bool converted : 1 = false; bool anonymous : 1 = false; @@ -83,6 +85,7 @@ struct CreditsHistoryEntry final { bool savedToProfile : 1 = false; bool fromGiftsList : 1 = false; bool soldOutInfo : 1 = false; + bool canUpgradeGift : 1 = false; bool reaction : 1 = false; bool refunded : 1 = false; bool pending : 1 = false; diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 0d9d91a6a..cd2100437 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -136,20 +136,24 @@ enum class GiftType : uchar { struct GiftCode { QString slug; - DocumentId stickerId = 0; + uint64 stargiftId = 0; + DocumentData *document = nullptr; std::shared_ptr unique; TextWithEntities message; ChannelData *channel = nullptr; MsgId giveawayMsgId = 0; int starsConverted = 0; + int starsUpgraded = 0; int limitedCount = 0; int limitedLeft = 0; int count = 0; GiftType type = GiftType::Premium; bool viaGiveaway : 1 = false; + bool upgradable : 1 = false; bool unclaimed : 1 = false; bool anonymous : 1 = false; bool converted : 1 = false; + bool upgraded : 1 = false; bool saved : 1 = false; }; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index fe580ed28..3619788a9 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -15,10 +15,11 @@ struct UniqueGiftAttribute { }; struct UniqueGiftModel : UniqueGiftAttribute { + not_null document; }; struct UniqueGiftPattern : UniqueGiftAttribute { - DocumentId documentId = 0; + not_null document; }; struct UniqueGiftBackdrop : UniqueGiftAttribute { @@ -50,11 +51,13 @@ struct StarGift { std::shared_ptr unique; int64 stars = 0; int64 starsConverted = 0; - DocumentId stickerId = 0; + int64 starsUpgraded = 0; + not_null document; int limitedLeft = 0; int limitedCount = 0; TimeId firstSaleDate = 0; TimeId lastSaleDate = 0; + bool upgradable = false; bool birthday = false; friend inline bool operator==( @@ -66,9 +69,11 @@ struct UserStarGift { StarGift info; TextWithEntities message; int64 starsConverted = 0; + int64 starsUpgraded = 0; PeerId fromId = 0; MsgId messageId = 0; TimeId date = 0; + bool upgradable = false; bool anonymous = false; bool hidden = false; bool mine = false; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 23de3dc21..6e1ca060b 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1692,6 +1692,18 @@ ServiceAction ParseServiceAction( .anonymous = data.is_name_hidden(), }; }); + }, [&](const MTPDmessageActionStarGiftUnique &data) { + data.vgift().match([&](const MTPDstarGift &gift) { + result.content = ActionStarGift{ + .giftId = uint64(gift.vid().v), + .stars = int64(gift.vstars().v), + .limited = gift.is_limited(), + }; + }, [&](const MTPDstarGiftUnique &gift) { + result.content = ActionStarGift{ + .giftId = uint64(gift.vid().v), + }; + }); }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 648306f92..ecd6490d6 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -5424,6 +5424,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { lt_user, Ui::Text::Link(peer->shortName(), 1), // Link 1. Ui::Text::WithEntities); + return result; } const auto cost = TextWithEntities{ tr::lng_action_gift_for_stars(tr::now, lt_count, stars), @@ -5455,6 +5456,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareStarGiftUnique = [&]( + const MTPDmessageActionStarGiftUnique &action) { + auto result = PreparedServiceText(); + const auto isSelf = _from->isSelf(); + const auto peer = isSelf ? _history->peer : _from; + result.links.push_back(peer->createOpenLink()); + result.text = (isSelf + ? tr::lng_action_gift_upgraded_mine + : tr::lng_action_gift_upgraded)( + tr::now, + lt_user, + Ui::Text::Link(peer->shortName(), 1), // Link 1. + Ui::Text::WithEntities); + return result; + }; + setServiceText(action.match( prepareChatAddUserText, prepareChatJoinedByLink, @@ -5501,6 +5518,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareGiftStars, prepareGiftPrize, prepareStarGift, + prepareStarGiftUnique, PrepareEmptyText, PrepareErrorText)); @@ -5639,18 +5657,44 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { } : TextWithEntities()), .starsConverted = int(data.vconvert_stars().value_or_empty()), + .starsUpgraded = int(data.vupgrade_stars().value_or_empty()), .type = Data::GiftType::StarGift, + .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), .converted = data.is_converted(), + .upgraded = data.is_upgraded(), .saved = data.is_saved(), }; if (auto gift = Api::FromTL(&history()->session(), data.vgift())) { - fields.stickerId = gift->stickerId; + fields.stargiftId = gift->id; + fields.document = gift->document; fields.limitedCount = gift->limitedCount; fields.limitedLeft = gift->limitedLeft; fields.count = gift->stars; fields.unique = gift->unique; } + _media = std::make_unique( + this, + _from, + std::move(fields)); + }, [&](const MTPDmessageActionStarGiftUnique &data) { + using Fields = Data::GiftCode; + auto fields = Fields{ + .type = Data::GiftType::StarGift, + .saved = data.is_saved(), + }; + if (auto gift = Api::FromTL(&history()->session(), data.vgift())) { + fields.stargiftId = gift->id; + fields.document = gift->document; + fields.limitedCount = gift->limitedCount; + fields.limitedLeft = gift->limitedLeft; + fields.count = gift->stars; + fields.unique = gift->unique; + } + _media = std::make_unique( + this, + _from, + std::move(fields)); }, [](const auto &) { }); } 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 ecd51900c..c589d4d6a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -294,20 +294,13 @@ int PremiumGift::credits() const { void PremiumGift::ensureStickerCreated() const { if (_sticker) { return; - } else if (const auto stickerId = _data.stickerId) { - if (!_lifetime) { - const auto owner = &_parent->history()->owner(); - _lifetime = owner->customEmojiManager().resolve( - stickerId - ) | rpl::start_with_next([=](not_null document) { - const auto sticker = document->sticker(); - Assert(sticker != nullptr); - _sticker.emplace(_parent, document, false, _parent); - _sticker->setPlayingOnce(true); - _sticker->initSize(st::msgServiceGiftBoxStickerSize); - _parent->repaint(); - }); - } + } else if (const auto document = _data.document) { + const auto sticker = document->sticker(); + Assert(sticker != nullptr); + _sticker.emplace(_parent, document, false, _parent); + _sticker->setPlayingOnce(true); + _sticker->initSize(st::msgServiceGiftBoxStickerSize); + _parent->repaint(); return; } const auto &session = _parent->history()->session(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index f070c54a6..db976370e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -60,7 +60,6 @@ private: const not_null _gift; const Data::GiftCode &_data; mutable std::optional _sticker; - mutable rpl::lifetime _lifetime; }; 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 ade604d44..3023a46f7 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -452,16 +452,15 @@ not_null Delegate::hiddenMark() { return _hiddenMark.get(); } -DocumentId GiftStickerId( +DocumentData *LookupGiftSticker( not_null session, const GiftDescriptor &descriptor) { return v::match(descriptor, [&](GiftTypePremium data) { auto &packs = session->giftBoxStickersPacks(); packs.load(); - const auto document = packs.lookup(data.months); - return document ? document->id : DocumentId(); + return packs.lookup(data.months); }, [&](GiftTypeStars data) { - return data.info.stickerId; + return data.info.document.get(); }); } @@ -486,9 +485,7 @@ rpl::producer> GiftStickerValue( return not_null(document); }) | rpl::type_erased(); }, [&](GiftTypeStars data) { - return session->data().customEmojiManager().resolve( - data.info.stickerId - ) | rpl::map_error_to_done(); + return rpl::single(data.info.document) | rpl::type_erased(); }); } 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 ca28214e4..2db0ee2f0 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -132,7 +132,7 @@ private: }; -[[nodiscard]] DocumentId GiftStickerId( +[[nodiscard]] DocumentData *LookupGiftSticker( not_null session, const GiftDescriptor &descriptor); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index f984ab874..4122878c7 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/gift_premium_box.h" +#include "boxes/star_gift_box.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "chat_helpers/stickers_lottie.h" #include "core/application.h" @@ -174,7 +175,6 @@ void ToggleStarGiftSaved( const auto weak = base::make_weak(window); api->request(MTPpayments_SaveStarGift( MTP_flags(save ? Flag(0) : Flag::f_unsave), - sender->inputUser, MTP_int(itemId.bare) )).done([=] { done(true); @@ -232,7 +232,6 @@ void ConvertStarGift( const auto api = &window->session().api(); const auto weak = base::make_weak(window); api->request(MTPpayments_ConvertStarGift( - sender->inputUser, MTP_int(itemId) )).done([=] { if (const auto strong = weak.get()) { @@ -1329,14 +1328,36 @@ 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.starsUpgraded, + [=](bool) { *upgradeGuard = false; }); + } + }; const auto canToggle = canConvert || couldConvert || nonConvertible; + const auto canUpgrade = e.stargiftId + && e.canUpgradeGift + && !e.uniqueGift; AddStarGiftTable( controller, content, e, canToggle ? toggleVisibility : Fn(), - canConvert ? convert : Fn()); + canConvert ? convert : Fn(), + canUpgrade ? upgrade : Fn()); } else { AddCreditsHistoryEntryTable(controller, content, e); AddSubscriptionEntryTable(controller, content, s); @@ -1584,16 +1605,19 @@ void UserStarGiftBox( .credits = StarsAmount(data.info.stars), .bareMsgId = uint64(data.messageId.bare), .barePeerId = data.fromId.value, - .bareGiftStickerId = data.info.stickerId, + .bareGiftStickerId = data.info.document->id, + .stargiftId = data.info.id, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = data.info.limitedCount, .limitedLeft = data.info.limitedLeft, .starsConverted = int(data.info.starsConverted), + .starsUpgraded = int(data.starsUpgraded), .converted = false, .anonymous = data.anonymous, .stargift = true, .savedToProfile = !data.hidden, .fromGiftsList = true, + .canUpgradeGift = data.upgradable, .in = data.mine, .gift = true, }, @@ -1615,15 +1639,18 @@ void StarGiftViewBox( .credits = StarsAmount(data.count), .bareMsgId = uint64(item->id.bare), .barePeerId = item->history()->peer->id.value, - .bareGiftStickerId = data.stickerId, + .bareGiftStickerId = data.document ? data.document->id : 0, + .stargiftId = data.stargiftId, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = data.limitedCount, .limitedLeft = data.limitedLeft, .starsConverted = data.starsConverted, + .starsUpgraded = data.starsUpgraded, .converted = data.converted, .anonymous = data.anonymous, .stargift = true, .savedToProfile = data.saved, + .canUpgradeGift = data.upgradable, .in = true, .gift = true, }, diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index fe03ea0a6..1e439afff 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -173,3 +173,15 @@ creditsHistoryEntriesList: PeerList(defaultPeerList) { } subscriptionCreditsBadgePadding: margins(10px, 1px, 8px, 3px); + +uniqueGiftModelTop: 20px; +uniqueGiftTitle: FlatLabel(boxTitle) { + align: align(top); +} +uniqueGiftTitleTop: 140px; +uniqueGiftSubtitle: FlatLabel(defaultFlatLabel) { + minWidth: 256px; + align: align(top); +} +uniqueGiftSubtitleTop: 170px; +uniqueGiftBottom: 20px; diff --git a/Telegram/SourceFiles/ui/new_badges.cpp b/Telegram/SourceFiles/ui/new_badges.cpp index 664248feb..05321816f 100644 --- a/Telegram/SourceFiles/ui/new_badges.cpp +++ b/Telegram/SourceFiles/ui/new_badges.cpp @@ -25,6 +25,7 @@ not_null CreateNewBadge( std::move(text), st::settingsPremiumNewBadge), st::settingsPremiumNewBadgePadding); + badge->show(); badge->setAttribute(Qt::WA_TransparentForMouseEvents); badge->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(badge);