Allow placing your gifts on sale.

This commit is contained in:
John Preston 2025-04-22 12:04:26 +04:00
parent 5b71281ec4
commit 0a92e12a62
14 changed files with 265 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

View file

@ -3422,6 +3422,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_limited" = "limited"; "lng_gift_stars_limited" = "limited";
"lng_gift_stars_sold_out" = "sold out"; "lng_gift_stars_sold_out" = "sold out";
"lng_gift_stars_resale" = "resale"; "lng_gift_stars_resale" = "resale";
"lng_gift_stars_on_sale" = "on sale";
"lng_gift_stars_tabs_all" = "All Gifts"; "lng_gift_stars_tabs_all" = "All Gifts";
"lng_gift_stars_tabs_my" = "My Gifts"; "lng_gift_stars_tabs_my" = "My Gifts";
"lng_gift_stars_tabs_limited" = "Limited"; "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_button_for" = "Transfer for {price}";
"lng_gift_transfer_wear" = "Wear"; "lng_gift_transfer_wear" = "Wear";
"lng_gift_transfer_take_off" = "Take Off"; "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_show" = "Show";
"lng_gift_menu_hide" = "Hide"; "lng_gift_menu_hide" = "Hide";
"lng_gift_wear_title" = "Wear {name}"; "lng_gift_wear_title" = "Wear {name}";

View file

@ -145,6 +145,10 @@ void EditPriceBox(
field->resize(width, field->height()); field->resize(width, field->height());
wrap->resize(width, field->height()); wrap->resize(width, field->height());
}, wrap->lifetime()); }, 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(); field->selectAll();
box->setFocusCallback([=] { box->setFocusCallback([=] {
field->setFocusFast(); field->setFocusFast();
@ -165,11 +169,6 @@ void EditPriceBox(
return false; 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 save = [=] {
const auto now = field->getLastText().toULongLong(); const auto now = field->getLastText().toULongLong();
if (now > limit) { if (now > limit) {

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_global_privacy.h" #include "api/api_global_privacy.h"
#include "api/api_premium.h" #include "api/api_premium.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "base/qt_signal_producer.h"
#include "base/random.h" #include "base/random.h"
#include "base/timer_rpl.h" #include "base/timer_rpl.h"
#include "base/unixtime.h" #include "base/unixtime.h"
@ -83,6 +84,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
@ -4059,6 +4061,149 @@ void ShowUniqueGiftWearBox(
})); }));
} }
void ShowUniqueGiftSellBox(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> 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<Ui::GenericBox*> 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<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
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<Ui::FlatLabel>(
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 { struct UpgradeArgs : StarGiftUpgradeArgs {
std::vector<Data::UniqueGiftModel> models; std::vector<Data::UniqueGiftModel> models;
std::vector<Data::UniqueGiftPattern> patterns; std::vector<Data::UniqueGiftPattern> patterns;

View file

@ -17,6 +17,7 @@ namespace Data {
struct UniqueGift; struct UniqueGift;
struct GiftCode; struct GiftCode;
struct CreditsHistoryEntry; struct CreditsHistoryEntry;
class SavedStarGiftId;
} // namespace Data } // namespace Data
namespace Main { namespace Main {
@ -68,6 +69,13 @@ void ShowUniqueGiftWearBox(
const Data::UniqueGift &gift, const Data::UniqueGift &gift,
Settings::GiftWearBoxStyleOverride st); Settings::GiftWearBoxStyleOverride st);
void ShowUniqueGiftSellBox(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const Data::UniqueGift &gift,
Data::SavedStarGiftId savedId,
Settings::GiftWearBoxStyleOverride st);
struct PatternPoint { struct PatternPoint {
QPointF position; QPointF position;
float64 scale = 1.; float64 scale = 1.;

View file

@ -221,6 +221,8 @@ darkGiftShare: icon {{ "menu/share", groupCallMembersFg }};
darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }}; darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }};
darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }}; darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }};
darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }}; darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }};
darkGiftNftResell: icon {{ "menu/tag_sell", groupCallMembersFg }};
darkGiftNftUnlist: icon {{ "menu/tag_remove", groupCallMembersFg }};
darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }}; darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }};
darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }}; darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }};
darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }}; darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }};
@ -262,3 +264,10 @@ darkGiftTableMessage: FlatLabel(giveawayGiftMessage) {
darkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) { darkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) {
textFg: mediaviewMenuFg; textFg: mediaviewMenuFg;
} }
darkGiftBoxClose: IconButton(boxTitleClose) {
icon: icon {{ "box_button_close", groupCallMemberInactiveIcon }};
iconOver: icon {{ "box_button_close", groupCallMemberInactiveIcon }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: groupCallMembersBgOver;
}
}

View file

@ -502,7 +502,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
&& !data.userpic && !data.userpic
&& !data.info.limitedLeft; && !data.info.limitedLeft;
return GiftBadge{ 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)) ? ('#' + QString::number(unique->number))
: data.resale : data.resale
? tr::lng_gift_stars_resale(tr::now) ? tr::lng_gift_stars_resale(tr::now)
@ -518,17 +520,25 @@ void GiftButton::paintEvent(QPaintEvent *e) {
(((count % 1000) && (count < 10'000)) (((count % 1000) && (count < 10'000))
? Lang::FormatCountDecimal(count) ? Lang::FormatCountDecimal(count)
: Lang::FormatCountToShort(count).string))), : Lang::FormatCountToShort(count).string))),
.bg1 = (unique .bg1 = ((unique && data.resale && _small)
? st::boxTextFgGood->c
: unique
? unique->backdrop.edgeColor ? unique->backdrop.edgeColor
: data.resale : data.resale
? st::boxTextFgGood->c ? st::boxTextFgGood->c
: soldOut : soldOut
? st::attentionButtonFg->c ? st::attentionButtonFg->c
: st::windowActiveTextFg->c), : st::windowActiveTextFg->c),
.bg2 = (unique .bg2 = ((unique && data.resale && _small)
? QColor(0, 0, 0, 0)
: unique
? unique->backdrop.patternColor ? unique->backdrop.patternColor
: QColor(0, 0, 0, 0)), : 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, .small = true,
}; };
} }

View file

@ -128,6 +128,18 @@ bool AppConfig::confcallPrioritizeVP8() const {
return get<bool>(u"confcall_use_vp8"_q, false); return get<bool>(u"confcall_use_vp8"_q, false);
} }
int AppConfig::giftResalePriceMax() const {
return get<int>(u"stars_stargift_resale_amount_max"_q, 35000);
}
int AppConfig::giftResalePriceMin() const {
return get<int>(u"stars_stargift_resale_amount_min"_q, 125);
}
int AppConfig::giftResaleReceiveThousandths() const {
return get<int>(u"stars_stargift_resale_commission_permille"_q, 800);
}
void AppConfig::refresh(bool force) { void AppConfig::refresh(bool force) {
if (_requestId || !_api) { if (_requestId || !_api) {
if (force) { if (force) {

View file

@ -83,6 +83,10 @@ public:
[[nodiscard]] int confcallSizeLimit() const; [[nodiscard]] int confcallSizeLimit() const;
[[nodiscard]] bool confcallPrioritizeVP8() const; [[nodiscard]] bool confcallPrioritizeVP8() const;
[[nodiscard]] int giftResalePriceMax() const;
[[nodiscard]] int giftResalePriceMin() const;
[[nodiscard]] int giftResaleReceiveThousandths() const;
void refresh(bool force = false); void refresh(bool force = false);
private: private:

View file

@ -1042,11 +1042,56 @@ void FillUniqueGiftMenu(
}, st.wear ? st.wear : &st::menuIconNftWear); }, 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<void()> 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() { GiftWearBoxStyleOverride DarkGiftWearBoxStyle() {
return { return {
.box = &st::darkUpgradeGiftBox, .box = &st::darkUpgradeGiftBox,
.close = &st::darkGiftBoxClose,
.title = &st::darkUpgradeGiftTitle, .title = &st::darkUpgradeGiftTitle,
.subtitle = &st::darkUpgradeGiftSubtitle, .subtitle = &st::darkUpgradeGiftSubtitle,
.radiantIcon = &st::darkUpgradeGiftRadiant, .radiantIcon = &st::darkUpgradeGiftRadiant,
@ -1068,6 +1113,8 @@ CreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle() {
.transfer = &st::darkGiftTransfer, .transfer = &st::darkGiftTransfer,
.wear = &st::darkGiftNftWear, .wear = &st::darkGiftNftWear,
.takeoff = &st::darkGiftNftTakeOff, .takeoff = &st::darkGiftNftTakeOff,
.resell = &st::darkGiftNftResell,
.unlist = &st::darkGiftNftUnlist,
.show = &st::darkGiftShow, .show = &st::darkGiftShow,
.hide = &st::darkGiftHide, .hide = &st::darkGiftHide,
.pin = &st::darkGiftPin, .pin = &st::darkGiftPin,

View file

@ -46,6 +46,7 @@ struct Box;
struct Table; struct Table;
struct FlatLabel; struct FlatLabel;
struct PopupMenu; struct PopupMenu;
struct IconButton;
struct PeerListItem; struct PeerListItem;
} // namespace style } // namespace style
@ -94,6 +95,7 @@ void AddWithdrawalWidget(
struct GiftWearBoxStyleOverride { struct GiftWearBoxStyleOverride {
const style::Box *box = nullptr; const style::Box *box = nullptr;
const style::IconButton *close = nullptr;
const style::FlatLabel *title = nullptr; const style::FlatLabel *title = nullptr;
const style::FlatLabel *subtitle = nullptr; const style::FlatLabel *subtitle = nullptr;
const style::icon *radiantIcon = nullptr; const style::icon *radiantIcon = nullptr;
@ -114,6 +116,8 @@ struct CreditsEntryBoxStyleOverrides {
const style::icon *transfer = nullptr; const style::icon *transfer = nullptr;
const style::icon *wear = nullptr; const style::icon *wear = nullptr;
const style::icon *takeoff = nullptr; const style::icon *takeoff = nullptr;
const style::icon *resell = nullptr;
const style::icon *unlist = nullptr;
const style::icon *show = nullptr; const style::icon *show = nullptr;
const style::icon *hide = nullptr; const style::icon *hide = nullptr;
const style::icon *pin = nullptr; const style::icon *pin = nullptr;

View file

@ -159,6 +159,8 @@ menuIconAsTopics: icon {{ "menu/mode_topics", menuIconColor }};
menuIconAsMessages: icon {{ "menu/mode_messages", menuIconColor }}; menuIconAsMessages: icon {{ "menu/mode_messages", menuIconColor }};
menuIconTagFilter: icon{{ "menu/tag_filter", menuIconColor }}; menuIconTagFilter: icon{{ "menu/tag_filter", menuIconColor }};
menuIconTagRename: icon{{ "menu/tag_rename", 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 }}; menuIconGroupsHide: icon {{ "menu/hide_members", menuIconColor }};
menuIconFont: icon {{ "menu/fonts", menuIconColor }}; menuIconFont: icon {{ "menu/fonts", menuIconColor }};
menuIconFactcheck: icon {{ "menu/factcheck", menuIconColor }}; menuIconFactcheck: icon {{ "menu/factcheck", menuIconColor }};