Allow buying reselling gifts.

This commit is contained in:
John Preston 2025-04-18 18:41:13 +04:00
parent 94e0ac3f54
commit c726bef740
14 changed files with 214 additions and 45 deletions

View file

@ -3447,12 +3447,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_send_limited_left#other" = "{count} left";
"lng_gift_send_button" = "Send a Gift for {cost}";
"lng_gift_send_button_self" = "Buy a Gift for {cost}";
"lng_gift_buy_resale_title" = "Buy {name}";
"lng_gift_buy_resale_button" = "Buy for {cost}";
"lng_gift_buy_resale_confirm" = "Do you want to buy {gift} for {cost} and gift it to {user}?";
"lng_gift_buy_resale_confirm_self" = "Do you want to buy {gift} for {cost}?";
"lng_gift_buy_resale_confirm" = "Do you want to buy {name} for {price} and gift it to {user}?";
"lng_gift_buy_resale_confirm_self" = "Do you want to buy {name} for {price}?";
"lng_gift_sent_title" = "Gift Sent!";
"lng_gift_sent_resale_done" = "{user} has been notified about your gift.";
"lng_gift_sent_resale_done_self" = "{gift} is not yours.";
"lng_gift_sent_resale_done_self" = "{gift} is now yours.";
"lng_gift_sent_about#one" = "You spent **{count}** Star from your balance.";
"lng_gift_sent_about#other" = "You spent **{count}** Stars from your balance.";
"lng_gift_limited_of_one" = "unique";

View file

@ -853,10 +853,10 @@ std::optional<Data::StarGift> FromTL(
? peerFromMTP(*data.vowner_id())
: PeerId()),
.number = data.vnum().v,
.starsForResale = int(data.vresell_stars().value_or_empty()),
.model = *model,
.pattern = *pattern,
}),
.starsResellMin = int64(data.vresell_stars().value_or_empty()),
.document = model->document,
.limitedLeft = (total - data.vavailability_issued().v),
.limitedCount = total,

View file

@ -1336,7 +1336,13 @@ void AddStarGiftTable(
}, tooltip->lifetime());
};
if (unique && entry.bareGiftOwnerId) {
if (unique && entry.bareGiftResaleRecipientId) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer(),
MakePeerTableValue(table, show, PeerId(entry.bareGiftResaleRecipientId)),
st::giveawayGiftCodePeerMargin);
} else if (unique && entry.bareGiftOwnerId) {
const auto ownerId = PeerId(entry.bareGiftOwnerId);
const auto was = std::make_shared<std::optional<CollectibleId>>();
const auto handleChange = [=](

View file

@ -1896,16 +1896,15 @@ void ShowGiftUpgradedToast(
}
void SendStarsFormRequest(
not_null<Window::SessionController*> controller,
std::shared_ptr<Main::SessionShow> show,
Settings::SmallBalanceResult result,
uint64 formId,
MTPInputInvoice invoice,
Fn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {
using BalanceResult = Settings::SmallBalanceResult;
const auto session = &controller->session();
const auto session = &show->session();
if (result == BalanceResult::Success
|| result == BalanceResult::Already) {
const auto weak = base::make_weak(controller);
session->api().request(MTPpayments_SendStarsForm(
MTP_long(formId),
invoice
@ -1917,9 +1916,7 @@ void SendStarsFormRequest(
done(Payments::CheckoutResult::Failed, nullptr);
});
}).fail([=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
strong->showToast(error.type());
}
show->showToast(error.type());
done(Payments::CheckoutResult::Failed, nullptr);
}).send();
} else if (result == BalanceResult::Cancelled) {
@ -1965,7 +1962,7 @@ void UpgradeGift(
}
using Flag = MTPDinputInvoiceStarGiftUpgrade::Flag;
RequestStarsFormAndSubmit(
window,
window->uiShow(),
MTP_inputInvoiceStarGiftUpgrade(
MTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()),
Api::InputSavedStarGiftId(savedId)),
@ -2613,7 +2610,7 @@ void SendGiftBox(
button->setDescriptor(descriptor, GiftButton::Mode::Full);
button->setClickedCallback([=] {
const auto star = std::get_if<GiftTypeStars>(&descriptor);
if (star && star->info.unique) {
if (star && star->info.unique && star->mine) {
const auto done = [=] {
window->session().credits().load(true);
window->showPeerHistory(peer);
@ -2624,7 +2621,14 @@ void SendGiftBox(
star->info.unique,
star->transferId,
done);
} else if (star->resale) {
} else if (star && star->info.unique && star->resale) {
window->show(Box(
Settings::GlobalStarGiftBox,
window->uiShow(),
star->info,
peer->id,
Settings::CreditsEntryBoxStyleOverrides()));
} else if (star && star->resale) {
const auto id = star->info.id;
if (state->resaleRequestingId == id) {
return;
@ -4460,11 +4464,10 @@ void AddUniqueCloseButton(
}
void RequestStarsFormAndSubmit(
not_null<Window::SessionController*> window,
std::shared_ptr<Main::SessionShow> show,
MTPInputInvoice invoice,
Fn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {
const auto weak = base::make_weak(window);
window->session().api().request(MTPpayments_GetPaymentForm(
show->session().api().request(MTPpayments_GetPaymentForm(
MTP_flags(0),
invoice,
MTPDataJSON() // theme_params
@ -4472,16 +4475,15 @@ void RequestStarsFormAndSubmit(
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) {
if (!show->valid()) {
done(Payments::CheckoutResult::Failed, nullptr);
return;
}
const auto ready = [=](Settings::SmallBalanceResult result) {
SendStarsFormRequest(strong, result, formId, invoice, done);
SendStarsFormRequest(show, result, formId, invoice, done);
};
Settings::MaybeRequestBalanceIncrease(
strong->uiShow(),
show,
prices.front().data().vamount().v,
Settings::SmallBalanceDeepLink{},
ready);
@ -4493,31 +4495,45 @@ void RequestStarsFormAndSubmit(
if (type == u"STARGIFT_EXPORT_IN_PROGRESS"_q) {
done(Payments::CheckoutResult::Cancelled, nullptr);
} else {
if (const auto strong = weak.get()) {
strong->showToast(type);
}
show->showToast(type);
done(Payments::CheckoutResult::Failed, nullptr);
}
}).send();
}
void ShowGiftTransferredToast(
base::weak_ptr<Window::SessionController> weak,
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift) {
if (const auto strong = weak.get()) {
strong->showToast({
.title = tr::lng_gift_transferred_title(tr::now),
.text = tr::lng_gift_transferred_about(
tr::now,
show->showToast({
.title = tr::lng_gift_transferred_title(tr::now),
.text = tr::lng_gift_transferred_about(
tr::now,
lt_name,
Text::Bold(Data::UniqueGiftName(gift)),
lt_recipient,
Text::Bold(to->shortName()),
Ui::Text::WithEntities),
.duration = kUpgradeDoneToastDuration,
});
}
.duration = kUpgradeDoneToastDuration,
});
}
void ShowResaleGiftBoughtToast(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift) {
show->showToast({
.title = tr::lng_gift_sent_title(tr::now),
.text = (to->isSelf()
? tr::lng_gift_sent_resale_done_self(
tr::now,
lt_gift,
Data::UniqueGiftName(gift))
: tr::lng_gift_sent_resale_done(
tr::now,
lt_user,
to->shortName())),
.duration = kUpgradeDoneToastDuration,
});
}
} // namespace Ui

View file

@ -19,6 +19,10 @@ struct GiftCode;
struct CreditsHistoryEntry;
} // namespace Data
namespace Main {
class SessionShow;
} // namespace Main
namespace Payments {
enum class CheckoutResult;
} // namespace Payments
@ -101,12 +105,17 @@ void AddUniqueCloseButton(
Fn<void(not_null<PopupMenu*>)> fillMenu = nullptr);
void RequestStarsFormAndSubmit(
not_null<Window::SessionController*> window,
std::shared_ptr<Main::SessionShow> show,
MTPInputInvoice invoice,
Fn<void(Payments::CheckoutResult, const MTPUpdates *)> done);
void ShowGiftTransferredToast(
base::weak_ptr<Window::SessionController> weak,
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift);
void ShowResaleGiftBoughtToast(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift);

View file

@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h" // peerListSingleRow.
#include "styles/style_credits.h" // starIconEmoji.
#include "styles/style_dialogs.h" // recentPeersSpecialName.
#include "styles/style_layers.h" // boxLabel.
@ -431,12 +432,12 @@ void TransferGift(
const MTPUpdates *updates) {
done(result);
if (result == Payments::CheckoutResult::Paid) {
session->data().notifyGiftUpdate({
.id = savedId,
.action = Data::GiftUpdate::Action::Transfer,
});
if (const auto strong = weak.get()) {
strong->session().data().notifyGiftUpdate({
.id = savedId,
.action = Data::GiftUpdate::Action::Transfer,
});
Ui::ShowGiftTransferredToast(strong, to, *gift);
Ui::ShowGiftTransferredToast(strong->uiShow(), to, *gift);
}
}
};
@ -456,13 +457,35 @@ void TransferGift(
return;
}
Ui::RequestStarsFormAndSubmit(
window,
window->uiShow(),
MTP_inputInvoiceStarGiftTransfer(
Api::InputSavedStarGiftId(savedId),
to->input),
std::move(formDone));
}
void BuyResaleGift(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> to,
std::shared_ptr<Data::UniqueGift> gift,
Fn<void(Payments::CheckoutResult)> done) {
Expects(to->isUser());
auto formDone = [=](
Payments::CheckoutResult result,
const MTPUpdates *updates) {
done(result);
if (result == Payments::CheckoutResult::Paid) {
AssertIsDebug(Refresh owners gifts list, refresh self list);
Ui::ShowResaleGiftBoughtToast(show, to, *gift);
}
};
Ui::RequestStarsFormAndSubmit(
show,
MTP_inputInvoiceStarGiftResale(MTP_string(gift->slug), to->input),
std::move(formDone));
}
} // namespace
void ShowTransferToBox(
@ -566,3 +589,73 @@ void ShowTransferGiftBox(
Box<PeerListBox>(std::move(controller), std::move(initBox)),
Ui::LayerOption::KeepOther);
}
void ShowBuyResaleGiftBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> gift,
not_null<PeerData*> to,
Fn<void()> closeParentBox) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_gift_buy_resale_title(
lt_name,
rpl::single(UniqueGiftName(*gift))));
auto transfer = tr::lng_gift_buy_resale_button(
lt_cost,
rpl::single(
Ui::Text::IconEmoji(&st::starIconEmoji).append(
Lang::FormatCountDecimal(gift->starsForResale))),
Ui::Text::WithEntities);
struct State {
bool sent = false;
};
const auto state = std::make_shared<State>();
auto callback = [=](Fn<void()> close) {
if (state->sent) {
return;
}
state->sent = true;
const auto weak = Ui::MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Cancelled) {
closeParentBox();
close();
} else if (result != Payments::CheckoutResult::Paid) {
state->sent = false;
} else {
show->showToast(u"done!"_q);
closeParentBox();
close();
}
};
BuyResaleGift(show, to, gift, done);
};
Ui::ConfirmBox(box, {
.text = to->isSelf()
? tr::lng_gift_buy_resale_confirm_self(
lt_name,
rpl::single(Ui::Text::Bold(UniqueGiftName(*gift))),
lt_price,
tr::lng_action_gift_for_stars(
lt_count,
rpl::single(gift->starsForResale * 1.),
Ui::Text::Bold),
Ui::Text::WithEntities)
: tr::lng_gift_buy_resale_confirm(
lt_name,
rpl::single(Ui::Text::Bold(UniqueGiftName(*gift))),
lt_price,
tr::lng_action_gift_for_stars(
lt_count,
rpl::single(gift->starsForResale * 1.),
Ui::Text::Bold),
lt_user,
rpl::single(Ui::Text::Bold(to->shortName())),
Ui::Text::WithEntities),
.confirmed = std::move(callback),
.confirmText = std::move(transfer),
});
}));
}

View file

@ -11,6 +11,10 @@ namespace Window {
class SessionController;
} // namespace Window
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
struct UniqueGift;
class SavedStarGiftId;
@ -27,3 +31,9 @@ void ShowTransferGiftBox(
not_null<Window::SessionController*> window,
std::shared_ptr<Data::UniqueGift> gift,
Data::SavedStarGiftId savedId);
void ShowBuyResaleGiftBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> gift,
not_null<PeerData*> to,
Fn<void()> closeParentBox);

View file

@ -1920,7 +1920,7 @@ void ResolveAndShowUniqueGift(
session->data().processUsers(data.vusers());
if (const auto gift = Api::FromTL(session, data.vgift())) {
using namespace ::Settings;
show->show(Box(GlobalStarGiftBox, show, *gift, st));
show->show(Box(GlobalStarGiftBox, show, *gift, PeerId(), st));
}
}).fail([=](const MTP::Error &error) {
clear();

View file

@ -65,6 +65,7 @@ struct CreditsHistoryEntry final {
uint64 bareGiveawayMsgId = 0;
uint64 bareGiftStickerId = 0;
uint64 bareGiftOwnerId = 0;
uint64 bareGiftResaleRecipientId = 0;
uint64 bareActorId = 0;
uint64 bareEntryOwnerId = 0;
uint64 giftChannelSavedId = 0;

View file

@ -46,6 +46,7 @@ struct UniqueGift {
PeerId ownerId = 0;
int number = 0;
int starsForTransfer = -1;
int starsForResale = -1;
TimeId exportAt = 0;
UniqueGiftModel model;
UniqueGiftPattern pattern;

View file

@ -145,7 +145,9 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
? (unique
? _delegate->monostar()
: _delegate->star()).append(' ').append(
Lang::FormatCountDecimal(data.info.starsResellMin)
Lang::FormatCountDecimal(unique
? unique->starsForResale
: data.info.starsResellMin)
).append(data.info.resellCount > 1 ? "+" : "")
: unique
? tr::lng_gift_transfer_button(

View file

@ -1923,6 +1923,16 @@ void GenericCreditsEntryBox(
if (willBusy) {
state->confirmButtonBusy = true;
send();
} else if (uniqueGift && uniqueGift->starsForResale && !giftToSelf) {
const auto to = e.bareGiftResaleRecipientId
? show->session().data().peer(
PeerId(e.bareGiftResaleRecipientId))
: show->session().user();
ShowBuyResaleGiftBox(
show,
e.uniqueGift,
to,
crl::guard(box, [=] { box->closeBox(); }));
} else if (canUpgradeFree) {
upgrade();
} else if (canToggle && !e.savedToProfile) {
@ -1931,6 +1941,14 @@ void GenericCreditsEntryBox(
box->closeBox();
}
});
if (uniqueGift && uniqueGift->starsForResale && !giftToSelf) {
button->setText(tr::lng_gift_buy_resale_button(
lt_cost,
rpl::single(
Ui::Text::IconEmoji(&st::starIconEmoji).append(
Lang::FormatCountDecimal(uniqueGift->starsForResale))),
Ui::Text::WithEntities));
}
{
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
@ -2012,6 +2030,7 @@ void GlobalStarGiftBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
const Data::StarGift &data,
PeerId resaleRecipientId,
CreditsEntryBoxStyleOverrides st) {
const auto ownerId = data.unique ? data.unique->ownerId.value : 0;
Settings::GenericCreditsEntryBox(
@ -2021,6 +2040,7 @@ void GlobalStarGiftBox(
.credits = StarsAmount(data.stars),
.bareGiftStickerId = data.document->id,
.bareGiftOwnerId = ownerId,
.bareGiftResaleRecipientId = resaleRecipientId.value,
.stargiftId = data.id,
.uniqueGift = data.unique,
.peerType = Data::CreditsHistoryEntry::PeerType::Peer,

View file

@ -154,6 +154,7 @@ void GlobalStarGiftBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
const Data::StarGift &data,
PeerId resaleRecipientId,
CreditsEntryBoxStyleOverrides st = {});
[[nodiscard]] Data::CreditsHistoryEntry SavedStarGiftEntry(

View file

@ -58,14 +58,23 @@ void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {
};
const auto &defaultButtonStyle = box->getDelegate()->style().button;
const auto confirmTextPlain = v::text::is_plain(args.confirmText);
const auto confirmButton = box->addButton(
v::text::take_plain(std::move(args.confirmText), tr::lng_box_ok()),
(confirmTextPlain
? v::text::take_plain(
std::move(args.confirmText),
tr::lng_box_ok())
: rpl::single(QString())),
[=, c = prepareCallback(args.confirmed)]() {
lifetime->destroy();
c();
},
args.confirmStyle ? *args.confirmStyle : defaultButtonStyle);
if (!confirmTextPlain) {
confirmButton->setText(
v::text::take_marked(std::move(args.confirmText)));
}
box->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
if ((e->type() != QEvent::KeyPress) || !confirmButton) {