Generalize gift sending.

This commit is contained in:
John Preston 2024-10-02 14:43:03 +04:00
parent 42dd08ace5
commit 0282786b4c
11 changed files with 168 additions and 104 deletions

View file

@ -1868,6 +1868,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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_gift_premium_months#one" = "{count} Month Premium";
"lng_action_gift_premium_months#other" = "{count} Months Premium";
"lng_action_gift_premium_about" = "Subscription for exclusive Telegram features.";
"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_button" = "View Photo";
@ -3014,6 +3017,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_send_message" = "Enter Message";
"lng_gift_send_anonymous" = "Hide My Name";
"lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.";
"lng_gift_send_premium_about" = "Only {user} will see your message.";
"lng_gift_send_button" = "Send a Gift for {cost}";
"lng_gift_sent_title" = "Gift Sent!";
"lng_gift_sent_about#one" = "You spent **{count}** Star from your balance.";

View file

@ -92,6 +92,7 @@ struct GiftsDescriptor {
struct GiftDetails {
GiftDescriptor descriptor;
TextWithEntities text;
uint64 randomId = 0;
bool anonymous = false;
};
@ -161,8 +162,6 @@ auto GenerateGiftMedia(
Element *replacing,
const GiftDetails &data)
-> Fn<void(Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
Expects(v::is<GiftTypeStars>(data.descriptor));
return [=](Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto &descriptor = data.descriptor;
auto pushText = [&](
@ -181,16 +180,8 @@ auto GenerateGiftMedia(
};
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
const auto &session = parent->history()->session();
auto &packs = session.giftBoxStickersPacks();
packs.load();
auto sticker = v::match(descriptor, [&](GiftTypePremium data) {
return packs.lookup(data.months);
}, [&](GiftTypeStars data) {
return data.document
? data.document
: packs.lookup(packs.monthsForStars(data.stars));
});
const auto session = &parent->history()->session();
const auto sticker = LookupGiftSticker(session, descriptor);
return StickerInBubblePart::Data{
.sticker = sticker,
.size = st::chatIntroStickerSize,
@ -203,15 +194,28 @@ auto GenerateGiftMedia(
replacing,
sticker,
st::giftBoxPreviewStickerPadding));
const auto title = tr::lng_action_gift_got_subtitle(
tr::now,
lt_user,
parent->data()->history()->session().user()->shortName());
auto textFallback = tr::lng_action_gift_got_stars_text(
tr::now,
lt_count,
v::get<GiftTypeStars>(descriptor).convertStars,
Ui::Text::RichLangValue);
const auto title = v::match(descriptor, [&](GiftTypePremium gift) {
return tr::lng_action_gift_premium_months(
tr::now,
lt_count,
gift.months);
}, [&](const GiftTypeStars &gift) {
return tr::lng_action_gift_got_subtitle(
tr::now,
lt_user,
parent->history()->session().user()->shortName());
});
auto textFallback = v::match(descriptor, [&](GiftTypePremium gift) {
return tr::lng_action_gift_premium_about(
tr::now,
Ui::Text::RichLangValue);
}, [&](const GiftTypeStars &gift) {
return tr::lng_action_gift_got_stars_text(
tr::now,
lt_count,
gift.convertStars,
Ui::Text::RichLangValue);
});
auto description = data.text.empty()
? std::move(textFallback)
: data.text;
@ -221,7 +225,7 @@ auto GenerateGiftMedia(
st::giftBoxPreviewTextPadding,
{},
Core::MarkedTextContext{
.session = &parent->data()->history()->session(),
.session = &parent->history()->session(),
.customEmojiRepaint = [parent] { parent->repaint(); },
});
};
@ -260,24 +264,34 @@ PreviewWrap::PreviewWrap(
void ShowSentToast(
not_null<Window::SessionController*> window,
GiftTypeStars gift) {
const GiftDescriptor &descriptor) {
const auto &st = st::historyPremiumToast;
const auto skip = st.padding.top();
const auto size = st.style.font->height * 2;
const auto leftSkip = skip + size + skip - st.padding.left();
const auto strong = window->showToast({
.title = tr::lng_gift_sent_title(tr::now),
.text = tr::lng_gift_sent_about(
const auto document = LookupGiftSticker(&window->session(), descriptor);
const auto leftSkip = document
? (skip + size + skip - st.padding.left())
: 0;
auto text = v::match(descriptor, [&](const GiftTypePremium &gift) {
return tr::lng_action_gift_premium_about(
tr::now,
Ui::Text::RichLangValue);
}, [&](const GiftTypeStars &gift) {
return tr::lng_gift_sent_about(
tr::now,
lt_count,
gift.stars,
Ui::Text::RichLangValue),
Ui::Text::RichLangValue);
});
const auto strong = window->showToast({
.title = tr::lng_gift_sent_title(tr::now),
.text = std::move(text),
.padding = rpl::single(QMargins(leftSkip, 0, 0, 0)),
.st = &st,
.attach = RectPart::Top,
.duration = kSentToastDuration,
}).get();
if (!strong) {
if (!strong || !document) {
return;
}
const auto widget = strong->widget();
@ -286,7 +300,6 @@ void ShowSentToast(
preview->resize(size, size);
preview->show();
const auto document = gift.document;
const auto bytes = document->createMediaView()->bytes();
const auto filepath = document->filepath();
const auto ratio = style::DevicePixelRatio();
@ -807,14 +820,38 @@ struct GiftPriceTabs {
return field;
}
void SendGift(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDetails &details,
Fn<void(Payments::CheckoutResult)> done) {
v::match(details.descriptor, [&](const GiftTypePremium &gift) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}, [&](const GiftTypeStars &gift) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.id,
.randomId = details.randomId,
.message = details.text,
.user = peer->asUser(),
.anonymous = details.anonymous,
}, done, processNonPanelPaymentFormFactory);
});
}
void SendGiftBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDescriptor &descriptor) {
Expects(v::is<GiftTypeStars>(descriptor));
const auto gift = v::get<GiftTypeStars>(descriptor);
box->setStyle(st::giftBox);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_gift_send_title());
@ -841,15 +878,17 @@ void SendGiftBox(
struct State {
rpl::variable<GiftDetails> details;
std::shared_ptr<Data::DocumentMedia> media;
uint64 randomId = 0;
bool submitting = false;
};
const auto state = box->lifetime().make_state<State>();
state->details = GiftDetails{
.descriptor = descriptor,
.randomId = base::RandomValue<uint64>(),
};
state->media = gift.document->createMediaView();
state->media->checkStickerLarge();
const auto document = LookupGiftSticker(&window->session(), descriptor);
if ((state->media = document ? document->createMediaView() : nullptr)) {
state->media->checkStickerLarge();
}
const auto container = box->verticalLayout();
container->add(object_ptr<PreviewWrap>(
@ -901,25 +940,33 @@ void SendGiftBox(
&window->session(),
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
AddDivider(container);
AddSkip(container);
container->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_gift_send_anonymous(),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
auto now = state->details.current();
now.anonymous = toggled;
state->details = std::move(now);
}, container->lifetime());
AddSkip(container);
AddDividerText(container, tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())));
if (v::is<GiftTypeStars>(descriptor)) {
AddDivider(container);
AddSkip(container);
container->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_gift_send_anonymous(),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
auto now = state->details.current();
now.anonymous = toggled;
state->details = std::move(now);
}, container->lifetime());
AddSkip(container);
}
v::match(descriptor, [&](const GiftTypePremium &) {
AddDividerText(container, tr::lng_gift_send_premium_about(
lt_user,
rpl::single(peer->shortName())));
}, [&](const GiftTypeStars &) {
AddDividerText(container, tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())));
});
const auto buttonWidth = st::boxWideWidth
- st::giftBox.buttonPadding.left()
@ -929,26 +976,19 @@ void SendGiftBox(
return;
}
state->submitting = true;
state->randomId = base::RandomValue<uint64>();
const auto details = state->details.current();
const auto weak = Ui::MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Paid) {
const auto copy = state->media;
window->showPeerHistory(peer);
ShowSentToast(window, gift);
ShowSentToast(window, descriptor);
}
if (const auto strong = weak.data()) {
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));
SendGift(window, peer, api, details, done);
});
SetButtonMarkedLabel(
button,
@ -967,19 +1007,6 @@ void SendGiftBox(
}, 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(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
@ -999,6 +1026,13 @@ void SendPremiumGift(
const auto shadow = st::defaultDropdownMenu.wrap.shadow;
const auto extend = shadow.extend;
auto &packs = window->session().giftBoxStickersPacks();
packs.updated() | rpl::start_with_next([=] {
for (const auto &button : state->buttons) {
button->update();
}
}, raw->lifetime());
std::move(
gifts
) | rpl::start_with_next([=](const GiftsDescriptor &gifts) {
@ -1049,14 +1083,15 @@ void SendPremiumGift(
} else {
state->sending = true;
}
SendPremiumGift(
SendGift(
window,
peer,
api,
v::get<GiftTypePremium>(descriptor),
GiftDetails{ descriptor },
premiumSent);
} else {
window->show(Box(SendGiftBox, window, peer, descriptor));
window->show(
Box(SendGiftBox, window, peer, api, descriptor));
}
});
}

View file

@ -22,6 +22,10 @@ GiftBoxPack::GiftBoxPack(not_null<Main::Session*> session)
GiftBoxPack::~GiftBoxPack() = default;
rpl::producer<> GiftBoxPack::updated() const {
return _updated.events();
}
int GiftBoxPack::monthsForStars(int stars) const {
if (stars <= 1000) {
return 3;
@ -112,6 +116,7 @@ void GiftBoxPack::applySet(const MTPDmessages_stickerSet &data) {
}
});
}
_updated.fire({});
}
} // namespace Stickers

View file

@ -28,6 +28,7 @@ public:
[[nodiscard]] int monthsForStars(int stars) const;
[[nodiscard]] DocumentData *lookup(int months) const;
[[nodiscard]] Data::FileOrigin origin() const;
[[nodiscard]] rpl::producer<> updated() const;
private:
using SetId = uint64;
@ -36,6 +37,7 @@ private:
const not_null<Main::Session*> _session;
const std::vector<int> _localMonths;
rpl::event_stream<> _updated;
std::vector<DocumentData*> _documents;
SetId _setId = 0;
uint64 _accessHash = 0;

View file

@ -463,10 +463,6 @@ bool UserData::canAddContact() const {
return canShareThisContact() && !isContact();
}
bool UserData::canReceiveGifts() const {
return flags() & UserDataFlag::CanReceiveGifts;
}
bool UserData::canShareThisContactFast() const {
return !_phone.isEmpty();
}
@ -584,14 +580,10 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
if (const auto pinned = update.vpinned_msg_id()) {
SetTopPinnedMessageId(user, pinned->v);
}
const auto canReceiveGifts = (update.vflags().v
& MTPDuserFull::Flag::f_premium_gifts)
&& update.vpremium_gifts();
using Flag = UserDataFlag;
const auto mask = Flag::Blocked
| Flag::HasPhoneCalls
| Flag::PhoneCallsPrivate
| Flag::CanReceiveGifts
| Flag::CanPinMessages
| Flag::VoiceMessagesForbidden
| Flag::ReadDatesPrivate
@ -602,7 +594,6 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
? Flag::PhoneCallsPrivate
: Flag())
| (update.is_phone_calls_available() ? Flag::HasPhoneCalls : Flag())
| (canReceiveGifts ? Flag::CanReceiveGifts : Flag())
| (update.is_can_pin_message() ? Flag::CanPinMessages : Flag())
| (update.is_blocked() ? Flag::Blocked : Flag())
| (update.is_voice_messages_forbidden()

View file

@ -67,7 +67,7 @@ enum class UserDataFlag : uint32 {
DiscardMinPhoto = (1 << 12),
Self = (1 << 13),
Premium = (1 << 14),
CanReceiveGifts = (1 << 15),
//CanReceiveGifts = (1 << 15),
VoiceMessagesForbidden = (1 << 16),
PersonalPhoto = (1 << 17),
StoriesHidden = (1 << 18),
@ -146,8 +146,6 @@ public:
[[nodiscard]] bool canShareThisContact() const;
[[nodiscard]] bool canAddContact() const;
[[nodiscard]] bool canReceiveGifts() const;
// In Data::Session::processUsers() we check only that.
// When actually trying to share contact we perform
// a full check by canShareThisContact() call.

View file

@ -43,7 +43,7 @@ PremiumGift::PremiumGift(
PremiumGift::~PremiumGift() = default;
int PremiumGift::top() {
return st::msgServiceGiftBoxStickerTop;
return starGift() ? 0 : st::msgServiceGiftBoxStickerTop;
}
QSize PremiumGift::size() {
@ -66,7 +66,7 @@ QString PremiumGift::title() {
return tr::lng_gift_stars_title(tr::now, lt_count, count);
}
return gift()
? tr::lng_premium_summary_title(tr::now)
? tr::lng_action_gift_premium_months(tr::now, lt_count, _data.count)
: _data.unclaimed
? tr::lng_prize_unclaimed_title(tr::now)
: tr::lng_prize_title(tr::now);
@ -99,10 +99,14 @@ TextWithEntities PremiumGift::subtitle() {
tr::now,
lt_user,
Ui::Text::Bold(_parent->history()->peer->shortName()),
Ui::Text::WithEntities)
Ui::Text::RichLangValue)
: tr::lng_gift_stars_incoming(tr::now, Ui::Text::WithEntities);
} else if (gift()) {
return { GiftDuration(_data.count) };
return !_data.message.empty()
? _data.message
: tr::lng_action_gift_premium_about(
tr::now,
Ui::Text::RichLangValue);
}
const auto name = _data.channel ? _data.channel->name() : "channel";
auto result = (_data.unclaimed

View file

@ -109,6 +109,10 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor) {
_button = QRect(skipx, skipy, outer, inner.height());
}
bool GiftButton::documentResolved() const {
return _player || _mediaLifetime;
}
void GiftButton::setDocument(not_null<DocumentData*> document) {
const auto media = document->createMediaView();
media->checkStickerLarge();
@ -159,6 +163,12 @@ void GiftButton::resizeEvent(QResizeEvent *e) {
}
void GiftButton::paintEvent(QPaintEvent *e) {
if (!documentResolved()) {
if (const auto document = _delegate->lookupSticker(_descriptor)) {
setDocument(document);
}
}
auto p = QPainter(this);
const auto hidden = v::is<GiftTypeStars>(_descriptor)
&& v::get<GiftTypeStars>(_descriptor).hidden;;
@ -399,8 +409,17 @@ QImage Delegate::background() {
}
DocumentData *Delegate::lookupSticker(const GiftDescriptor &descriptor) {
const auto &session = _window->session();
auto &packs = session.giftBoxStickersPacks();
return LookupGiftSticker(&_window->session(), descriptor);
}
not_null<StickerPremiumMark*> Delegate::hiddenMark() {
return _hiddenMark.get();
}
DocumentData *LookupGiftSticker(
not_null<Main::Session*> session,
const GiftDescriptor &descriptor) {
auto &packs = session->giftBoxStickersPacks();
packs.load();
return v::match(descriptor, [&](GiftTypePremium data) {
return packs.lookup(data.months);
@ -411,8 +430,4 @@ DocumentData *Delegate::lookupSticker(const GiftDescriptor &descriptor) {
});
}
not_null<StickerPremiumMark*> Delegate::hiddenMark() {
return _hiddenMark.get();
}
} // namespace Info::PeerGifts

View file

@ -16,6 +16,10 @@ namespace HistoryView {
class StickerPlayer;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class DynamicImage;
} // namespace Ui
@ -86,6 +90,8 @@ private:
void resizeEvent(QResizeEvent *e) override;
void setDocument(not_null<DocumentData*> document);
[[nodiscard]] bool documentResolved() const;
void unsubscribe();
const not_null<GiftButtonDelegate*> _delegate;
@ -126,4 +132,8 @@ private:
};
[[nodiscard]] DocumentData *LookupGiftSticker(
not_null<Main::Session*> session,
const GiftDescriptor &descriptor);
} // namespace Info::PeerGifts

View file

@ -143,6 +143,7 @@ struct InvoicePremiumGiftCodeGiveaway {
struct InvoicePremiumGiftCodeUsers {
std::vector<not_null<UserData*>> users;
ChannelData *boostPeer = nullptr;
TextWithEntities message;
};
struct InvoicePremiumGiftCode {

View file

@ -1231,7 +1231,6 @@ void Filler::addGiftPremium() {
|| user->isSelf()
|| user->isBot()
|| user->isNotificationsUser()
|| !user->canReceiveGifts()
|| user->isRepliesChat()
|| user->isVerifyCodes()
|| !user->session().premiumCanBuy()) {
@ -1240,7 +1239,7 @@ void Filler::addGiftPremium() {
const auto navigation = _controller;
_addAction(tr::lng_profile_gift_premium(tr::now), [=] {
Ui::ChooseStarGiftRecipient(navigation);
Ui::ShowStarGiftBox(navigation, user);
}, &st::menuIconGiftPremium);
}