diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 01e7f247b1..1c2fb9e8bf 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -337,6 +337,8 @@ PRIVATE boxes/star_gift_auction_box.h boxes/star_gift_box.cpp boxes/star_gift_box.h + boxes/star_gift_craft_box.cpp + boxes/star_gift_craft_box.h boxes/star_gift_preview_box.cpp boxes/star_gift_preview_box.h boxes/star_gift_resale_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 24bf34b301..ffbc01391b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3816,6 +3816,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; "lng_gift_collectible_tag" = "gift"; +"lng_gift_crafted_tag" = "crafted"; +"lng_gift_epic_tag" = "epic"; +"lng_gift_legendary_tag" = "legendary"; "lng_gift_view_unpack" = "Unpack"; "lng_gift_anonymous_hint" = "Only you can see the sender's name."; "lng_gift_anonymous_hint_channel" = "Only admins of this channel can see the sender's name."; @@ -4094,6 +4097,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_collection_reorder_exit" = "Apply Reorder"; "lng_gift_collection_remove_from" = "Remove from Collection"; "lng_gift_locked_title" = "Gift Locked"; +"lng_gift_craft_info_level" = "Level {n} Gift"; +"lng_gift_craft_info_title" = "Gift Crafting"; +"lng_gift_craft_info_about" = "Use your existing gifts to craft new ones."; +"lng_gift_craft_combine_title" = "Combine Gifts"; +"lng_gift_craft_combine_about" = "Add up to 4 NFT Gifts to attempt crafting a new upgraded model."; +"lng_gift_craft_input_title" = "Input Matters"; +"lng_gift_craft_input_about" = "Each craft has a success chance. Better combinations improve the outcome."; +"lng_gift_craft_exclusive_title" = "Exclusive Look"; +"lng_gift_craft_exclusive_about" = "Reforge gifts into a rarer collectibles with a new look."; +"lng_gift_craft_start_button" = "Start Crafting"; +"lng_gift_craft_title" = "Craft Gift"; +"lng_gift_craft_about1" = "Add up to **4** gifts to craft new {gift}."; +"lng_gift_craft_about2" = "If crafting fails, all selected gifts will be **consumed**."; +"lng_gift_craft_view_all" = "{emoji} View all new variants {arrow}"; +"lng_gift_craft_button" = "Craft {gift}"; +"lng_gift_craft_button_chance" = "{percent} Success Chance"; +"lng_gift_craft_select_title" = "Select Gifts"; +"lng_gift_craft_select_about" = "You need to add up to **3 gifts** from the same collection for crafting."; +"lng_gift_craft_select_your" = "Your gifts"; +"lng_gift_craft_select_none" = "You don't have other gifts from this collection."; +"lng_gift_craft_select_market#one" = "{count} suitable gift on sale"; +"lng_gift_craft_select_market#other" = "{count} suitable gifts on sale"; +"lng_gift_craft_progress" = "Crafting"; +"lng_gift_craft_progress_chance" = "{percent} success chance"; +"lng_gift_craft_progress_fails" = "If crafting fails, all selected gifts will be consumed."; "lng_auction_about_title" = "Auction"; "lng_auction_about_subtitle" = "Join the battle for exclusive gifts."; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 04bf454f34..1ff23cdf42 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -950,6 +950,7 @@ std::optional FromTL( .number = data.vnum().v, .onlyAcceptTon = data.is_resale_ton_only(), .canBeTheme = data.is_theme_available(), + .crafted = data.is_crafted(), .model = *model, .pattern = *pattern, .value = (data.vvalue_amount() @@ -999,6 +1000,9 @@ std::optional FromTL( unique->exportAt = data.vcan_export_at().value_or_empty(); unique->canTransferAt = data.vcan_transfer_at().value_or_empty(); unique->canResellAt = data.vcan_resell_at().value_or_empty(); + unique->canCraftAt = data.vcan_craft_at().value_or_empty(); + unique->craftChancePermille + = data.vcraft_chance_permille().value_or_empty(); } using Id = Data::SavedStarGiftId; const auto hasUnique = parsed->unique != nullptr; @@ -1045,7 +1049,17 @@ Data::UniqueGiftModel FromTL( .document = session->data().processDocument(data.vdocument()), }; result.name = qs(data.vname()); - result.rarityPermille = data.vrarity_permille().v; + data.vrarity().match([&](const MTPDstarGiftAttributeRarity &data) { + result.rarityPermille = data.vpermille().v; + }, [](const auto &) {}); + //MTPDstarGiftAttributeRarityRare &c_starGiftAttributeRarityRare() const; + //[[nodiscard]] const MTPDstarGiftAttributeRarityEpic &c_starGiftAttributeRarityEpic() const; + //[[nodiscard]] const MTPDstarGiftAttributeRarityLegendary &c_starGiftAttributeRarityLegendary() const; + //[[nodiscard]] const MTPDstarGiftAttributeRarityMythic &c_starGiftAttributeRarityMythic() const; + // ); + //data.vrarity().c_starGiftAttributeRarity(); + //data.vrarity().c_() + //result.rarityPermille = data.vrarity_permille().v; return result; } @@ -1057,14 +1071,18 @@ Data::UniqueGiftPattern FromTL( }; result.document->overrideEmojiUsesTextColor(true); result.name = qs(data.vname()); - result.rarityPermille = data.vrarity_permille().v; + data.vrarity().match([&](const MTPDstarGiftAttributeRarity &data) { + result.rarityPermille = data.vpermille().v; + }, [](const auto &) {}); return result; } Data::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) { auto result = Data::UniqueGiftBackdrop{ .id = data.vbackdrop_id().v }; result.name = qs(data.vname()); - result.rarityPermille = data.vrarity_permille().v; + data.vrarity().match([&](const MTPDstarGiftAttributeRarity &data) { + result.rarityPermille = data.vpermille().v; + }, [](const auto &) {}); result.centerColor = Ui::ColorFromSerialized( data.vcenter_color()); result.edgeColor = Ui::ColorFromSerialized( diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 8d1b90c4b0..fe23e31a6d 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" // kMaxAccounts #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" +#include "ui/controls/feature_list.h" #include "ui/layers/generic_box.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_graphics.h" @@ -904,42 +905,28 @@ struct VideoPreviewDocument { } void AddGiftsInfoRows(not_null container) { - const auto infoRow = [&]( - rpl::producer title, - rpl::producer text, - not_null icon) { - auto raw = container->add( - object_ptr(container)); - raw->add( - object_ptr( - raw, - std::move(title) | rpl::map(tr::bold), - st::defaultFlatLabel), - st::settingsPremiumRowTitlePadding); - raw->add( - object_ptr( - raw, - std::move(text), - st::upgradeGiftSubtext), - st::settingsPremiumRowAboutPadding); - object_ptr( - raw, - *icon, - st::starrefInfoIconPosition); + const auto features = std::vector{ + { + st::menuIconUnique, + tr::lng_gift_upgrade_unique_title(tr::now), + tr::lng_gift_upgrade_unique_about(tr::now, tr::marked), + }, + { + st::menuIconTradable, + tr::lng_gift_upgrade_tradable_title(tr::now), + tr::lng_gift_upgrade_tradable_about(tr::now, tr::marked), + }, + { + st::menuIconNftWear, + tr::lng_gift_upgrade_wearable_title(tr::now), + tr::lng_gift_upgrade_wearable_about(tr::now, tr::marked), + }, }; - - infoRow( - tr::lng_gift_upgrade_unique_title(), - tr::lng_gift_upgrade_unique_about(), - &st::menuIconUnique); - infoRow( - tr::lng_gift_upgrade_tradable_title(), - tr::lng_gift_upgrade_tradable_about(), - &st::menuIconTradable); - infoRow( - tr::lng_gift_upgrade_wearable_title(), - tr::lng_gift_upgrade_wearable_about(), - &st::menuIconNftWear); + for (const auto &feature : features) { + container->add( + Ui::MakeFeatureListEntry(container, feature), + st::boxRowPadding); + } } void PreviewBox( diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 7dd14d98ca..3a907bb986 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "api/api_premium.h" #include "api/api_text_entities.h" -//#include "base/call_delayed.h" #include "base/event_filter.h" #include "base/qt_signal_producer.h" #include "base/random.h" @@ -85,6 +84,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/controls/button_labels.h" #include "ui/controls/emoji_button.h" +#include "ui/controls/feature_list.h" #include "ui/controls/ton_common.h" #include "ui/controls/userpic_button.h" #include "ui/effects/path_shift_gradient.h" @@ -5224,60 +5224,54 @@ void UpgradeBox( AddSkip(container, st::defaultVerticalListSkip * 2); - const auto infoRow = [&]( - rpl::producer title, - rpl::producer text, - not_null icon) { - auto raw = container->add( - object_ptr(container)); - raw->add( - object_ptr( - raw, - std::move(title) | rpl::map(tr::bold), - st::defaultFlatLabel), - st::settingsPremiumRowTitlePadding); - raw->add( - object_ptr( - raw, - std::move(text), - st::upgradeGiftSubtext), - st::settingsPremiumRowAboutPadding); - object_ptr( - raw, - *icon, - st::starrefInfoIconPosition); + const auto features = std::vector{ + { + st::menuIconUnique, + tr::lng_gift_upgrade_unique_title(tr::now), + (args.savedId + ? tr::lng_gift_upgrade_unique_about(tr::now, tr::marked) + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_unique_about_channel + : tr::lng_gift_upgrade_unique_about_user)( + tr::now, + lt_name, + tr::marked(args.peer->shortName()), + tr::marked)), + }, + { + st::menuIconReplace, + tr::lng_gift_upgrade_transferable_title(tr::now), + (args.savedId + ? tr::lng_gift_upgrade_transferable_about( + tr::now, + tr::marked) + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_transferable_about_channel + : tr::lng_gift_upgrade_transferable_about_user)( + tr::now, + lt_name, + tr::marked(args.peer->shortName()), + tr::marked)), + }, + { + st::menuIconTradable, + tr::lng_gift_upgrade_tradable_title(tr::now), + (args.savedId + ? tr::lng_gift_upgrade_tradable_about(tr::now, tr::marked) + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_tradable_about_channel + : tr::lng_gift_upgrade_tradable_about_user)( + tr::now, + lt_name, + tr::marked(args.peer->shortName()), + tr::marked)), + }, }; - - infoRow( - tr::lng_gift_upgrade_unique_title(), - (args.savedId - ? tr::lng_gift_upgrade_unique_about() - : (args.peer->isBroadcast() - ? tr::lng_gift_upgrade_unique_about_channel - : tr::lng_gift_upgrade_unique_about_user)( - lt_name, - rpl::single(args.peer->shortName()))), - &st::menuIconUnique); - infoRow( - tr::lng_gift_upgrade_transferable_title(), - (args.savedId - ? tr::lng_gift_upgrade_transferable_about() - : (args.peer->isBroadcast() - ? tr::lng_gift_upgrade_transferable_about_channel - : tr::lng_gift_upgrade_transferable_about_user)( - lt_name, - rpl::single(args.peer->shortName()))), - &st::menuIconReplace); - infoRow( - tr::lng_gift_upgrade_tradable_title(), - (args.savedId - ? tr::lng_gift_upgrade_tradable_about() - : (args.peer->isBroadcast() - ? tr::lng_gift_upgrade_tradable_about_channel - : tr::lng_gift_upgrade_tradable_about_user)( - lt_name, - rpl::single(args.peer->shortName()))), - &st::menuIconTradable); + for (const auto &feature : features) { + container->add( + Ui::MakeFeatureListEntry(container, feature), + st::boxRowPadding); + } const auto gifting = !args.savedId && !args.giftPrepayUpgradeHash.isEmpty(); diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp new file mode 100644 index 0000000000..2df5955893 --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp @@ -0,0 +1,497 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/star_gift_craft_box.h" + +#include "apiwrap.h" +#include "api/api_credits.h" +#include "api/api_premium.h" +#include "boxes/star_gift_box.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "data/data_star_gift.h" +#include "data/data_user.h" +#include "info/peer_gifts/info_peer_gifts_common.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/about_cocoon_box.h" // AddUniqueCloseButton. +#include "ui/controls/button_labels.h" +#include "ui/controls/feature_list.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "ui/top_background_gradient.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" + +namespace Ui { +namespace { + +using namespace Info::PeerGifts; + +[[nodiscard]] QImage CreateBgGradient( + QSize size, + const Data::UniqueGiftBackdrop &backdrop) { + 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( + QPoint(size.width() / 2, size.height() / 2), + size.height() / 2); + gradient.setStops({ + { 0., backdrop.centerColor }, + { 1., backdrop.edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRect(0, 0, size.width(), size.height()); + p.end(); + + const auto mask = Images::CornersMask(st::boxRadius); + return Images::Round(std::move(result), mask); +} + +void ShowSelectGiftBox( + not_null controller, + std::vector list, + Fn)> chosen, + bool exists) { + controller->show(Box([=](not_null box) { + box->setTitle(rpl::single(u"Choose Gift"_q)); + box->setWidth(st::boxWideWidth); + + struct Entry { + Data::SavedStarGift gift; + GiftButton *button = nullptr; + }; + struct State { + explicit State(not_null session) + : delegate(session, GiftButtonMode::Minimal) { + } + + Delegate delegate; + std::vector entries; + rpl::event_stream<> changes; + rpl::variable successPercentPermille; + rpl::variable successPercentText; + + int requestingIndex = 0; + bool crafting = false; + }; + const auto session = &controller->session(); + const auto state = box->lifetime().make_state(session); + + const auto single = state->delegate.buttonSize(); + const auto extend = state->delegate.buttonExtend(); + const auto full = single.grownBy(extend).height(); + const auto skip = st::boxRowPadding.left() / 2; + auto row = (RpWidget*)nullptr; + for (auto &gift : list) { + if (!row) { + row = box->addRow(object_ptr(box), QMargins()); + row->resize(row->width(), full); + } + const auto col = (state->entries.size() % 3); + state->entries.push_back(Entry{ + .gift = gift, + .button = CreateChild( + row, + &state->delegate), + }); + const auto button = state->entries.back().button; + button->setClickedCallback([=] { + chosen(gift); + box->closeBox(); + }); + button->show(); + button->setDescriptor(GiftTypeStars{ + .info = { + .id = gift.info.id, + .unique = gift.info.unique, + .document = gift.info.unique->model.document, + }, + }, GiftButton::Mode::Minimal); + const auto width = (st::boxWideWidth - 2 * skip - st::boxRowPadding.left() - st::boxRowPadding.right()) / 3; + const auto left = st::boxRowPadding.left() + (width + skip) * col; + button->setGeometry(QRect(left, extend.top(), width, single.height()), extend); + if (col == 2) { + row = nullptr; + } + } + + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + if (exists) { + box->addLeftButton(rpl::single(u"Remove"_q), [=] { + chosen(std::nullopt); + box->closeBox(); + }); + } + box->setMaxHeight(st::boxWideWidth); + })); +} + +[[nodiscard]] object_ptr MakeCraftContent( + not_null box, + not_null controller, + std::shared_ptr gift, + Data::SavedStarGiftId savedId) { + auto result = object_ptr(box->verticalLayout()); + const auto raw = result.data(); + + const auto width = st::boxWidth; + const auto height = st::boxWideWidth; + + struct BackdropView { + Data::UniqueGiftBackdrop colors; + QImage gradient; + }; + struct PatternView { + DocumentData *document = nullptr; + std::unique_ptr emoji; + base::flat_map> emojis; + }; + struct Entry { + Data::SavedStarGiftId id; + std::shared_ptr gift; + GiftButton *button = nullptr; + AbstractButton *add = nullptr; + }; + struct State { + explicit State(not_null session) + : delegate(session, GiftButtonMode::Minimal) { + } + + Delegate delegate; + BackdropView backdrop; + PatternView pattern; + std::vector entries; + rpl::event_stream<> changes; + rpl::variable successPercentPermille; + rpl::variable successPercentText; + + int requestingIndex = 0; + bool crafting = false; + }; + const auto session = &controller->session(); + const auto state = raw->lifetime().make_state(session); + state->backdrop.colors.centerColor = QColor(42, 61, 82, 255); + state->backdrop.colors.edgeColor = QColor(35, 45, 63, 255); + state->entries.push_back(Entry{ savedId, gift }); + for (auto i = 0; i != 3; ++i) { + state->entries.emplace_back(); + } + + state->successPercentPermille = state->changes.events_starting_with_copy( + rpl::empty + ) | rpl::map([=] { + auto result = 0; + for (const auto &entry : state->entries) { + if (const auto gift = entry.gift.get()) { + result += gift->craftChancePermille; + } + } + return result; + }); + state->successPercentText = state->successPercentPermille.value( + ) | rpl::map([](int permille) { + return QString::number(permille / 10.) + '%'; + }); + + const auto editEntry = [=](int index) { + const auto guard = base::make_weak(raw); + if (state->requestingIndex) { + state->requestingIndex = index; + return; + } + state->requestingIndex = index; + session->api().request(MTPpayments_GetCraftStarGifts( + MTP_long(gift->initialGiftId), + MTP_string(), + MTP_int(30) + )).done([=](const MTPpayments_SavedStarGifts &result) { + if (!guard) { + return; + } + const auto user = session->user(); + const auto owner = &session->data(); + const auto &data = result.data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + + auto list = std::vector(); + list.reserve(data.vgifts().v.size()); + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(user, gift)) { + list.push_back(std::move(*parsed)); + } + } + + const auto index = base::take(state->requestingIndex); + ShowSelectGiftBox(controller, list, [=](std::optional chosen) { + if (chosen) { + state->entries[index].gift = chosen->info.unique; + state->entries[index].id = chosen->manageId; + } else { + state->entries[index].gift = nullptr; + state->entries[index].id = Data::SavedStarGiftId(); + } + state->changes.fire({}); + }, (state->entries[index].gift != nullptr)); + }).send(); + }; + const auto refreshButton = [=](int index) { + Expects(index >= 0 && index < state->entries.size()); + + auto &entry = state->entries[index]; + const auto single = state->delegate.buttonSize(); + const auto skip = st::boxTitleClose.width; + const auto geometry = QRect( + (index % 2) ? (width - skip - single.width()) : skip, + (index < 2) ? skip : (height - st::giftBox.buttonPadding.bottom() - st::giftBox.buttonHeight - st::giftBox.buttonPadding.top() - single.height()), + single.width(), + single.height()); + delete base::take(entry.add); + delete base::take(entry.button); + if (entry.gift) { + entry.button = CreateChild(raw, &state->delegate); + entry.button->setDescriptor(GiftTypeStars{ + .info = { + .id = entry.gift->initialGiftId, + .unique = entry.gift, + .document = entry.gift->model.document, + }, + }, GiftButton::Mode::Minimal); + entry.button->show(); + if (index > 0) { + entry.button->setClickedCallback([=] { + editEntry(index); + }); + } else { + entry.button->setAttribute(Qt::WA_TransparentForMouseEvents); + } + entry.button->setGeometry( + geometry, + state->delegate.buttonExtend()); + } else { + entry.add = CreateChild(raw); + entry.add->show(); + entry.add->paintOn([=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + const auto radius = st::boxRadius; + p.setPen(Qt::NoPen); + p.setBrush(QColor(57, 77, 99)); + p.drawRoundedRect( + QRect(QPoint(), geometry.size()), + radius, + radius); + const auto addSize = geometry.width() / 3; + p.setBrush(QColor(255, 255, 255)); + p.drawEllipse(addSize, (geometry.height() - addSize) / 2, addSize, addSize); + + const auto plusSize = geometry.width() / 6; + p.setPen(QPen(QBrush(QColor(57, 77, 99)), plusSize / 5)); + p.setBrush(Qt::NoBrush); + p.drawLine((geometry.width() - plusSize) / 2, geometry.height() / 2, (geometry.width() - plusSize) / 2 + plusSize, geometry.height() / 2); + p.drawLine(geometry.width() / 2, (geometry.height() - plusSize) / 2, geometry.width() / 2, (geometry.height() - plusSize) / 2 + plusSize); + }); + entry.add->setClickedCallback([=] { + editEntry(index); + }); + entry.add->setGeometry(geometry); + } + }; + state->changes.events_starting_with_copy(rpl::empty) | rpl::on_next([=] { + for (auto i = 0; i != 4; ++i) { + refreshButton(i); + } + }, raw->lifetime()); + + const auto setupPattern = [=]( + PatternView &to, + const Data::UniqueGiftPattern &pattern) { + const auto document = pattern.document; + const auto callback = [=] { + if (state->pattern.document == document) { + raw->update(); + } + }; + to.document = document; + to.emoji = document->owner().customEmojiManager().create( + document, + callback, + Data::CustomEmojiSizeTag::Large); + [[maybe_unused]] const auto preload = to.emoji->ready(); + }; + + setupPattern(state->pattern, gift->pattern); + raw->paintOn([=](QPainter &p) { + const auto width = raw->width(); + const auto getBackdrop = [&](BackdropView &backdrop) { + const auto ratio = style::DevicePixelRatio(); + const auto gradientSize = QSize(width, raw->height()); + auto &gradient = backdrop.gradient; + if (gradient.size() != gradientSize * ratio) { + gradient = CreateBgGradient(gradientSize, backdrop.colors); + } + return gradient; + }; + const auto paintPattern = [&]( + QPainter &p, + PatternView &pattern, + const BackdropView &backdrop, + float64 shown) { + const auto color = backdrop.colors.patternColor; + const auto key = (color.red() << 16) + | (color.green() << 8) + | color.blue(); + PaintBgPoints( + p, + PatternBgPoints(), + pattern.emojis[key], + pattern.emoji.get(), + color, + QRect(0, 0, width, st::uniqueGiftSubtitleTop), + shown); + }; + p.drawImage(0, 0, getBackdrop(state->backdrop)); + paintPattern(p, state->pattern, state->backdrop, 1.); + }); + + raw->resize(width, height); + + const auto button = CreateChild( + raw, + rpl::single(QString()), + st::giftBox.button); + button->setClickedCallback([=] { + if (state->crafting) { + return; + } + state->crafting = true; + auto inputs = QVector(); + for (const auto &entry : state->entries) { + if (entry.gift) { + inputs.push_back( + Api::InputSavedStarGiftId(entry.id, entry.gift)); + } + } + const auto weak = base::make_weak(controller); + session->api().request(MTPpayments_CraftStarGift( + MTP_vector(inputs) + )).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + if (const auto strong = weak.get()) { + strong->showPeerHistory(strong->session().user()->id); + } + }).send(); + }); + raw->sizeValue() | rpl::on_next([=](QSize size) { + const auto width = size.width(); + const auto height = size.height(); + const auto padding = st::giftBox.buttonPadding; + button->setFullWidth(width - padding.left() - padding.right()); + button->moveToLeft( + padding.left(), + height - padding.bottom() - button->height()); + }, raw->lifetime()); + SetButtonTwoLabels( + button, + tr::lng_gift_craft_button( + lt_gift, + rpl::single(tr::marked(Data::UniqueGiftName(*gift))), + tr::marked), + tr::lng_gift_craft_button_chance( + lt_percent, + state->successPercentText.value() | rpl::map(tr::marked), + tr::marked), + st::resaleButtonTitle, + st::resaleButtonSubtitle); + + return result; +} + +void ShowGiftCraftBox( + not_null controller, + std::shared_ptr gift, + Data::SavedStarGiftId savedId) { + controller->show(Box([=](not_null box) { + box->setStyle(st::giftCraftBox); + box->setWidth(st::boxWidth); + box->setNoContentMargin(true); + box->addRow( + MakeCraftContent(box, controller, gift, savedId), QMargins()); + AddUniqueCloseButton(box); + })); +} + +} // namespace + +void ShowGiftCraftInfoBox( + not_null controller, + std::shared_ptr gift, + Data::SavedStarGiftId savedId) { + controller->show(Box([=](not_null box) { + const auto container = box->verticalLayout(); + auto cover = tr::lng_gift_craft_info_title( + ) | rpl::map([=](const QString &title) { + auto result = UniqueGiftCover{ *gift }; + result.values.title = title; + return result; + }); + AddUniqueGiftCover(container, std::move(cover), { + .pretitle = tr::lng_gift_craft_info_level( + lt_n, + rpl::single(QString::number(2))), + .subtitle = tr::lng_gift_craft_info_about(tr::marked), + }); + AddSkip(container); + + AddUniqueCloseButton(box); + + const auto features = std::vector{ + { + st::menuIconUnique, + tr::lng_gift_craft_combine_title(tr::now), + tr::lng_gift_craft_combine_about(tr::now, tr::rich), + }, + { + st::menuIconTradable, + tr::lng_gift_craft_input_title(tr::now), + tr::lng_gift_craft_input_about(tr::now, tr::marked), + }, + { + st::menuIconNftWear, + tr::lng_gift_craft_exclusive_title(tr::now), + tr::lng_gift_craft_exclusive_about(tr::now, tr::marked), + }, + }; + for (const auto &feature : features) { + container->add( + MakeFeatureListEntry(container, feature), + st::boxRowPadding); + } + + box->setStyle(st::giftBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + + box->addButton(tr::lng_gift_craft_start_button(), [=] { + ShowGiftCraftBox(controller, gift, savedId); + }); + })); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.h b/Telegram/SourceFiles/boxes/star_gift_craft_box.h new file mode 100644 index 0000000000..eaad97fef7 --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.h @@ -0,0 +1,26 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +struct UniqueGift; +class SavedStarGiftId; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { + +void ShowGiftCraftInfoBox( + not_null controller, + std::shared_ptr gift, + Data::SavedStarGiftId savedId); + +} // namespace Ui diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 93a991bfc3..1deb105a99 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -203,6 +203,7 @@ struct GiftCode { bool refunded : 1 = false; bool upgrade : 1 = false; bool saved : 1 = false; + bool craft : 1 = false; }; class Media { diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index cba0f2fded..6515479329 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -97,15 +97,18 @@ struct UniqueGift { PeerData *releasedBy = nullptr; PeerData *themeUser = nullptr; int64 nanoTonForResale = -1; + int craftChancePermille = 0; int starsForResale = -1; int starsForTransfer = -1; int starsMinOffer = -1; int number = 0; bool onlyAcceptTon = false; bool canBeTheme = false; + bool crafted = false; TimeId exportAt = 0; TimeId canTransferAt = 0; TimeId canResellAt = 0; + TimeId canCraftAt = 0; UniqueGiftModel model; UniqueGiftPattern pattern; UniqueGiftBackdrop backdrop; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ee9a540922..132a70f428 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -6925,6 +6925,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { .refunded = data.is_refunded(), .upgrade = data.is_upgrade(), .saved = data.is_saved(), + .craft = data.is_craft(), }; if (auto gift = Api::FromTL(&history()->session(), data.vgift())) { fields.stargiftId = gift->id; @@ -6940,6 +6941,9 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { unique->exportAt = data.vcan_export_at().value_or_empty(); unique->canTransferAt = data.vcan_transfer_at().value_or_empty(); unique->canResellAt = data.vcan_resell_at().value_or_empty(); + unique->canCraftAt = data.vcan_craft_at().value_or_empty(); + unique->craftChancePermille + = data.vcraft_chance_permille().value_or_empty(); } } _media = std::make_unique( diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 54dca1ebb9..5bc140fd0e 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1955,9 +1955,9 @@ botVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true botVerification#f93cd45c bot_id:long icon:long description:string = BotVerification; -starGiftAttributeModel#39d99013 name:string document:Document rarity_permille:int = StarGiftAttribute; -starGiftAttributePattern#13acff19 name:string document:Document rarity_permille:int = StarGiftAttribute; -starGiftAttributeBackdrop#d93d859c name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity_permille:int = StarGiftAttribute; +starGiftAttributeModel#565251e2 flags:# crafted:flags.0?true name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributePattern#4e7085ea name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributeBackdrop#9f2504e4 name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity:StarGiftAttributeRarity = StarGiftAttribute; starGiftAttributeOriginalDetails#e0bff26c flags:# sender_id:flags.0?Peer recipient_id:Peer date:int message:flags.1?TextWithEntities = StarGiftAttribute; payments.starGiftUpgradePreview#3de1dfed sample_attributes:Vector prices:Vector next_prices:Vector = payments.StarGiftUpgradePreview; @@ -2114,6 +2114,12 @@ messages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:l messages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo; messages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector plays_left:flags.0?int = messages.EmojiGameInfo; +starGiftAttributeRarity#36437737 permille:int = StarGiftAttributeRarity; +starGiftAttributeRarityRare#f08d516b = StarGiftAttributeRarity; +starGiftAttributeRarityEpic#78fbf3a8 = StarGiftAttributeRarity; +starGiftAttributeRarityLegendary#cef7e7a8 = StarGiftAttributeRarity; +starGiftAttributeRarityMythic#f8374495 = StarGiftAttributeRarity; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index ff6d7255c6..9c15876598 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/gift_premium_box.h" #include "boxes/share_box.h" #include "boxes/star_gift_box.h" +#include "boxes/star_gift_craft_box.h" #include "boxes/star_gift_resale_box.h" #include "boxes/transfer_gift_box.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -963,6 +964,25 @@ void ProcessReceivedSubscriptions( // (owner->isChannel() && owner->asChannel()->canTransferGifts()); } +[[nodiscard]] bool CanCraftGift( + not_null session, + const Data::CreditsHistoryEntry &e) { + const auto unique = e.uniqueGift.get(); + if (!unique || !unique->craftChancePermille) { + return false; + } + const auto owner = (unique && unique->ownerId) + ? session->data().peer(unique->ownerId).get() + : nullptr; + return !owner + ? false + : owner->isSelf() + ? e.in + : false; + // Currently we're not crafting channel gifts. + // (owner->isChannel() && owner->asChannel()->canTransferGifts()); +} + [[nodiscard]] bool ShowOfferBuyButton( not_null session, const Data::CreditsHistoryEntry &e) { @@ -1121,6 +1141,14 @@ void FillUniqueGiftMenu( } }, st.transfer ? st.transfer : &st::menuIconReplace); } + if (CanCraftGift(&show->session(), e)) { + AssertIsDebug(icon and if we even put this here); + menu->addAction(tr::lng_gift_craft_title(tr::now), [=] { + if (const auto window = show->resolveWindow()) { + Ui::ShowGiftCraftInfoBox(window, unique, savedId); + } + }, st.transfer ? st.transfer : &st::menuIconReplace); + } const auto wear = host->isSelf() ? e.in : (host->isChannel() && host->asChannel()->canEditEmoji()); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 1f59c47e6f..9b81d87507 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -122,6 +122,7 @@ struct CreditsEntryBoxStyleOverrides { const style::icon *share = nullptr; const style::icon *theme = nullptr; const style::icon *transfer = nullptr; + const style::icon *craft = nullptr; const style::icon *wear = nullptr; const style::icon *takeoff = nullptr; const style::icon *resell = nullptr; diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index ac82d8ab54..2daab159e8 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -139,6 +139,10 @@ giftBox: Box(defaultBox) { giftLimitedBox: Box(giftBox) { buttonPadding: margins(22px, 48px, 22px, 12px); } +giftCraftBox: Box(giftBox) { + buttonPadding: margins(0px, 0px, 0px, 0px); + buttonHeight: 0px; +} giftLimitedPadding: margins(8px, 4px, 8px, 4px); giftBoxSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) {