diff --git a/Telegram/Resources/icons/menu/tag_sell.png b/Telegram/Resources/icons/menu/tag_sell.png new file mode 100644 index 0000000000..5afc942076 Binary files /dev/null and b/Telegram/Resources/icons/menu/tag_sell.png differ diff --git a/Telegram/Resources/icons/menu/tag_sell@2x.png b/Telegram/Resources/icons/menu/tag_sell@2x.png new file mode 100644 index 0000000000..bd0cc198f0 Binary files /dev/null and b/Telegram/Resources/icons/menu/tag_sell@2x.png differ diff --git a/Telegram/Resources/icons/menu/tag_sell@3x.png b/Telegram/Resources/icons/menu/tag_sell@3x.png new file mode 100644 index 0000000000..da06c7bf1c Binary files /dev/null and b/Telegram/Resources/icons/menu/tag_sell@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 701870489a..4e8c9c3ea3 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3422,6 +3422,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_stars_limited" = "limited"; "lng_gift_stars_sold_out" = "sold out"; "lng_gift_stars_resale" = "resale"; +"lng_gift_stars_on_sale" = "on sale"; "lng_gift_stars_tabs_all" = "All Gifts"; "lng_gift_stars_tabs_my" = "My Gifts"; "lng_gift_stars_tabs_limited" = "Limited"; @@ -3581,6 +3582,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_transfer_button_for" = "Transfer for {price}"; "lng_gift_transfer_wear" = "Wear"; "lng_gift_transfer_take_off" = "Take Off"; +"lng_gift_transfer_sell" = "Sell"; +"lng_gift_transfer_unlist" = "Unlist"; +"lng_gift_sell_unlist_title" = "Unlist {name}"; +"lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?"; +"lng_gift_sell_title" = "Price in Stars"; +"lng_gift_sell_placeholder" = "Enter price in Stars"; +"lng_gift_sell_about" = "You will receive {percent} of the selected amount."; +"lng_gift_sell_amount#one" = "You will receive **{count}** Star."; +"lng_gift_sell_amount#other" = "You will receive **{count}** Stars."; +"lng_gift_sell_min_price#one" = "Minimum price is {count} Star."; +"lng_gift_sell_min_price#other" = "Minimum price is {count} Stars."; +"lng_gift_sell_put" = "Put for Sale"; +"lng_gift_sell_update" = "Update the Price"; +"lng_gift_sell_toast" = "{name} is now for sale!"; +"lng_gift_sell_removed" = "{name} is removed from sale."; "lng_gift_menu_show" = "Show"; "lng_gift_menu_hide" = "Hide"; "lng_gift_wear_title" = "Wear {name}"; diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index e9d04211de..1ef95d7c89 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -145,6 +145,10 @@ void EditPriceBox( field->resize(width, field->height()); wrap->resize(width, field->height()); }, wrap->lifetime()); + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); field->selectAll(); box->setFocusCallback([=] { field->setFocusFast(); @@ -165,11 +169,6 @@ void EditPriceBox( return false; }); - field->paintRequest() | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(field); - st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); - }, field->lifetime()); - const auto save = [=] { const auto now = field->getLastText().toULongLong(); if (now > limit) { diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index a7009ba124..7279a8fead 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "api/api_premium.h" #include "base/event_filter.h" +#include "base/qt_signal_producer.h" #include "base/random.h" #include "base/timer_rpl.h" #include "base/unixtime.h" @@ -83,6 +84,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" +#include "ui/widgets/fields/number_input.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/popup_menu.h" @@ -4059,6 +4061,149 @@ void ShowUniqueGiftWearBox( })); } +void ShowUniqueGiftSellBox( + std::shared_ptr show, + not_null peer, + const Data::UniqueGift &gift, + Data::SavedStarGiftId savedId, + Settings::GiftWearBoxStyleOverride st) { + const auto priceNow = gift.starsForResale; + const auto name = Data::UniqueGiftName(gift); + const auto slug = gift.slug; + show->show(Box([=](not_null box) { + box->setTitle(tr::lng_gift_sell_title()); + box->setStyle(st.box ? *st.box : st::upgradeGiftBox); + box->setWidth(st::boxWideWidth); + + box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { + box->closeBox(); + }); + + const auto session = &show->session(); + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_gift_sell_placeholder(), + (st::boxRowPadding - QMargins( + st::defaultSubsectionTitlePadding.left(), + 0, + st::defaultSubsectionTitlePadding.right(), + st::defaultSubsectionTitlePadding.bottom()))); + const auto &appConfig = session->appConfig(); + const auto limit = appConfig.giftResalePriceMax(); + const auto minimal = appConfig.giftResalePriceMin(); + const auto thousandths = appConfig.giftResaleReceiveThousandths(); + const auto wrap = box->addRow(object_ptr( + box, + st::editTagField.heightMin)); + auto owned = object_ptr( + wrap, + st::editTagField, + rpl::single(QString()), + QString::number(priceNow ? priceNow : minimal), + limit); + const auto field = owned.data(); + wrap->widthValue() | rpl::start_with_next([=](int width) { + field->move(0, 0); + field->resize(width, field->height()); + wrap->resize(width, field->height()); + }, wrap->lifetime()); + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); + field->selectAll(); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + const auto errors = box->lifetime().make_state< + rpl::event_stream<> + >(); + auto goods = rpl::merge( + rpl::single(rpl::empty) | rpl::map_to(true), + base::qt_signal_producer( + field, + &Ui::NumberInput::changed + ) | rpl::map_to(true), + errors->events() | rpl::map_to(false) + ) | rpl::start_spawning(box->lifetime()); + auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { + const auto value = field->getLastText().toInt(); + const auto receive = (int64(value) * thousandths) / 1000; + return !good + ? tr::lng_gift_sell_min_price( + tr::now, + lt_count, + minimal, + Ui::Text::RichLangValue) + : (value >= minimal) + ? tr::lng_gift_sell_amount( + tr::now, + lt_count, + receive, + Ui::Text::RichLangValue) + : tr::lng_gift_sell_about( + tr::now, + lt_percent, + TextWithEntities{ u"%1%"_q.arg(thousandths / 10.) }, + Ui::Text::RichLangValue); + }); + const auto details = box->addRow(object_ptr( + box, + std::move(text) | rpl::after_next([=] { + box->verticalLayout()->resizeToWidth(box->width()); + }), + st::boxLabel)); + Ui::AddSkip(box->verticalLayout()); + + rpl::duplicate(goods) | rpl::start_with_next([=](bool good) { + details->setTextColorOverride( + good ? st::windowSubTextFg->c : st::boxTextFgError->c); + }, details->lifetime()); + + const auto button = box->addButton(priceNow + ? tr::lng_gift_sell_update() + : tr::lng_gift_sell_put(), [=] { + const auto count = field->getLastText().toInt(); + if (count < minimal) { + field->showError(); + errors->fire({}); + return; + } + box->closeBox(); + session->api().request(MTPpayments_UpdateStarGiftPrice( + (savedId + ? Api::InputSavedStarGiftId(savedId) + : MTP_inputSavedStarGiftSlug(MTP_string(slug))), + MTP_long(count) + )).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + show->showToast(tr::lng_gift_sell_toast( + tr::now, + lt_name, + name)); + session->data().notifyGiftUpdate({ + + }); + }).fail([=](const MTP::Error &error) { + show->showToast(error.type()); + }).send(); + + }); + 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()); + })); +} + struct UpgradeArgs : StarGiftUpgradeArgs { std::vector models; std::vector patterns; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 722c90a3dc..8612197b7e 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -17,6 +17,7 @@ namespace Data { struct UniqueGift; struct GiftCode; struct CreditsHistoryEntry; +class SavedStarGiftId; } // namespace Data namespace Main { @@ -68,6 +69,13 @@ void ShowUniqueGiftWearBox( const Data::UniqueGift &gift, Settings::GiftWearBoxStyleOverride st); +void ShowUniqueGiftSellBox( + std::shared_ptr show, + not_null peer, + const Data::UniqueGift &gift, + Data::SavedStarGiftId savedId, + Settings::GiftWearBoxStyleOverride st); + struct PatternPoint { QPointF position; float64 scale = 1.; diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index d70bc257b7..445337d33d 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -221,6 +221,8 @@ darkGiftShare: icon {{ "menu/share", groupCallMembersFg }}; darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }}; darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }}; darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }}; +darkGiftNftResell: icon {{ "menu/tag_sell", groupCallMembersFg }}; +darkGiftNftUnlist: icon {{ "menu/tag_remove", groupCallMembersFg }}; darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }}; darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }}; darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }}; @@ -262,3 +264,10 @@ darkGiftTableMessage: FlatLabel(giveawayGiftMessage) { darkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) { textFg: mediaviewMenuFg; } +darkGiftBoxClose: IconButton(boxTitleClose) { + icon: icon {{ "box_button_close", groupCallMemberInactiveIcon }}; + iconOver: icon {{ "box_button_close", groupCallMemberInactiveIcon }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: groupCallMembersBgOver; + } +} 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 a30cd5b333..ad95a55a9c 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -502,7 +502,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { && !data.userpic && !data.info.limitedLeft; return GiftBadge{ - .text = ((unique && (data.resale || pinned)) + .text = ((unique && data.resale && _small) + ? tr::lng_gift_stars_on_sale(tr::now) + : (unique && (data.resale || pinned)) ? ('#' + QString::number(unique->number)) : data.resale ? tr::lng_gift_stars_resale(tr::now) @@ -518,17 +520,25 @@ void GiftButton::paintEvent(QPaintEvent *e) { (((count % 1000) && (count < 10'000)) ? Lang::FormatCountDecimal(count) : Lang::FormatCountToShort(count).string))), - .bg1 = (unique + .bg1 = ((unique && data.resale && _small) + ? st::boxTextFgGood->c + : unique ? unique->backdrop.edgeColor : data.resale ? st::boxTextFgGood->c : soldOut ? st::attentionButtonFg->c : st::windowActiveTextFg->c), - .bg2 = (unique + .bg2 = ((unique && data.resale && _small) + ? QColor(0, 0, 0, 0) + : unique ? unique->backdrop.patternColor : QColor(0, 0, 0, 0)), - .fg = unique ? QColor(255, 255, 255) : st::windowBg->c, + .fg = ((unique && data.resale && _small) + ? st::windowBg->c + : unique + ? QColor(255, 255, 255) + : st::windowBg->c), .small = true, }; } diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 942138b23d..62a2fd6b36 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -128,6 +128,18 @@ bool AppConfig::confcallPrioritizeVP8() const { return get(u"confcall_use_vp8"_q, false); } +int AppConfig::giftResalePriceMax() const { + return get(u"stars_stargift_resale_amount_max"_q, 35000); +} + +int AppConfig::giftResalePriceMin() const { + return get(u"stars_stargift_resale_amount_min"_q, 125); +} + +int AppConfig::giftResaleReceiveThousandths() const { + return get(u"stars_stargift_resale_commission_permille"_q, 800); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 72a4e69c97..d536557758 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -83,6 +83,10 @@ public: [[nodiscard]] int confcallSizeLimit() const; [[nodiscard]] bool confcallPrioritizeVP8() const; + [[nodiscard]] int giftResalePriceMax() const; + [[nodiscard]] int giftResalePriceMin() const; + [[nodiscard]] int giftResaleReceiveThousandths() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 0bfc032364..0738bca33f 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1042,11 +1042,56 @@ void FillUniqueGiftMenu( }, st.wear ? st.wear : &st::menuIconNftWear); } } + const auto sell = owner->isSelf() + ? e.in + : (owner->isChannel() && owner->asChannel()->canTransferGifts()); + if (sell) { + const auto resalePrice = unique->starsForResale; + if (resalePrice > 0) { + menu->addAction(tr::lng_gift_transfer_unlist(tr::now), [=] { + const auto name = UniqueGiftName(*unique); + const auto confirm = [=](Fn close) { + close(); + session->api().request(MTPpayments_UpdateStarGiftPrice( + (savedId + ? Api::InputSavedStarGiftId(savedId) + : MTP_inputSavedStarGiftSlug( + MTP_string(unique->slug))), + MTP_long(0) + )).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + show->showToast(tr::lng_gift_sell_removed( + tr::now, + lt_name, + name)); + }).fail([=](const MTP::Error &error) { + show->showToast(error.type()); + }).send(); + }; + show->show(Ui::MakeConfirmBox({ + .text = tr::lng_gift_sell_unlist_sure(), + .confirmed = confirm, + .confirmText = tr::lng_gift_transfer_unlist(), + .title = tr::lng_gift_sell_unlist_title( + lt_name, + rpl::single(name)), + })); + }, st.unlist ? st.unlist : &st::menuIconTagRemove); + } else { + menu->addAction(tr::lng_gift_transfer_sell(tr::now), [=] { + const auto style = st.giftWearBox + ? *st.giftWearBox + : GiftWearBoxStyleOverride(); + ShowUniqueGiftSellBox(show, owner, *unique, savedId, style); + }, st.resell ? st.resell : &st::menuIconTagSell); + } + } } GiftWearBoxStyleOverride DarkGiftWearBoxStyle() { return { .box = &st::darkUpgradeGiftBox, + .close = &st::darkGiftBoxClose, .title = &st::darkUpgradeGiftTitle, .subtitle = &st::darkUpgradeGiftSubtitle, .radiantIcon = &st::darkUpgradeGiftRadiant, @@ -1068,6 +1113,8 @@ CreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle() { .transfer = &st::darkGiftTransfer, .wear = &st::darkGiftNftWear, .takeoff = &st::darkGiftNftTakeOff, + .resell = &st::darkGiftNftResell, + .unlist = &st::darkGiftNftUnlist, .show = &st::darkGiftShow, .hide = &st::darkGiftHide, .pin = &st::darkGiftPin, diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 5ab1f9ab84..a1bf02a4bd 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -46,6 +46,7 @@ struct Box; struct Table; struct FlatLabel; struct PopupMenu; +struct IconButton; struct PeerListItem; } // namespace style @@ -94,6 +95,7 @@ void AddWithdrawalWidget( struct GiftWearBoxStyleOverride { const style::Box *box = nullptr; + const style::IconButton *close = nullptr; const style::FlatLabel *title = nullptr; const style::FlatLabel *subtitle = nullptr; const style::icon *radiantIcon = nullptr; @@ -114,6 +116,8 @@ struct CreditsEntryBoxStyleOverrides { const style::icon *transfer = nullptr; const style::icon *wear = nullptr; const style::icon *takeoff = nullptr; + const style::icon *resell = nullptr; + const style::icon *unlist = nullptr; const style::icon *show = nullptr; const style::icon *hide = nullptr; const style::icon *pin = nullptr; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 8dca36fc9e..48c2b0d8fa 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -159,6 +159,8 @@ menuIconAsTopics: icon {{ "menu/mode_topics", menuIconColor }}; menuIconAsMessages: icon {{ "menu/mode_messages", menuIconColor }}; menuIconTagFilter: icon{{ "menu/tag_filter", menuIconColor }}; menuIconTagRename: icon{{ "menu/tag_rename", menuIconColor }}; +menuIconTagRemove: icon {{ "menu/tag_remove", menuIconColor }}; +menuIconTagSell: icon {{ "menu/tag_sell", menuIconColor }}; menuIconGroupsHide: icon {{ "menu/hide_members", menuIconColor }}; menuIconFont: icon {{ "menu/fonts", menuIconColor }}; menuIconFactcheck: icon {{ "menu/factcheck", menuIconColor }};