Allow sending premium / star gifts.

This commit is contained in:
John Preston 2024-09-17 18:59:24 +04:00
parent 71deef61f5
commit 0d0d0ab994
13 changed files with 407 additions and 109 deletions

View file

@ -1853,11 +1853,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_proximity_distance_km#other" = "{count} km"; "lng_action_proximity_distance_km#other" = "{count} km";
"lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot."; "lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot.";
"lng_action_gift_received" = "{user} sent you a gift for {cost}"; "lng_action_gift_received" = "{user} sent you a gift for {cost}";
"lng_action_gift_received_me" = "You sent to {user} a gift for {cost}"; "lng_action_gift_sent" = "You sent a gift for {cost}";
"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}"; "lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
"lng_action_gift_anonymous" = "Anonymous Gift"; "lng_action_gift_for_stars#one" = "{count} Star";
"lng_action_gift_for_stars#other" = "{count} Stars";
"lng_action_gift_got_subtitle" = "Gift from {user}"; "lng_action_gift_got_subtitle" = "Gift from {user}";
"lng_action_gift_got_stars_text" = "Display this gift on your page or convert it to {cost}."; "lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star.";
"lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars.";
"lng_action_gift_sent_subtitle" = "Gift for {user}";
"lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star.";
"lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars.";
"lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile."; "lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile.";
"lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile."; "lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile.";
"lng_action_suggested_photo_button" = "View Photo"; "lng_action_suggested_photo_button" = "View Photo";

View file

@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/gift_credits_box.h" #include "boxes/gift_credits_box.h"
#include "base/random.h"
#include "api/api_credits.h" #include "api/api_credits.h"
#include "api/api_premium.h" #include "api/api_premium.h"
#include "boxes/peer_list_controllers.h" #include "boxes/peer_list_controllers.h"
#include "boxes/send_credits_box.h" #include "boxes/send_credits_box.h"
#include "chat_helpers/stickers_gift_box_pack.h" #include "chat_helpers/stickers_gift_box_pack.h"
#include "chat_helpers/stickers_lottie.h" #include "chat_helpers/stickers_lottie.h"
#include "core/click_handler_types.h"
#include "core/local_url_handlers.h"
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -27,6 +30,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/session/session_show.h" #include "main/session/session_show.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "payments/payments_form.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/settings_credits_graphics.h" #include "settings/settings_credits_graphics.h"
#include "settings/settings_credits.h" #include "settings/settings_credits.h"
#include "settings/settings_premium.h" #include "settings/settings_premium.h"
@ -38,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/premium_stars_colored.h" #include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h" #include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/text/format_values.h" #include "ui/text/format_values.h"
@ -83,6 +90,11 @@ struct GiftTypePremium {
const GiftTypePremium &) = default; const GiftTypePremium &) = default;
}; };
struct PremiumGiftsDescriptor {
std::vector<GiftTypePremium> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
struct GiftTypeStars { struct GiftTypeStars {
uint64 id = 0; uint64 id = 0;
int64 stars = 0; int64 stars = 0;
@ -102,6 +114,11 @@ struct GiftDescriptor : std::variant<GiftTypePremium, GiftTypeStars> {
const GiftDescriptor&) = default; const GiftDescriptor&) = default;
}; };
struct GiftsDescriptor {
std::vector<GiftDescriptor> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
struct GiftDetails { struct GiftDetails {
GiftDescriptor descriptor; GiftDescriptor descriptor;
QString text; QString text;
@ -174,6 +191,8 @@ auto GenerateGiftMedia(
Element *replacing, Element *replacing,
const GiftDetails &data) const GiftDetails &data)
-> Fn<void(Fn<void(std::unique_ptr<MediaGenericPart>)>)> { -> Fn<void(Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
Expects(v::is<GiftTypeStars>(data.descriptor));
return [=](Fn<void(std::unique_ptr<MediaGenericPart>)> push) { return [=](Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto &descriptor = data.descriptor; const auto &descriptor = data.descriptor;
auto pushText = [&]( auto pushText = [&](
@ -212,27 +231,15 @@ auto GenerateGiftMedia(
replacing, replacing,
sticker, sticker,
st::giftBoxPreviewStickerPadding)); st::giftBoxPreviewStickerPadding));
const auto title = data.anonymous const auto title = tr::lng_action_gift_got_subtitle(
? tr::lng_action_gift_anonymous(tr::now) tr::now,
: tr::lng_action_gift_got_subtitle( lt_user,
tr::now, parent->data()->history()->session().user()->shortName());
lt_user, auto textFallback = tr::lng_action_gift_got_stars_text(
parent->data()->history()->session().user()->shortName()); tr::now,
auto textFallback = v::match(descriptor, [&](GiftTypePremium data) { lt_count,
return TextWithEntities{ v::get<GiftTypeStars>(descriptor).stars,
u"Use all those premium features with joy!"_q Ui::Text::RichLangValue);
};
}, [&](GiftTypeStars data) {
return tr::lng_action_gift_got_stars_text(
tr::now,
lt_cost,
tr::lng_gift_stars_title(
tr::now,
lt_count,
data.stars,
Ui::Text::Bold),
Ui::Text::WithEntities);
});
auto description = data.text.isEmpty() auto description = data.text.isEmpty()
? std::move(textFallback) ? std::move(textFallback)
: TextWithEntities{ data.text }; : TextWithEntities{ data.text };
@ -284,14 +291,12 @@ void PreviewWrap::prepare(rpl::producer<GiftDetails> details) {
}, [&](GiftTypeStars data) { }, [&](GiftTypeStars data) {
return tr::lng_gift_stars_title(tr::now, lt_count, data.stars); return tr::lng_gift_stars_title(tr::now, lt_count, data.stars);
}); });
const auto text = details.anonymous const auto text = tr::lng_action_gift_received(
? tr::lng_action_gift_received_anonymous(tr::now, lt_cost, cost) tr::now,
: tr::lng_action_gift_received( lt_user,
tr::now, _history->session().user()->shortName(),
lt_user, lt_cost,
_history->session().user()->shortName(), cost);
lt_cost,
cost);
const auto item = _history->makeMessage({ const auto item = _history->makeMessage({
.id = _history->nextNonHistoryEntryId(), .id = _history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeAboutView .flags = (MessageFlag::FakeAboutView
@ -355,11 +360,11 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
_item->draw(p, context); _item->draw(p, context);
} }
[[nodiscard]] rpl::producer<std::vector<GiftTypePremium>> GiftsPremium( [[nodiscard]] rpl::producer<PremiumGiftsDescriptor> GiftsPremium(
not_null<Main::Session*> session, not_null<Main::Session*> session,
not_null<PeerData*> peer) { not_null<PeerData*> peer) {
struct Session { struct Session {
std::vector<GiftTypePremium> last; PremiumGiftsDescriptor last;
}; };
static auto Map = base::flat_map<not_null<Main::Session*>, Session>(); static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
return [=](auto consumer) { return [=](auto consumer) {
@ -370,12 +375,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
i = Map.emplace(session, Session()).first; i = Map.emplace(session, Session()).first;
session->lifetime().add([=] { Map.remove(session); }); session->lifetime().add([=] { Map.remove(session); });
} }
if (!i->second.last.empty()) { if (!i->second.last.list.empty()) {
consumer.put_next_copy(i->second.last); consumer.put_next_copy(i->second.last);
} }
using namespace Api; using namespace Api;
const auto api = lifetime.make_state<PremiumGiftCodeOptions>(peer); const auto api = std::make_shared<PremiumGiftCodeOptions>(peer);
api->request() | rpl::start_with_error_done([=](QString error) { api->request() | rpl::start_with_error_done([=](QString error) {
consumer.put_next({}); consumer.put_next({});
}, [=] { }, [=] {
@ -411,9 +416,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
} }
ranges::sort(list, ranges::less(), &GiftTypePremium::months); ranges::sort(list, ranges::less(), &GiftTypePremium::months);
auto &map = Map[session]; auto &map = Map[session];
if (map.last != list) { if (map.last.list != list) {
map.last = list; map.last = PremiumGiftsDescriptor{
consumer.put_next_copy(list); std::move(list),
api,
};
consumer.put_next_copy(map.last);
} }
}, lifetime); }, lifetime);
@ -923,6 +931,9 @@ void SendGiftBox(
not_null<Window::SessionController*> window, not_null<Window::SessionController*> window,
not_null<PeerData*> peer, not_null<PeerData*> peer,
const GiftDescriptor &descriptor) { const GiftDescriptor &descriptor) {
Expects(v::is<GiftTypeStars>(descriptor));
const auto gift = v::get<GiftTypeStars>(descriptor);
box->setStyle(st::giftBox); box->setStyle(st::giftBox);
box->setWidth(st::boxWideWidth); box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_gift_send_title()); box->setTitle(tr::lng_gift_send_title());
@ -949,21 +960,11 @@ void SendGiftBox(
Lang::FormatCountDecimal(std::abs(data.stars))); Lang::FormatCountDecimal(std::abs(data.stars)));
}); });
}()); }());
const auto button = box->addButton(rpl::single(QString()), [=] {
box->closeBox();
});
SetButtonMarkedLabel(
button,
tr::lng_gift_send_button(
lt_cost,
std::move(cost),
Ui::Text::WithEntities),
session,
st::creditsBoxButtonLabel,
st::giftBox.button.textFg->c);
struct State { struct State {
rpl::variable<GiftDetails> details; rpl::variable<GiftDetails> details;
uint64 randomId = 0;
bool submitting = false;
}; };
const auto state = box->lifetime().make_state<State>(); const auto state = box->lifetime().make_state<State>();
state->details = GiftDetails{ state->details = GiftDetails{
@ -1010,6 +1011,33 @@ void SendGiftBox(
const auto buttonWidth = st::boxWideWidth const auto buttonWidth = st::boxWideWidth
- st::giftBox.buttonPadding.left() - st::giftBox.buttonPadding.left()
- st::giftBox.buttonPadding.right(); - st::giftBox.buttonPadding.right();
const auto button = box->addButton(rpl::single(QString()), [=] {
if (state->submitting) {
return;
}
state->submitting = true;
state->randomId = base::RandomValue<uint64>();
const auto details = state->details.current();
const auto done = [=](Payments::CheckoutResult result) {
box->closeBox();
};
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.id,
.randomId = state->randomId,
.message = { details.text },
.user = peer->asUser(),
.anonymous = details.anonymous,
}, done, Payments::ProcessNonPanelPaymentFormFactory(window, done));
});
SetButtonMarkedLabel(
button,
tr::lng_gift_send_button(
lt_cost,
std::move(cost),
Ui::Text::WithEntities),
session,
st::creditsBoxButtonLabel,
st::giftBox.button.textFg->c);
button->resizeToWidth(buttonWidth); button->resizeToWidth(buttonWidth);
button->widthValue() | rpl::start_with_next([=](int width) { button->widthValue() | rpl::start_with_next([=](int width) {
if (width != buttonWidth) { if (width != buttonWidth) {
@ -1018,10 +1046,23 @@ void SendGiftBox(
}, button->lifetime()); }, button->lifetime());
} }
void SendPremiumGift(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftTypePremium &gift,
Fn<void(Payments::CheckoutResult)> done) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
{ peer->asUser() }
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}
[[nodiscard]] object_ptr<RpWidget> MakeGiftsList( [[nodiscard]] object_ptr<RpWidget> MakeGiftsList(
not_null<Window::SessionController*> window, not_null<Window::SessionController*> window,
not_null<PeerData*> peer, not_null<PeerData*> peer,
rpl::producer<std::vector<GiftDescriptor>> gifts) { rpl::producer<GiftsDescriptor> gifts) {
auto result = object_ptr<RpWidget>((QWidget*)nullptr); auto result = object_ptr<RpWidget>((QWidget*)nullptr);
const auto raw = result.data(); const auto raw = result.data();
@ -1062,7 +1103,7 @@ void SendGiftBox(
return _bg; return _bg;
} }
DocumentData *lookupSticker( DocumentData *lookupSticker(
const GiftDescriptor &descriptor) { const GiftDescriptor &descriptor) override {
const auto &session = _window->session(); const auto &session = _window->session();
auto &packs = session.giftBoxStickersPacks(); auto &packs = session.giftBoxStickersPacks();
packs.load(); packs.load();
@ -1086,6 +1127,7 @@ void SendGiftBox(
struct State { struct State {
Delegate delegate; Delegate delegate;
std::vector<std::unique_ptr<GiftButton>> buttons; std::vector<std::unique_ptr<GiftButton>> buttons;
bool sending = false;
}; };
const auto state = raw->lifetime().make_state<State>(State{ const auto state = raw->lifetime().make_state<State>(State{
.delegate = Delegate(window, peer), .delegate = Delegate(window, peer),
@ -1136,23 +1178,24 @@ void SendGiftBox(
state->delegate.setBackground(std::move(bg)); state->delegate.setBackground(std::move(bg));
std::move( std::move(
gifts gifts
) | rpl::start_with_next([=](const std::vector<GiftDescriptor> &gifts) { ) | rpl::start_with_next([=](const GiftsDescriptor &gifts) {
const auto width = st::boxWideWidth; const auto width = st::boxWideWidth;
const auto padding = st::giftBoxPadding; const auto padding = st::giftBoxPadding;
const auto available = width - padding.left() - padding.right(); const auto available = width - padding.left() - padding.right();
auto x = padding.left(); auto x = padding.left();
auto y = padding.top(); auto y = padding.top();
state->buttons.resize(gifts.size()); state->buttons.resize(gifts.list.size());
for (auto &button : state->buttons) { for (auto &button : state->buttons) {
if (!button) { if (!button) {
button = std::make_unique<GiftButton>(raw, &state->delegate); button = std::make_unique<GiftButton>(raw, &state->delegate);
button->show(); button->show();
} }
} }
for (auto i = 0, count = int(gifts.size()); i != count; ++i) { const auto api = gifts.api;
for (auto i = 0, count = int(gifts.list.size()); i != count; ++i) {
const auto button = state->buttons[i].get(); const auto button = state->buttons[i].get();
const auto &descriptor = gifts[i]; const auto &descriptor = gifts.list[i];
button->setDescriptor(descriptor); button->setDescriptor(descriptor);
const auto last = !((i + 1) % kGiftsPerRow); const auto last = !((i + 1) % kGiftsPerRow);
@ -1167,16 +1210,36 @@ void SendGiftBox(
x += single.width() + st::giftBoxGiftSkip.x(); x += single.width() + st::giftBoxGiftSkip.x();
} }
const auto premiumSent = [=](Payments::CheckoutResult result) {
state->sending = false;
if (result == Payments::CheckoutResult::Paid) {
window->showToast(u"Sent!"_q);
}
};
button->setClickedCallback([=] { button->setClickedCallback([=] {
window->show(Box(SendGiftBox, window, peer, descriptor)); if (v::is<GiftTypePremium>(descriptor)) {
if (state->sending) {
return;
} else {
state->sending = true;
}
SendPremiumGift(
window,
peer,
api,
v::get<GiftTypePremium>(descriptor),
premiumSent);
} else {
window->show(Box(SendGiftBox, window, peer, descriptor));
}
}); });
} }
if (gifts.size() % kGiftsPerRow) { if (gifts.list.size() % kGiftsPerRow) {
y += padding.bottom() + single.height(); y += padding.bottom() + single.height();
} else { } else {
y += padding.bottom() - st::giftBoxGiftSkip.y(); y += padding.bottom() - st::giftBoxGiftSkip.y();
} }
raw->resize(raw->width(), gifts.empty() ? 0 : y); raw->resize(raw->width(), gifts.list.empty() ? 0 : y);
}, raw->lifetime()); }, raw->lifetime());
return result; return result;
@ -1228,16 +1291,20 @@ void AddBlock(
not_null<Window::SessionController*> window, not_null<Window::SessionController*> window,
not_null<PeerData*> peer) { not_null<PeerData*> peer) {
struct State { struct State {
rpl::variable<std::vector<GiftDescriptor>> gifts; rpl::variable<PremiumGiftsDescriptor> gifts;
}; };
auto state = std::make_unique<State>(); auto state = std::make_unique<State>();
state->gifts = GiftsPremium(&window->session(), peer) | rpl::map([=]( state->gifts = GiftsPremium(&window->session(), peer);
const std::vector<GiftTypePremium> &gifts) { ;
return gifts | ranges::to<std::vector<GiftDescriptor>>;
});
auto result = MakeGiftsList(window, peer, state->gifts.value()); auto result = MakeGiftsList(window, peer, state->gifts.value(
) | rpl::map([=](const PremiumGiftsDescriptor &gifts) {
return GiftsDescriptor{
gifts.list | ranges::to<std::vector<GiftDescriptor>>,
gifts.api,
};
}));
result->lifetime().add([state = std::move(state)] {}); result->lifetime().add([state = std::move(state)] {});
return result; return result;
} }
@ -1267,7 +1334,9 @@ void AddBlock(
? (!gift.limited) ? (!gift.limited)
: (price && gift.stars != price); : (price && gift.stars != price);
}), end(gifts)); }), end(gifts));
return gifts | ranges::to<std::vector<GiftDescriptor>>(); return GiftsDescriptor{
gifts | ranges::to<std::vector<GiftDescriptor>>(),
};
}))); })));
return result; return result;

View file

@ -440,4 +440,22 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
}, st, textFg); }, st, textFg);
} }
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) {
session->api().request(MTPpayments_SendStarsForm(
MTP_long(data->formId),
data->inputInvoice
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
done(std::nullopt);
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
} // namespace Ui } // namespace Ui

View file

@ -52,4 +52,9 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
const style::FlatLabel &st, const style::FlatLabel &st,
std::optional<QColor> textFg = {}); std::optional<QColor> textFg = {});
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done);
} // namespace Ui } // namespace Ui

View file

@ -130,16 +130,24 @@ struct GiveawayResults {
enum class GiftType : uchar { enum class GiftType : uchar {
Premium, // count - months Premium, // count - months
Credits, // count - credits Credits, // count - credits
StarGift, // count - stars
}; };
struct GiftCode { struct GiftCode {
QString slug; QString slug;
DocumentData *document = nullptr;
TextWithEntities message;
ChannelData *channel = nullptr; ChannelData *channel = nullptr;
MsgId giveawayMsgId = 0;
int convertStars = 0;
int limitedCount = 0;
int count = 0; int count = 0;
int giveawayMsgId = 0;
GiftType type = GiftType::Premium; GiftType type = GiftType::Premium;
bool viaGiveaway = false; bool viaGiveaway : 1 = false;
bool unclaimed = false; bool unclaimed : 1 = false;
bool anonymous : 1 = false;
bool converted : 1 = false;
bool saved : 1 = false;
}; };
class Media { class Media {

View file

@ -4917,12 +4917,15 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
Ui::Text::WithEntities); Ui::Text::WithEntities);
} else { } else {
result.links.push_back(peer->createOpenLink()); result.links.push_back(peer->createOpenLink());
result.text = (isSelf result.text = isSelf
? tr::lng_action_gift_received_me ? tr::lng_action_gift_sent(tr::now,
: tr::lng_action_gift_received)( lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now, tr::now,
lt_user, lt_user,
Ui::Text::Link(peer->name(), 1), // Link 1. Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost, lt_cost,
cost, cost,
Ui::Text::WithEntities); Ui::Text::WithEntities);
@ -5130,18 +5133,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
} else { } else {
const auto isSelf = (_from->id == _from->session().userPeerId()); const auto isSelf = (_from->id == _from->session().userPeerId());
const auto peer = isSelf ? _history->peer : _from; const auto peer = isSelf ? _history->peer : _from;
const auto cost = AmountAndStarCurrency(
&_history->session(),
action.vamount().value_or_empty(),
qs(action.vcurrency().value_or_empty()));
result.links.push_back(peer->createOpenLink()); result.links.push_back(peer->createOpenLink());
result.text = (isSelf result.text = isSelf
? tr::lng_action_gift_received_me ? tr::lng_action_gift_sent(tr::now,
: tr::lng_action_gift_received)( lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now, tr::now,
lt_user, lt_user,
Ui::Text::Link(peer->name(), 1), // Link 1. Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost, lt_cost,
AmountAndStarCurrency( cost,
&_history->session(),
action.vamount().value_or_empty(),
qs(action.vcurrency().value_or_empty())),
Ui::Text::WithEntities); Ui::Text::WithEntities);
} }
@ -5238,18 +5245,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
_history->session().giftBoxStickersPacks().load(); _history->session().giftBoxStickersPacks().load();
const auto amount = action.vamount().v; const auto amount = action.vamount().v;
const auto currency = qs(action.vcurrency()); const auto currency = qs(action.vcurrency());
const auto cost = AmountAndStarCurrency(
&_history->session(),
amount,
currency);
result.links.push_back(peer->createOpenLink()); result.links.push_back(peer->createOpenLink());
result.text = (isSelf result.text = isSelf
? tr::lng_action_gift_received_me ? tr::lng_action_gift_sent(tr::now,
: tr::lng_action_gift_received)( lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now, tr::now,
lt_user, lt_user,
Ui::Text::Link(peer->name(), 1), // Link 1. Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost, lt_cost,
AmountAndStarCurrency( cost,
&_history->session(),
amount,
currency),
Ui::Text::WithEntities); Ui::Text::WithEntities);
return result; return result;
}; };
@ -5273,6 +5284,34 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
auto prepareStarGift = [&]( auto prepareStarGift = [&](
const MTPDmessageActionStarGift &action) { const MTPDmessageActionStarGift &action) {
auto result = PreparedServiceText(); auto result = PreparedServiceText();
const auto isSelf = _from->isSelf();
const auto peer = isSelf ? _history->peer : _from;
const auto stars = action.vgift().data().vstars().v;
const auto cost = TextWithEntities{
tr::lng_action_gift_for_stars(tr::now, lt_count, stars),
};
const auto anonymous = _from->isServiceUser();
if (anonymous) {
result.text = tr::lng_action_gift_received_anonymous(
tr::now,
lt_cost,
cost,
Ui::Text::WithEntities);
} else {
result.links.push_back(peer->createOpenLink());
result.text = isSelf
? tr::lng_action_gift_sent(tr::now,
lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now,
lt_user,
Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost,
cost,
Ui::Text::WithEntities);
}
return result; return result;
}; };
@ -5428,18 +5467,35 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
.slug = qs(data.vtransaction_id()), .slug = qs(data.vtransaction_id()),
.channel = history()->owner().channel( .channel = history()->owner().channel(
peerToChannel(peerFromMTP(data.vboost_peer()))), peerToChannel(peerFromMTP(data.vboost_peer()))),
.count = int(data.vstars().v),
.giveawayMsgId = data.vgiveaway_msg_id().v, .giveawayMsgId = data.vgiveaway_msg_id().v,
.count = int(data.vstars().v),
.type = Data::GiftType::Credits, .type = Data::GiftType::Credits,
.viaGiveaway = true, .viaGiveaway = true,
.unclaimed = data.is_unclaimed(), .unclaimed = data.is_unclaimed(),
}); });
}, [&](const MTPDmessageActionStarGift &data) { }, [&](const MTPDmessageActionStarGift &data) {
_media = std::make_unique<Data::MediaGiftBox>( const auto &gift = data.vgift().data();
this, const auto document = history()->owner().processDocument(
_from, gift.vsticker());
Data::GiftType::Credits, using Fields = Data::GiftCode;
data.vstars_amount().v); _media = std::make_unique<Data::MediaGiftBox>(this, _from, Fields{
.document = document->sticker() ? document.get() : nullptr,
.message = (data.vmessage()
? TextWithEntities{
.text = qs(data.vmessage()->data().vtext()),
.entities = Api::EntitiesFromMTP(
&history()->session(),
data.vmessage()->data().ventities().v),
}
: TextWithEntities()),
.convertStars = int(data.vconvert_stars().v),
.limitedCount = gift.vavailability_total().value_or_empty(),
.count = int(gift.vstars().v),
.type = Data::GiftType::StarGift,
.anonymous = data.is_name_hidden(),
.converted = data.is_converted(),
.saved = data.is_saved(),
});
}, [](const auto &) { }, [](const auto &) {
}); });
} }

View file

@ -52,7 +52,14 @@ QSize PremiumGift::size() {
} }
QString PremiumGift::title() { QString PremiumGift::title() {
if (creditsPrize()) { if (starGift()) {
return (outgoingGift()
? tr::lng_action_gift_sent_subtitle
: tr::lng_action_gift_got_subtitle)(
tr::now,
lt_user,
_parent->history()->peer->shortName());
} else if (creditsPrize()) {
return tr::lng_prize_title(tr::now); return tr::lng_prize_title(tr::now);
} else if (const auto count = credits()) { } else if (const auto count = credits()) {
return tr::lng_gift_stars_title(tr::now, lt_count, count); return tr::lng_gift_stars_title(tr::now, lt_count, count);
@ -65,6 +72,23 @@ QString PremiumGift::title() {
} }
TextWithEntities PremiumGift::subtitle() { TextWithEntities PremiumGift::subtitle() {
if (starGift()) {
return !_data.message.empty()
? _data.message
: outgoingGift()
? tr::lng_action_gift_sent_text(
tr::now,
lt_count,
_data.count,
lt_user,
Ui::Text::Bold(_parent->history()->peer->shortName()),
Ui::Text::RichLangValue)
: tr::lng_action_gift_got_stars_text(
tr::now,
lt_count,
_data.count,
Ui::Text::RichLangValue);
}
const auto isCreditsPrize = creditsPrize(); const auto isCreditsPrize = creditsPrize();
if (const auto count = credits(); count && !isCreditsPrize) { if (const auto count = credits(); count && !isCreditsPrize) {
return outgoingGift() return outgoingGift()
@ -111,7 +135,9 @@ TextWithEntities PremiumGift::subtitle() {
} }
rpl::producer<QString> PremiumGift::button() { rpl::producer<QString> PremiumGift::button() {
return creditsPrize() return (starGift() && outgoingGift())
? nullptr
: creditsPrize()
? tr::lng_view_button_giftcode() ? tr::lng_view_button_giftcode()
: (gift() && (outgoingGift() || !_data.unclaimed)) : (gift() && (outgoingGift() || !_data.unclaimed))
? tr::lng_sticker_premium_view() ? tr::lng_sticker_premium_view()
@ -119,6 +145,9 @@ rpl::producer<QString> PremiumGift::button() {
} }
ClickHandlerPtr PremiumGift::createViewLink() { ClickHandlerPtr PremiumGift::createViewLink() {
if (starGift() && outgoingGift()) {
return nullptr;
}
const auto from = _gift->from(); const auto from = _gift->from();
const auto peer = _parent->history()->peer; const auto peer = _parent->history()->peer;
const auto date = _parent->data()->date(); const auto date = _parent->data()->date();
@ -142,7 +171,7 @@ ClickHandlerPtr PremiumGift::createViewLink() {
.barePeerId = data.channel .barePeerId = data.channel
? data.channel->id.value ? data.channel->id.value
: 0, : 0,
.bareGiveawayMsgId = uint64(data.giveawayMsgId), .bareGiveawayMsgId = uint64(data.giveawayMsgId.bare),
.peerType = Type::Peer, .peerType = Type::Peer,
.in = true, .in = true,
}, },
@ -223,6 +252,10 @@ bool PremiumGift::gift() const {
return _data.slug.isEmpty() || !_data.channel; return _data.slug.isEmpty() || !_data.channel;
} }
bool PremiumGift::starGift() const {
return _data.type == Data::GiftType::StarGift;
}
bool PremiumGift::creditsPrize() const { bool PremiumGift::creditsPrize() const {
return _data.viaGiveaway return _data.viaGiveaway
&& (_data.type == Data::GiftType::Credits) && (_data.type == Data::GiftType::Credits)
@ -236,6 +269,14 @@ int PremiumGift::credits() const {
void PremiumGift::ensureStickerCreated() const { void PremiumGift::ensureStickerCreated() const {
if (_sticker) { if (_sticker) {
return; return;
} else if (const auto document = _data.document) {
if (const auto sticker = document->sticker()) {
const auto skipPremiumEffect = false;
_sticker.emplace(_parent, document, skipPremiumEffect, _parent);
_sticker->setDiceIndex(sticker->alt, 1);
_sticker->initSize(st::msgServiceGiftBoxStickerSize);
return;
}
} }
const auto &session = _parent->history()->session(); const auto &session = _parent->history()->session();
auto &packs = session.giftBoxStickersPacks(); auto &packs = session.giftBoxStickersPacks();

View file

@ -48,6 +48,7 @@ public:
private: private:
[[nodiscard]] bool incomingGift() const; [[nodiscard]] bool incomingGift() const;
[[nodiscard]] bool outgoingGift() const; [[nodiscard]] bool outgoingGift() const;
[[nodiscard]] bool starGift() const;
[[nodiscard]] bool gift() const; [[nodiscard]] bool gift() const;
[[nodiscard]] bool creditsPrize() const; [[nodiscard]] bool creditsPrize() const;
[[nodiscard]] int credits() const; [[nodiscard]] int credits() const;

View file

@ -181,6 +181,30 @@ void CheckoutProcess::Start(
j->second->requestActivate(); j->second->requestActivate();
} }
void CheckoutProcess::Start(
InvoiceStarGift giftInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto randomId = giftInvoice.randomId;
auto id = InvoiceId{ std::move(giftInvoice) };
auto &processes = LookupSessionProcesses(SessionFromId(id));
const auto i = processes.byRandomId.find(randomId);
if (i != end(processes.byRandomId)) {
i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess));
return;
}
processes.byRandomId.emplace(
randomId,
std::make_unique<CheckoutProcess>(
std::move(id),
Mode::Payment,
std::move(reactivate),
std::move(nonPanelPaymentFormProcess),
PrivateTag{}));
}
std::optional<PaidInvoice> CheckoutProcess::InvoicePaid( std::optional<PaidInvoice> CheckoutProcess::InvoicePaid(
not_null<const HistoryItem*> item) { not_null<const HistoryItem*> item) {
const auto session = &item->history()->session(); const auto session = &item->history()->session();

View file

@ -38,6 +38,7 @@ class Form;
struct FormUpdate; struct FormUpdate;
struct Error; struct Error;
struct InvoiceCredits; struct InvoiceCredits;
struct InvoiceStarGift;
struct InvoiceId; struct InvoiceId;
struct InvoicePremiumGiftCode; struct InvoicePremiumGiftCode;
struct CreditsFormData; struct CreditsFormData;
@ -91,6 +92,10 @@ public:
static void Start( static void Start(
InvoiceCredits creditsInvoice, InvoiceCredits creditsInvoice,
Fn<void(CheckoutResult)> reactivate); Fn<void(CheckoutResult)> reactivate);
static void Start(
InvoiceStarGift giftInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);
[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid( [[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(
not_null<const HistoryItem*> item); not_null<const HistoryItem*> item);
[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid( [[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(

View file

@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/format_values.h" #include "ui/text/format_values.h"
#include "ui/text/text_entity.h" #include "ui/text/text_entity.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "api/api_text_entities.h"
#include "core/core_cloud_password.h" #include "core/core_cloud_password.h"
#include "window/themes/window_theme.h" #include "window/themes/window_theme.h"
#include "webview/webview_interface.h" #include "webview/webview_interface.h"
@ -120,6 +121,8 @@ not_null<Main::Session*> SessionFromId(const InvoiceId &id) {
return slug->session; return slug->session;
} else if (const auto slug = std::get_if<InvoiceCredits>(&id.value)) { } else if (const auto slug = std::get_if<InvoiceCredits>(&id.value)) {
return slug->session; return slug->session;
} else if (const auto gift = std::get_if<InvoiceStarGift>(&id.value)) {
return &gift->user->session();
} }
const auto &giftCode = v::get<InvoicePremiumGiftCode>(id.value); const auto &giftCode = v::get<InvoicePremiumGiftCode>(id.value);
const auto users = std::get_if<InvoicePremiumGiftCodeUsers>( const auto users = std::get_if<InvoicePremiumGiftCodeUsers>(
@ -376,6 +379,19 @@ MTPInputInvoice Form::inputInvoice() const {
MTP_long(credits->credits), MTP_long(credits->credits),
MTP_string(credits->currency), MTP_string(credits->currency),
MTP_long(credits->amount))); MTP_long(credits->amount)));
} else if (const auto gift = std::get_if<InvoiceStarGift>(&_id.value)) {
using Flag = MTPDinputInvoiceStarGift::Flag;
return MTP_inputInvoiceStarGift(
MTP_flags((gift->anonymous ? Flag::f_hide_name : Flag(0))
| (gift->message.empty() ? Flag(0) : Flag::f_message)),
gift->user->inputUser,
MTP_long(gift->giftId),
MTP_textWithEntities(
MTP_string(gift->message.text),
Api::EntitiesToMTP(
&gift->user->session(),
gift->message.entities,
Api::ConvertOption::SkipLocal)));
} }
const auto &giftCode = v::get<InvoicePremiumGiftCode>(_id.value); const auto &giftCode = v::get<InvoicePremiumGiftCode>(_id.value);
if (giftCode.creditsAmount) { if (giftCode.creditsAmount) {
@ -461,7 +477,31 @@ void Form::requestForm() {
}; };
_updates.fire(CreditsPaymentStarted{ .data = formData }); _updates.fire(CreditsPaymentStarted{ .data = formData });
}, [&](const MTPDpayments_paymentFormStarGift &data) { }, [&](const MTPDpayments_paymentFormStarGift &data) {
// todo pay for star gift. const auto currency = qs(data.vinvoice().data().vcurrency());
const auto &tlPrices = data.vinvoice().data().vprices().v;
const auto amount = tlPrices.empty()
? 0
: tlPrices.front().data().vamount().v;
if (currency != ::Ui::kCreditsCurrency || !amount) {
using Type = Error::Type;
_updates.fire(Error{ Type::Form, u"Bad Stars Form."_q });
return;
}
const auto invoice = InvoiceCredits{
.session = _session,
.randomId = 0,
.credits = amount,
.currency = currency,
.amount = amount,
};
const auto formData = CreditsFormData{
.id = _id,
.formId = data.vform_id().v,
.invoice = invoice,
.inputInvoice = inputInvoice(),
.starGiftForm = true,
};
_updates.fire(CreditsPaymentStarted{ .data = formData });
}); });
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
hideProgress(); hideProgress();

View file

@ -171,12 +171,21 @@ struct InvoiceCredits {
PeerId giftPeerId = PeerId(0); PeerId giftPeerId = PeerId(0);
}; };
struct InvoiceStarGift {
uint64 giftId = 0;
uint64 randomId = 0;
TextWithEntities message;
not_null<UserData*> user;
bool anonymous = false;
};
struct InvoiceId { struct InvoiceId {
std::variant< std::variant<
InvoiceMessage, InvoiceMessage,
InvoiceSlug, InvoiceSlug,
InvoicePremiumGiftCode, InvoicePremiumGiftCode,
InvoiceCredits> value; InvoiceCredits,
InvoiceStarGift> value;
}; };
struct CreditsFormData { struct CreditsFormData {
@ -188,6 +197,7 @@ struct CreditsFormData {
PhotoData *photo = nullptr; PhotoData *photo = nullptr;
InvoiceCredits invoice; InvoiceCredits invoice;
MTPInputInvoice inputInvoice; MTPInputInvoice inputInvoice;
bool starGiftForm = false;
}; };
struct CreditsReceiptData { struct CreditsReceiptData {

View file

@ -40,10 +40,10 @@ bool IsCreditsInvoice(not_null<HistoryItem*> item) {
} }
void ProcessCreditsPayment( void ProcessCreditsPayment(
std::shared_ptr<Main::SessionShow> show, std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks, QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form, std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot) { Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto done = [=](Settings::SmallBalanceResult result) { const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Blocked) { if (result == Settings::SmallBalanceResult::Blocked) {
if (const auto onstack = maybeReturnToBot) { if (const auto onstack = maybeReturnToBot) {
@ -55,6 +55,20 @@ void ProcessCreditsPayment(
onstack(CheckoutResult::Cancelled); onstack(CheckoutResult::Cancelled);
} }
return; return;
} else if (form->starGiftForm) {
const auto done = [=](std::optional<QString> error) {
const auto onstack = maybeReturnToBot;
if (error) {
show->showToast(*error);
if (onstack) {
onstack(CheckoutResult::Failed);
}
} else if (onstack) {
onstack(CheckoutResult::Paid);
}
};
Ui::SendStarGift(&show->session(), form, done);
return;
} }
const auto unsuccessful = std::make_shared<bool>(true); const auto unsuccessful = std::make_shared<bool>(true);
const auto box = show->show(Box( const auto box = show->show(Box(
@ -65,14 +79,16 @@ void ProcessCreditsPayment(
if (const auto widget = fireworks.data()) { if (const auto widget = fireworks.data()) {
Ui::StartFireworks(widget); Ui::StartFireworks(widget);
} }
if (maybeReturnToBot) { if (const auto onstack = maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Paid); onstack(CheckoutResult::Paid);
} }
})); }));
box->boxClosing() | rpl::start_with_next([=] { box->boxClosing() | rpl::start_with_next([=] {
crl::on_main([=] { crl::on_main([=] {
if ((*unsuccessful) && maybeReturnToBot) { if (*unsuccessful) {
maybeReturnToBot(CheckoutResult::Cancelled); if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Cancelled);
}
} }
}); });
}, box->lifetime()); }, box->lifetime());