diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index d3fc32100..7ee4e538b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -475,6 +475,8 @@ PRIVATE data/business/data_business_info.h data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.h + data/components/credits.cpp + data/components/credits.h data/components/factchecks.cpp data/components/factchecks.h data/components/location_pickers.cpp @@ -1236,6 +1238,8 @@ PRIVATE payments/payments_form.h payments/payments_non_panel_process.cpp payments/payments_non_panel_process.h + payments/payments_reaction_process.cpp + payments/payments_reaction_process.h platform/linux/file_utilities_linux.cpp platform/linux/file_utilities_linux.h platform/linux/launcher_linux.cpp diff --git a/Telegram/Resources/animations/star_reaction/center.tgs b/Telegram/Resources/animations/star_reaction/center.tgs new file mode 100644 index 000000000..61f153854 Binary files /dev/null and b/Telegram/Resources/animations/star_reaction/center.tgs differ diff --git a/Telegram/Resources/animations/star_reaction/effect.tgs b/Telegram/Resources/animations/star_reaction/effect.tgs deleted file mode 100644 index 0b87a225a..000000000 Binary files a/Telegram/Resources/animations/star_reaction/effect.tgs and /dev/null differ diff --git a/Telegram/Resources/animations/star_reaction/effect1.tgs b/Telegram/Resources/animations/star_reaction/effect1.tgs new file mode 100644 index 000000000..f31897de5 Binary files /dev/null and b/Telegram/Resources/animations/star_reaction/effect1.tgs differ diff --git a/Telegram/Resources/animations/star_reaction/effect2.tgs b/Telegram/Resources/animations/star_reaction/effect2.tgs new file mode 100644 index 000000000..2bbeb3b7a Binary files /dev/null and b/Telegram/Resources/animations/star_reaction/effect2.tgs differ diff --git a/Telegram/Resources/animations/star_reaction/effect3.tgs b/Telegram/Resources/animations/star_reaction/effect3.tgs new file mode 100644 index 000000000..983b41b7b Binary files /dev/null and b/Telegram/Resources/animations/star_reaction/effect3.tgs differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f45253adf..29638d49e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2399,6 +2399,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_title#one" = "{count} Star Needed"; "lng_credits_small_balance_title#other" = "{count} Stars Needed"; "lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps."; +"lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; "lng_credits_gift_title" = "Gift Telegram Stars"; @@ -3416,6 +3417,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_paid_about_link_url" = "https://telegram.org/blog/telegram-stars"; "lng_paid_price" = "Unlock for {price}"; +"lng_paid_react_title" = "Star Reaction"; +"lng_paid_react_about" = "Choose how many stars you want to send to {channel} to support this post."; +"lng_paid_react_top_title" = "Top Senders"; +"lng_paid_react_send" = "Send {price}"; +"lng_paid_react_agree" = "By sending stars, you agree to the {link}."; +"lng_paid_react_agree_link" = "Terms of Service"; + "lng_translate_show_original" = "Show Original"; "lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to_other" = "Translate to {name}"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 18a9e0734..6292da0c4 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -41,7 +41,10 @@ ../../animations/dice/winners.tgs ../../animations/star_reaction/appear.tgs - ../../animations/star_reaction/effect.tgs + ../../animations/star_reaction/center.tgs ../../animations/star_reaction/select.tgs + ../../animations/star_reaction/effect1.tgs + ../../animations/star_reaction/effect2.tgs + ../../animations/star_reaction/effect3.tgs diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index 4a5aece89..4458a5446 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -200,10 +200,14 @@ void CreditsStatus::request( _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input )).done([=](const TLResult &result) { _requestId = 0; - done(StatusFromTL(result, _peer)); + if (const auto onstack = done) { + onstack(StatusFromTL(result, _peer)); + } }).fail([=] { _requestId = 0; - done({}); + if (const auto onstack = done) { + onstack({}); + } }).send(); } diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 1951dc292..3aa7479c7 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_config.h" #include "mtproto/mtproto_dc_options.h" #include "data/business/data_shortcut_messages.h" +#include "data/components/credits.h" #include "data/components/scheduled_messages.h" #include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" @@ -2618,7 +2619,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateStarsBalance: { const auto &data = update.c_updateStarsBalance(); - _session->setCredits(data.vbalance().v); + _session->credits().apply(data); } break; } diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index a8771cc17..61b3b8952 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_credits.h" #include "apiwrap.h" #include "core/ui_integration.h" // Core::MarkedTextContext. +#include "data/components/credits.h" #include "data/data_credits.h" #include "data/data_photo.h" #include "data/data_session.h" @@ -301,7 +302,7 @@ void SendCreditsBox( st::giveawayGiftCodeStartButton.height / 2); AddChildToWidgetCenter(button.data(), loadingAnimation); loadingAnimation->showOn(state->confirmButtonBusy.value()); - } + } { auto buttonText = tr::lng_credits_box_out_confirm( lt_count, @@ -361,15 +362,11 @@ void SendCreditsBox( } { + session->credits().load(true); const auto balance = Settings::AddBalanceWidget( content, - session->creditsValue(), + session->credits().balanceValue(), false); - const auto api = balance->lifetime().make_state( - session->user()); - api->request({}, [=](Data::CreditsStatusSlice slice) { - session->setCredits(slice.balance); - }); rpl::combine( balance->sizeValue(), content->sizeValue() diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp new file mode 100644 index 000000000..e62b56ca2 --- /dev/null +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -0,0 +1,113 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/components/credits.h" + +#include "api/api_credits.h" +#include "data/data_user.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +constexpr auto kReloadThreshold = 60 * crl::time(1000); + +} // namespace + +Credits::Credits(not_null session) +: _session(session) +, _reload([=] { load(true); }) { +} + +Credits::~Credits() = default; + +void Credits::apply(const MTPDupdateStarsBalance &data) { + apply(data.vbalance().v); +} + +void Credits::load(bool force) { + if (_loader + || (!force + && _lastLoaded + && _lastLoaded + kReloadThreshold > crl::now())) { + return; + } + _loader = std::make_unique(_session->user()); + _loader->request({}, [=](Data::CreditsStatusSlice slice) { + _loader = nullptr; + apply(slice.balance); + }); +} + +bool Credits::loaded() const { + return _lastLoaded != 0; +} + +rpl::producer Credits::loadedValue() const { + if (loaded()) { + return rpl::single(true); + } + return rpl::single( + false + ) | rpl::then(_loadedChanges.events() | rpl::map_to(true)); +} + +uint64 Credits::balance() const { + const auto balance = _balance.current(); + const auto locked = _locked.current(); + return (balance >= locked) ? (balance - locked) : 0; +} + +rpl::producer Credits::balanceValue() const { + return rpl::combine( + _balance.value(), + _locked.value() + ) | rpl::map([=](uint64 balance, uint64 locked) { + return (balance >= locked) ? (balance - locked) : 0; + }); +} + +void Credits::lock(int count) { + Expects(loaded()); + Expects(count >= 0); + + _locked = _locked.current() + count; + + Ensures(_locked.current() <= _balance.current()); +} + +void Credits::unlock(int count) { + Expects(count >= 0); + Expects(_locked.current() >= count); + + _locked = _locked.current() - count; +} + +void Credits::withdrawLocked(int count) { + Expects(count >= 0); + Expects(_locked.current() >= count); + + const auto balance = _balance.current(); + _locked = _locked.current() - count; + apply(balance >= count ? (balance - count) : 0); + invalidate(); +} + +void Credits::invalidate() { + _reload.call(); +} + +void Credits::apply(uint64 balance) { + _balance = balance; + + const auto was = std::exchange(_lastLoaded, crl::now()); + if (!was) { + _loadedChanges.fire({}); + } +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h new file mode 100644 index 000000000..ff65f48f6 --- /dev/null +++ b/Telegram/SourceFiles/data/components/credits.h @@ -0,0 +1,55 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Api { +class CreditsStatus; +} // namespace Api + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Credits final { +public: + explicit Credits(not_null session); + ~Credits(); + + void load(bool force = false); + void apply(uint64 balance); + + [[nodiscard]] bool loaded() const; + [[nodiscard]] rpl::producer loadedValue() const; + + [[nodiscard]] uint64 balance() const; + [[nodiscard]] rpl::producer balanceValue() const; + + void lock(int count); + void unlock(int count); + void withdrawLocked(int count); + void invalidate(); + + void apply(const MTPDupdateStarsBalance &data); + +private: + const not_null _session; + + std::unique_ptr _loader; + + rpl::variable _balance; + rpl::variable _locked; + rpl::event_stream<> _loadedChanges; + crl::time _lastLoaded = 0; + + SingleQueuedInvokation _reload; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index c2b92de24..01956f309 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_app_config.h" #include "main/session/send_as_peers.h" +#include "data/components/credits.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_histories.h" @@ -34,6 +35,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "styles/style_chat.h" +#include "base/random.h" + namespace Data { namespace { @@ -45,6 +48,7 @@ constexpr auto kRecentReactionsLimit = 40; constexpr auto kMyTagsRequestTimeout = crl::time(1000); constexpr auto kTopRequestDelay = 60 * crl::time(1000); constexpr auto kTopReactionsLimit = 14; +constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000); [[nodiscard]] QString ReactionIdToLog(const ReactionId &id) { if (const auto custom = id.custom()) { @@ -109,17 +113,17 @@ constexpr auto kTopReactionsLimit = 14; : config->get("reactions_user_max_default", 1); } -bool IsMyRecent( +[[nodiscard]] bool IsMyRecent( const MTPDmessagePeerReaction &data, const ReactionId &id, not_null peer, const base::flat_map< ReactionId, std::vector> &recent, - bool ignoreChosen) { - if (peer->id == peer->session().userPeerId()) { + bool min) { + if (peer->isSelf()) { return true; - } else if (!ignoreChosen) { + } else if (!min) { return data.is_my(); } const auto j = recent.find(id); @@ -133,6 +137,20 @@ bool IsMyRecent( return (k != end(j->second)) && k->my; } +[[nodiscard]] bool IsMyTop( + const MTPDmessageReactor &data, + not_null peer, + const std::vector &top, + bool min) { + if (peer->isSelf()) { + return true; + } else if (!min) { + return data.is_my(); + } + const auto i = ranges::find(top, peer, &MessageReactionsTopPaid::peer); + return (i != end(top)) && i->my; +} + } // namespace PossibleItemReactionsRef LookupPossibleReactions( @@ -265,7 +283,8 @@ PossibleItemReactions::PossibleItemReactions( Reactions::Reactions(not_null owner) : _owner(owner) , _topRefreshTimer([=] { refreshTop(); }) -, _repaintTimer([=] { repaintCollected(); }) { +, _repaintTimer([=] { repaintCollected(); }) +, _sendPaidTimer([=] { sendPaid(); }) { refreshDefault(); _myTags.emplace(nullptr); @@ -284,6 +303,14 @@ Reactions::Reactions(not_null owner) _pollingItems.remove(item); _pollItems.remove(item); _repaintItems.remove(item); + _sendPaidItems.remove(item); + if (_sendingPaid == item) { + _sendingPaid = nullptr; + _owner->session().credits().invalidate(); + crl::on_main(&_owner->session(), [=] { + sendPaid(); + }); + } }, _lifetime); crl::on_main(&owner->session(), [=] { @@ -510,21 +537,49 @@ DocumentData *Reactions::chooseGenericAnimation( return i->aroundAnimation; } } - if (_genericAnimations.empty()) { + return randomLoadedFrom(_genericAnimations); +} + +void Reactions::fillPaidReactionAnimations() const { + const auto generate = [&](int index) { + const auto session = &_owner->session(); + const auto name = u"star_reaction_effect%1"_q.arg(index + 1); + return ChatHelpers::GenerateLocalTgsSticker(session, name); + }; + const auto kCount = 3; + for (auto i = 0; i != kCount; ++i) { + const auto document = generate(i); + _paidReactionAnimations.push_back(document); + _paidReactionCache.emplace( + document, + document->createMediaView()); + } + _paidReactionCache.front().second->checkStickerLarge(); +} + +DocumentData *Reactions::choosePaidReactionAnimation() const { + if (_paidReactionAnimations.empty()) { + fillPaidReactionAnimations(); + } + return randomLoadedFrom(_paidReactionAnimations); +} + +DocumentData *Reactions::randomLoadedFrom( + std::vector> list) const { + if (list.empty()) { return nullptr; } - auto copy = _genericAnimations; - ranges::shuffle(copy); - const auto first = copy.front(); + ranges::shuffle(list); + const auto first = list.front(); const auto view = first->createMediaView(); view->checkStickerLarge(); if (view->loaded()) { return first; } - const auto k = ranges::find_if(copy, [&](not_null value) { + const auto k = ranges::find_if(list, [&](not_null value) { return value->createMediaView()->loaded(); }); - return (k != end(copy)) ? (*k) : first; + return (k != end(list)) ? (*k) : first; } void Reactions::applyFavorite(const ReactionId &id) { @@ -593,7 +648,7 @@ void Reactions::preloadImageFor(const ReactionId &id) { auto &set = _images.emplace(id).first->second; set.effect = (id.custom() != 0); if (id.paid()) { - loadImage(set, lookupPaid()->selectAnimation, true); + loadImage(set, lookupPaid()->centerIcon, true); return; } auto &list = set.effect ? _effects : _available; @@ -631,6 +686,20 @@ void Reactions::preloadEffect(const Reaction &effect) { } void Reactions::preloadAnimationsFor(const ReactionId &id) { + const auto preload = [&](DocumentData *document) { + const auto view = document + ? document->activeMediaView() + : nullptr; + if (view) { + view->checkStickerLarge(); + } + }; + if (id.paid()) { + const auto fake = lookupPaid(); + preload(fake->centerIcon); + preload(fake->aroundAnimation); + return; + } const auto custom = id.custom(); const auto document = custom ? _owner->document(custom).get() : nullptr; const auto customSticker = document ? document->sticker() : nullptr; @@ -641,15 +710,6 @@ void Reactions::preloadAnimationsFor(const ReactionId &id) { if (i == end(_available)) { return; } - const auto preload = [&](DocumentData *document) { - const auto view = document - ? document->activeMediaView() - : nullptr; - if (view) { - view->checkStickerLarge(); - } - }; - if (!custom) { preload(i->centerIcon); } @@ -1380,21 +1440,6 @@ void Reactions::send(not_null item, bool addToRecent) { }).send(); } -void Reactions::sendPaid(not_null item, int count) { - const auto id = item->fullId(); - const auto randomId = base::unixtime::mtproto_msg_id(); - auto &api = _owner->session().api(); - api.request(MTPmessages_SendPaidReaction( - item->history()->peer->input, - MTP_int(id.msg), - MTP_int(count), - MTP_long(randomId) - )).done([=](const MTPUpdates &result) { - _owner->session().api().applyUpdates(result); - }).fail([=](const MTP::Error &error) { - }).send(); -} - void Reactions::poll(not_null item, crl::time now) { // Group them by one second. const auto last = item->lastReactionsRefreshTime(); @@ -1460,16 +1505,22 @@ not_null Reactions::lookupPaid() { const auto session = &_owner->session(); return ChatHelpers::GenerateLocalTgsSticker(session, name); }; + const auto appear = generate(u"star_reaction_appear"_q); + const auto center = generate(u"star_reaction_center"_q); const auto select = generate(u"star_reaction_select"_q); _paid.emplace(Reaction{ .id = ReactionId::Paid(), .title = u"Telegram Star"_q, - .appearAnimation = generate(u"star_reaction_appear"_q), + .appearAnimation = appear, .selectAnimation = select, - .centerIcon = select, - //.aroundAnimation = generate(u"star_reaction_effect"_q), + .centerIcon = center, .active = true, }); + _iconsCache.emplace(appear, appear->createMediaView()); + _iconsCache.emplace(center, center->createMediaView()); + _iconsCache.emplace(select, select->createMediaView()); + + fillPaidReactionAnimations(); } return &*_paid; } @@ -1488,6 +1539,13 @@ rpl::producer> Reactions::myTagsValue( ) | rpl::map(list)); } +void Reactions::schedulePaid(not_null item) { + _sendPaidItems[item] = crl::now() + kPaidAccumulatePeriod; + if (!_sendPaidTimer.isActive()) { + _sendPaidTimer.callOnce(kPaidAccumulatePeriod); + } +} + void Reactions::repaintCollected() { const auto now = crl::now(); auto closest = crl::time(); @@ -1543,7 +1601,7 @@ void Reactions::pollCollected() { } bool Reactions::sending(not_null item) const { - return _sentRequests.contains(item->fullId()); + return _sentRequests.contains(item->fullId()) || (_sendingPaid == item); } bool Reactions::HasUnread(const MTPMessageReactions &data) { @@ -1575,31 +1633,91 @@ void Reactions::CheckUnknownForUnread( }); } +void Reactions::sendPaid() { + if (_sendingPaid) { + return; + } + auto next = crl::time(); + const auto now = crl::now(); + for (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) { + const auto item = i->first; + const auto when = i->second; + if (when > now) { + if (!next || next > when) { + next = when; + } + ++i; + } else { + i = _sendPaidItems.erase(i); + if (sendPaid(item)) { + return; + } + } + } + if (next) { + _sendPaidTimer.callOnce(next - now); + } +} + +bool Reactions::sendPaid(not_null item) { + Expects(!_sendingPaid); + + const auto count = item->startPaidReactionSending(); + if (!count) { + return false; + } + + _sendingPaid = item; + sendPaidRequest(count); + return true; +} + +void Reactions::sendPaidRequest(int count) { + const auto id = _sendingPaid->fullId(); + const auto randomId = base::unixtime::mtproto_msg_id(); + auto &api = _owner->session().api(); + api.request(MTPmessages_SendPaidReaction( + _sendingPaid->history()->peer->input, + MTP_int(id.msg), + MTP_int(count), + MTP_long(randomId) + )).done([=](const MTPUpdates &result) { + sendPaidFinish(id, count, true); + _owner->session().api().applyUpdates(result); + }).fail([=](const MTP::Error &error) { + if (!_sendingPaid + || (_sendingPaid->fullId() != id) + || (error.type() != u"RANDOM_ID_EXPIRED"_q)) { + sendPaidFinish(id, count, false); + } else { + sendPaidRequest(count); + } + }).send(); +} + +void Reactions::sendPaidFinish(FullMsgId id, int count, bool success) { + if (_sendingPaid && _sendingPaid->fullId() == id) { + base::take(_sendingPaid)->finishPaidReactionSending(count, success); + sendPaid(); + } +} + MessageReactions::MessageReactions(not_null item) : _item(item) { } -void MessageReactions::addPaid(int count) { - Expects(_item->history()->peer->isBroadcast()); - - const auto id = Data::ReactionId::Paid(); - const auto history = _item->history(); - const auto peer = history->peer; - const auto i = ranges::find(_list, id, &MessageReaction::id); - if (i != end(_list)) { - i->my = true; - i->count += count; - std::rotate(i, i + 1, end(_list)); - } else { - _list.push_back({ .id = id, .count = count, .my = true }); +MessageReactions::~MessageReactions() { + cancelScheduledPaid(); + if (const auto paid = _paid.get()) { + if (paid->sending > 0) { + finishPaidSending(paid->sending, false); + } } - auto &owner = history->owner(); - owner.reactions().sendPaid(_item, count); - owner.notifyItemDataChange(_item); } void MessageReactions::add(const ReactionId &id, bool addToRecent) { Expects(!id.empty()); + Expects(!id.paid()); const auto history = _item->history(); const auto myLimit = SentReactionsLimit(_item); @@ -1613,6 +1731,9 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { history->owner().reactions().incrementMyTag(id, sublist); } _list.erase(ranges::remove_if(_list, [&](MessageReaction &one) { + if (one.id.paid()) { + return false; + } const auto removing = one.my && (my == myLimit || ++my == myLimit); if (!removing) { return false; @@ -1662,6 +1783,8 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { } void MessageReactions::remove(const ReactionId &id) { + Expects(!id.paid()); + const auto history = _item->history(); const auto self = history->session().user(); const auto i = ranges::find(_list, id, &MessageReaction::id); @@ -1765,6 +1888,7 @@ bool MessageReactions::checkIfChanged( bool MessageReactions::change( const QVector &list, const QVector &recent, + const QVector &top, bool min) { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { @@ -1844,8 +1968,7 @@ bool MessageReactions::change( if (list.size() >= i->count) { return; } - const auto peerId = peerFromMTP(data.vpeer_id()); - const auto peer = owner.peer(peerId); + const auto peer = owner.peer(peerFromMTP(data.vpeer_id())); const auto my = IsMyRecent(data, id, peer, _recent, min); list.push_back({ .peer = peer, @@ -1859,6 +1982,54 @@ bool MessageReactions::change( _recent = std::move(parsed); changed = true; } + + auto paidTop = std::vector(); + const auto &paindTopNow = _paid ? _paid->top : std::vector(); + for (const auto &reactor : top) { + const auto &data = reactor.data(); + const auto peer = owner.peer(peerFromMTP(data.vpeer_id())); + paidTop.push_back({ + .peer = peer, + .count = uint32(data.vcount().v), + .top = data.is_top(), + .my = IsMyTop(data, peer, paindTopNow, min), + }); + } + if (paidTop.empty()) { + if (_paid && !_paid->top.empty()) { + changed = true; + if (localPaidCount()) { + _paid->top.clear(); + } else { + _paid = nullptr; + } + } + } else { + if (min && _paid) { + const auto mine = [](const TopPaid &entry) { + return entry.my != 0; + }; + if (!ranges::contains(paidTop, true, mine)) { + const auto nonTopMine = [](const TopPaid &entry) { + return entry.my && !entry.top; + }; + const auto i = ranges::find(_paid->top, true, nonTopMine); + if (i != end(_paid->top)) { + paidTop.push_back(*i); + } + } + } + ranges::sort(paidTop, std::greater(), [](const TopPaid &entry) { + return entry.count; + }); + if (!_paid) { + _paid = std::make_unique(); + } + if (_paid->top != paidTop) { + _paid->top = std::move(paidTop); + changed = true; + } + } return changed; } @@ -1892,6 +2063,74 @@ void MessageReactions::markRead() { } } +void MessageReactions::scheduleSendPaid(int count) { + Expects(count > 0); + + if (!_paid) { + _paid = std::make_unique(); + } + _paid->scheduled += count; + _item->history()->session().credits().lock(count); + _item->history()->owner().reactions().schedulePaid(_item); +} + +int MessageReactions::scheduledPaid() const { + return _paid ? _paid->scheduled : 0; +} + +void MessageReactions::cancelScheduledPaid() { + if (_paid) { + if (_paid->scheduled > 0) { + _item->history()->session().credits().unlock( + base::take(_paid->scheduled)); + } + if (!_paid->sending && _paid->top.empty()) { + _paid = nullptr; + } + } +} + +int MessageReactions::startPaidSending() { + if (!_paid || !_paid->scheduled || _paid->sending) { + return 0; + } + _paid->sending = _paid->scheduled; + _paid->scheduled = 0; + return _paid->sending; +} + +void MessageReactions::finishPaidSending(int count, bool success) { + Expects(count > 0); + Expects(_paid != nullptr); + Expects(count == _paid->sending); + + _paid->sending = 0; + if (!_paid->scheduled && _paid->top.empty()) { + _paid = nullptr; + } + if (success) { + _item->history()->session().credits().withdrawLocked(count); + } else { + _item->history()->session().credits().unlock(count); + } +} + +int MessageReactions::localPaidCount() const { + return _paid ? (_paid->scheduled + _paid->sending) : 0; +} + +bool MessageReactions::clearCloudData() { + const auto result = !_list.empty(); + _recent.clear(); + _list.clear(); + if (localPaidCount()) { + _paid->top.clear(); + } else { + _paid = nullptr; + } + return result; +} + std::vector MessageReactions::chosen() const { return _list | ranges::views::filter(&MessageReaction::my) diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index fff6fcf04..1bcf46847 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -106,6 +106,7 @@ public: void renameTag(const ReactionId &id, const QString &name); [[nodiscard]] DocumentData *chooseGenericAnimation( not_null custom) const; + [[nodiscard]] DocumentData *choosePaidReactionAnimation() const; [[nodiscard]] rpl::producer<> topUpdates() const; [[nodiscard]] rpl::producer<> recentUpdates() const; @@ -129,7 +130,6 @@ public: void preloadAnimationsFor(const ReactionId &emoji); void send(not_null item, bool addToRecent); - void sendPaid(not_null item, int count); [[nodiscard]] bool sending(not_null item) const; void poll(not_null item, crl::time now); @@ -143,6 +143,8 @@ public: [[nodiscard]] rpl::producer> myTagsValue( SavedSublist *sublist = nullptr); + void schedulePaid(not_null item); + [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); static void CheckUnknownForUnread( not_null owner, @@ -233,9 +235,18 @@ private: void resolveEffectImages(); void downloadTaskFinished(); + void fillPaidReactionAnimations() const; + [[nodiscard]] DocumentData *randomLoadedFrom( + std::vector> list) const; + void repaintCollected(); void pollCollected(); + void sendPaid(); + bool sendPaid(not_null item); + void sendPaidRequest(int count); + void sendPaidFinish(FullMsgId id, int count, bool success); + const not_null _owner; std::vector _active; @@ -254,6 +265,7 @@ private: std::vector _topIds; base::flat_set _unresolvedTop; std::vector> _genericAnimations; + mutable std::vector> _paidReactionAnimations; std::vector _effects; ReactionId _favoriteId; ReactionId _unresolvedFavoriteId; @@ -264,6 +276,9 @@ private: base::flat_map< not_null, std::shared_ptr> _genericCache; + mutable base::flat_map< + not_null, + std::shared_ptr> _paidReactionCache; rpl::event_stream<> _topUpdated; rpl::event_stream<> _recentUpdated; rpl::event_stream<> _defaultUpdated; @@ -311,6 +326,10 @@ private: base::flat_set> _pollingItems; mtpRequestId _pollRequestId = 0; + base::flat_map, crl::time> _sendPaidItems; + HistoryItem *_sendingPaid = nullptr; + base::Timer _sendPaidTimer; + mtpRequestId _saveFaveRequestId = 0; rpl::lifetime _lifetime; @@ -323,29 +342,40 @@ struct RecentReaction { bool big = false; bool my = false; - friend inline auto operator<=>( - const RecentReaction &a, - const RecentReaction &b) = default; friend inline bool operator==( const RecentReaction &a, const RecentReaction &b) = default; }; +struct MessageReactionsTopPaid { + not_null peer; + uint32 count : 30 = 0; + uint32 top : 1 = 0; + uint32 my : 1 = 0; + + friend inline bool operator==( + const MessageReactionsTopPaid &a, + const MessageReactionsTopPaid &b) = default; +}; + class MessageReactions final { public: explicit MessageReactions(not_null item); + ~MessageReactions(); + + using TopPaid = MessageReactionsTopPaid; void add(const ReactionId &id, bool addToRecent); - void addPaid(int count); void remove(const ReactionId &id); bool change( const QVector &list, const QVector &recent, - bool ignoreChosen); + const QVector &top, + bool min); [[nodiscard]] bool checkIfChanged( const QVector &list, const QVector &recent, - bool ignoreChosen) const; + bool min) const; [[nodiscard]] const std::vector &list() const; [[nodiscard]] auto recent() const -> const base::flat_map> &; @@ -355,11 +385,27 @@ public: [[nodiscard]] bool hasUnread() const; void markRead(); + void scheduleSendPaid(int count); + [[nodiscard]] int scheduledPaid() const; + void cancelScheduledPaid(); + + int startPaidSending(); + void finishPaidSending(int count, bool success); + + [[nodiscard]] int localPaidCount() const; + bool clearCloudData(); + private: + struct Paid { + std::vector top; + int scheduled = 0; + int sending = 0; + }; const not_null _item; std::vector _list; base::flat_map> _recent; + std::unique_ptr _paid; }; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 5a8e833a2..7d2b3d821 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_emoji_interactions.h" #include "history/history_item_components.h" #include "history/history_item_text.h" +#include "payments/payments_reaction_process.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" @@ -490,13 +491,11 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { if (!item) { return; } else if (reaction.id.paid()) { - _controller->show(Ui::MakeConfirmBox({ - .text = u"Send 10-stars reaction?"_q, - .confirmed = [=](Fn close) { - item->addPaidReaction(10, HistoryItem::ReactionSource::Selector); - close(); - }, - })); + Payments::ShowPaidReactionDetails( + _controller->uiShow(), + item, + viewByItem(item), + HistoryReactionSource::Selector); return; } else if (Window::ShowReactPremiumError( _controller, @@ -507,7 +506,7 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { } return; } - item->toggleReaction(reaction.id, HistoryItem::ReactionSource::Selector); + item->toggleReaction(reaction.id, HistoryReactionSource::Selector); if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = viewByItem(item)) { @@ -2047,7 +2046,7 @@ void HistoryInner::toggleFavoriteReaction(not_null view) const { view->animateReaction({ .id = favorite }); } } - item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); + item->toggleReaction(favorite, HistoryReactionSource::Quick); } HistoryView::SelectedQuote HistoryInner::selectedQuote( diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index e40947033..ba8dc00ca 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2514,19 +2514,34 @@ bool HistoryItem::canReact() const { return true; } -void HistoryItem::addPaidReaction(int count, ReactionSource source) { +void HistoryItem::addPaidReaction(int count) { + Expects(count > 0); Expects(_history->peer->isBroadcast()); if (!_reactions) { _reactions = std::make_unique(this); } - _reactions->addPaid(count); + _reactions->scheduleSendPaid(count); + _history->owner().notifyItemDataChange(this); +} + +int HistoryItem::startPaidReactionSending() { + return _reactions ? _reactions->startPaidSending() : 0; +} + +void HistoryItem::finishPaidReactionSending(int count, bool success) { + Expects(_reactions != nullptr); + + _reactions->finishPaidSending(count, success); _history->owner().notifyItemDataChange(this); } void HistoryItem::toggleReaction( const Data::ReactionId &reaction, - ReactionSource source) { + HistoryReactionSource source) { + Expects(!reaction.paid()); + + const auto addToRecent = (source == HistoryReactionSource::Selector); if (!_reactions) { _reactions = std::make_unique(this); const auto canViewReactions = !isDiscussionPost() @@ -2534,7 +2549,7 @@ void HistoryItem::toggleReaction( if (canViewReactions) { _flags |= MessageFlag::CanViewReactions; } - _reactions->add(reaction, (source == ReactionSource::Selector)); + _reactions->add(reaction, addToRecent); } else if (ranges::contains(_reactions->chosen(), reaction)) { _reactions->remove(reaction); if (_reactions->empty()) { @@ -2542,7 +2557,7 @@ void HistoryItem::toggleReaction( _flags &= ~MessageFlag::CanViewReactions; } } else { - _reactions->add(reaction, (source == ReactionSource::Selector)); + _reactions->add(reaction, addToRecent); } _history->owner().notifyItemDataChange(this); } @@ -2556,6 +2571,30 @@ const std::vector &HistoryItem::reactions() const { return _reactions ? _reactions->list() : kEmpty; } +std::vector HistoryItem::reactionsWithLocal() const { + auto result = reactions(); + if (const auto local = _reactions ? _reactions->localPaidCount() : 0) { + const auto i = ranges::find( + result, + Data::ReactionId::Paid(), + &Data::MessageReaction::id); + if (i != end(result)) { + i->my = true; + i->count += local; + if (i != begin(result)) { + std::rotate(begin(result), i, i + 1); + } + } else { + result.insert(begin(result), Data::MessageReaction{ + .id = Data::ReactionId::Paid(), + .count = local, + .my = true, + }); + } + } + return result; +} + bool HistoryItem::reactionsAreTags() const { return _flags & MessageFlag::ReactionsAreTags; } @@ -3729,12 +3768,21 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { if (reactions || _reactionsLastRefreshed) { _reactionsLastRefreshed = crl::now(); } + const auto changeToEmpty = [&] { + if (!_reactions) { + return false; + } else if (!_reactions->localPaidCount()) { + _reactions = nullptr; + return true; + } + return _reactions->clearCloudData(); + }; if (!reactions) { _flags &= ~MessageFlag::CanViewReactions; if (_history->peer->isSelf()) { _flags |= MessageFlag::ReactionsAreTags; } - return (base::take(_reactions) != nullptr); + return changeToEmpty(); } const auto &data = reactions->data(); const auto empty = data.vresults().v.isEmpty(); @@ -3750,13 +3798,14 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { _flags &= ~MessageFlag::CanViewReactions; } if (empty) { - return (base::take(_reactions) != nullptr); + return changeToEmpty(); } else if (!_reactions) { _reactions = std::make_unique(this); } const auto min = data.is_min(); const auto &list = data.vresults().v; const auto &recent = data.vrecent_reactions().value_or_empty(); + const auto &top = data.vtop_reactors().value_or_empty(); if (min && hasUnreadReaction()) { // We can't update reactions from min if we have unread. if (_reactions->checkIfChanged(list, recent, min)) { @@ -3764,7 +3813,7 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { } return false; } - return _reactions->change(list, recent, min); + return _reactions->change(list, recent, top, min); } void HistoryItem::applyTTL(const MTPDmessage &data) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 9e852c77e..bead81aec 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -107,6 +107,12 @@ struct HistoryItemCommonFields { HistoryMessageMarkupData markup; }; +enum class HistoryReactionSource : char { + Selector, + Quick, + Existing, +}; + class HistoryItem final : public RuntimeComposer { public: [[nodiscard]] static std::unique_ptr CreateMedia( @@ -435,18 +441,17 @@ public: void translationDone(LanguageId to, TextWithEntities result); [[nodiscard]] bool canReact() const; - enum class ReactionSource { - Selector, - Quick, - Existing, - }; void toggleReaction( const Data::ReactionId &reaction, - ReactionSource source); - void addPaidReaction(int count, ReactionSource source); + HistoryReactionSource source); + void addPaidReaction(int count); + [[nodiscard]] int startPaidReactionSending(); + void finishPaidReactionSending(int count, bool success); void updateReactionsUnknown(); [[nodiscard]] auto reactions() const -> const std::vector &; + [[nodiscard]] auto reactionsWithLocal() const + -> std::vector; [[nodiscard]] auto recentReactions() const -> const base::flat_map< Data::ReactionId, diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 49013f1c8..6bda63135 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1549,9 +1549,7 @@ void ShowTagMenu( if (const auto item = owner->message(itemId)) { const auto &list = item->reactions(); if (ranges::contains(list, id, &MessageReaction::id)) { - item->toggleReaction( - id, - HistoryItem::ReactionSource::Quick); + item->toggleReaction(id, HistoryReactionSource::Quick); } } }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 4155adf9b..97aa4e81f 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2650,7 +2650,7 @@ void ListWidget::toggleFavoriteReaction(not_null view) const { view->animateReaction({ .id = favorite }); } } - item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); + item->toggleReaction(favorite, HistoryReactionSource::Quick); } void ListWidget::trySwitchToWordSelection() { @@ -2807,9 +2807,7 @@ void ListWidget::reactionChosen(ChosenReaction reaction) { } return; } - item->toggleReaction( - reaction.id, - HistoryItem::ReactionSource::Selector); + item->toggleReaction(reaction.id, HistoryReactionSource::Selector); if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = viewForItem(item)) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 14597582d..cb408e7fc 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwidget.h" #include "main/main_session.h" +#include "payments/payments_reaction_process.h" // TryAddingPaidReaction. #include "ui/text/text_options.h" #include "ui/painter.h" #include "window/window_session_controller.h" @@ -808,11 +809,7 @@ QSize Message::performCountOptimalSize() { const auto markup = item->inlineReplyMarkup(); const auto reactionsKey = [&] { - return embedReactionsInBottomInfo() - ? 0 - : embedReactionsInBubble() - ? 1 - : 2; + return embedReactionsInBubble() ? 0 : 1; }; const auto oldKey = reactionsKey(); validateText(); @@ -3248,97 +3245,62 @@ bool Message::isSignedAuthorElided() const { return _bottomInfo.isSignedAuthorElided(); } -bool Message::embedReactionsInBottomInfo() const { - return false; -#if 0 // legacy - const auto item = data(); - const auto user = item->history()->peer->asUser(); - if (!user - || user->isPremium() - || user->isSelf() - || user->session().premium()) { - // Only in messages of a non premium user with a non premium user. - // In saved messages we use reactions for tags, we don't embed them. - return false; - } - auto seenMy = false; - auto seenHis = false; - for (const auto &reaction : item->reactions()) { - if (reaction.id.custom()) { - // Only in messages without any custom emoji reactions. - return false; - } - // Only in messages without two reactions from the same person. - if (reaction.my) { - if (seenMy) { - return false; - } - seenMy = true; - } - if (!reaction.my || (reaction.count > 1)) { - if (seenHis) { - return false; - } - seenHis = true; - } - } - return true; -#endif -} - bool Message::embedReactionsInBubble() const { return needInfoDisplay(); } void Message::refreshReactions() { - const auto item = data(); - const auto &list = item->reactions(); - if (list.empty() || embedReactionsInBottomInfo()) { + using namespace Reactions; + auto reactionsData = InlineListDataFromMessage(this); + if (reactionsData.reactions.empty()) { setReactions(nullptr); return; } - using namespace Reactions; - auto reactionsData = InlineListDataFromMessage(this); if (!_reactions) { const auto handlerFactory = [=](ReactionId id) { const auto weak = base::make_weak(this); return std::make_shared([=]( ClickContext context) { - if (const auto strong = weak.get()) { - const auto item = strong->data(); - if (id.paid()) { - item->addPaidReaction( - 1, - HistoryItem::ReactionSource::Existing); - return; - } else if (item->reactionsAreTags()) { - if (item->history()->session().premium()) { - const auto tag = Data::SearchTagToQuery(id); - HashtagClickHandler(tag).onClick(context); - } else if (const auto controller - = ExtractController(context)) { - ShowPremiumPreviewBox( - controller, - PremiumFeature::TagsForMessages); - } - return; + const auto strong = weak.get(); + if (!strong) { + return; + } + const auto item = strong->data(); + const auto controller = ExtractController(context); + if (item->reactionsAreTags()) { + if (item->history()->session().premium()) { + const auto tag = Data::SearchTagToQuery(id); + HashtagClickHandler(tag).onClick(context); + } else if (controller) { + ShowPremiumPreviewBox( + controller, + PremiumFeature::TagsForMessages); } - item->toggleReaction( - id, - HistoryItem::ReactionSource::Existing); - if (const auto now = weak.get()) { - const auto chosen = now->data()->chosenReactions(); - if (ranges::contains(chosen, id)) { - now->animateReaction({ - .id = id, - }); - } + return; + } + if (id.paid()) { + Payments::TryAddingPaidReaction( + item, + weak.get(), + 1, + controller->uiShow()); + return; + } else { + const auto source = HistoryReactionSource::Existing; + item->toggleReaction(id, source); + } + if (const auto now = weak.get()) { + const auto chosen = now->data()->chosenReactions(); + if (id.paid() || ranges::contains(chosen, id)) { + now->animateReaction({ + .id = id, + }); } } }); }; setReactions(std::make_unique( - &item->history()->owner().reactions(), + &history()->owner().reactions(), handlerFactory, [=] { customEmojiRepaint(); }, std::move(reactionsData))); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 5c61fdaaa..48a45bb03 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -77,7 +77,6 @@ public: [[nodiscard]] const HistoryMessageEdited *displayedEditBadge() const; [[nodiscard]] HistoryMessageEdited *displayedEditBadge(); - [[nodiscard]] bool embedReactionsInBottomInfo() const; [[nodiscard]] bool embedReactionsInBubble() const; int marginTop() const override; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 7dcf5929b..71c0a6c49 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -791,7 +791,7 @@ InlineListData InlineListDataFromMessage(not_null message) { using Flag = InlineListData::Flag; const auto item = message->data(); auto result = InlineListData(); - result.reactions = item->reactions(); + result.reactions = item->reactionsWithLocal(); if (const auto user = item->history()->peer->asUser()) { // Always show userpics, we have all information. result.recent.reserve(result.reactions.size()); diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 5dd80e988..7f398e0a7 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/file_upload.h" #include "storage/storage_account.h" #include "storage/storage_facade.h" +#include "data/components/credits.h" #include "data/components/factchecks.h" #include "data/components/location_pickers.h" #include "data/components/recent_peers.h" @@ -115,6 +116,7 @@ Session::Session( std::make_unique(this, Data::TopPeerType::BotApp)) , _factchecks(std::make_unique(this)) , _locationPickers(std::make_unique()) +, _credits(std::make_unique(this)) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _saveSettingsTimer([=] { saveSettings(); }) { @@ -290,14 +292,6 @@ bool Session::premiumCanBuy() const { return _premiumPossible.current(); } -rpl::producer Session::creditsValue() const { - return _credits.value(); -} - -void Session::setCredits(uint64 credits) { - _credits = credits; -} - bool Session::isTestMode() const { return mtp().isTestMode(); } diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index ba5dbcd99..0a6917f38 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -37,6 +37,7 @@ class SponsoredMessages; class TopPeers; class Factchecks; class LocationPickers; +class Credits; } // namespace Data namespace HistoryView::Reactions { @@ -102,9 +103,6 @@ public: [[nodiscard]] bool premiumBadgesShown() const; [[nodiscard]] bool premiumCanBuy() const; - [[nodiscard]] rpl::producer creditsValue() const; - void setCredits(uint64 credits); - [[nodiscard]] bool isTestMode() const; [[nodiscard]] uint64 uniqueId() const; // userId() with TestDC shift. [[nodiscard]] UserId userId() const; @@ -138,6 +136,9 @@ public: [[nodiscard]] Data::LocationPickers &locationPickers() const { return *_locationPickers; } + [[nodiscard]] Data::Credits &credits() const { + return *_credits; + } [[nodiscard]] Api::Updates &updates() const { return *_updates; } @@ -268,6 +269,7 @@ private: const std::unique_ptr _topBotApps; const std::unique_ptr _factchecks; const std::unique_ptr _locationPickers; + const std::unique_ptr _credits; using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; const std::unique_ptr _cachedReactionIconFactory; @@ -275,7 +277,6 @@ private: const std::unique_ptr _supportHelper; std::shared_ptr _selfUserpicView; - rpl::variable _credits = 0; rpl::variable _premiumPossible = false; rpl::event_stream _termsLockChanges; diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index b62987a82..d05c256d4 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_credits.h" #include "base/unixtime.h" #include "boxes/send_credits_box.h" +#include "data/components/credits.h" #include "data/data_credits.h" #include "data/data_photo.h" #include "data/data_user.h" @@ -39,27 +40,35 @@ bool IsCreditsInvoice(not_null item) { } void ProcessCreditsPayment( - std::shared_ptr show, - QPointer fireworks, - std::shared_ptr form, - Fn maybeReturnToBot) { - const auto lifetime = std::make_shared(); - const auto api = lifetime->make_state( - show->session().user()); - const auto sendBox = [=] { + std::shared_ptr show, + QPointer fireworks, + std::shared_ptr form, + Fn maybeReturnToBot) { + const auto done = [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Blocked) { + if (const auto onstack = maybeReturnToBot) { + onstack(CheckoutResult::Failed); + } + return; + } else if (result == Settings::SmallBalanceResult::Cancelled) { + if (const auto onstack = maybeReturnToBot) { + onstack(CheckoutResult::Cancelled); + } + return; + } const auto unsuccessful = std::make_shared(true); const auto box = show->show(Box( Ui::SendCreditsBox, form, [=] { - *unsuccessful = false; - if (const auto widget = fireworks.data()) { - Ui::StartFireworks(widget); - } - if (maybeReturnToBot) { - maybeReturnToBot(CheckoutResult::Paid); - } - })); + *unsuccessful = false; + if (const auto widget = fireworks.data()) { + Ui::StartFireworks(widget); + } + if (maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Paid); + } + })); box->boxClosing() | rpl::start_with_next([=] { crl::on_main([=] { if ((*unsuccessful) && maybeReturnToBot) { @@ -68,28 +77,11 @@ void ProcessCreditsPayment( }); }, box->lifetime()); }; - api->request({}, [=](Data::CreditsStatusSlice slice) { - show->session().setCredits(slice.balance); - const auto creditsNeeded = int64(form->invoice.credits) - - int64(slice.balance); - if (creditsNeeded <= 0) { - sendBox(); - } else if (show->session().premiumPossible()) { - show->show(Box( - Settings::SmallBalanceBox, - show, - creditsNeeded, - form->botId, - sendBox)); - } else { - show->showToast( - tr::lng_credits_purchase_blocked(tr::now)); - if (maybeReturnToBot) { - maybeReturnToBot(CheckoutResult::Failed); - } - } - lifetime->destroy(); - }); + Settings::MaybeRequestBalanceIncrease( + show, + form->invoice.credits, + Settings::SmallBalanceBot{ .botId = form->botId }, + done); } void ProcessCreditsReceipt( diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp new file mode 100644 index 000000000..a5248776a --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -0,0 +1,190 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/payments_reaction_process.h" + +#include "api/api_credits.h" +#include "boxes/send_credits_box.h" // CreditsEmojiSmall. +#include "core/ui_integration.h" // MarkedTextContext. +#include "data/components/credits.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/session/session_show.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "payments/ui/payments_reaction_box.h" +#include "settings/settings_credits_graphics.h" +#include "ui/effects/reaction_fly_animation.h" +#include "ui/layers/box_content.h" +#include "ui/layers/generic_box.h" +#include "ui/layers/show.h" +#include "ui/text/text_utilities.h" + +namespace Payments { +namespace { + +constexpr auto kMaxPerReactionFallback = 2'500; +constexpr auto kDefaultPerReaction = 20; + +void TryAddingPaidReaction( + not_null session, + FullMsgId itemId, + base::weak_ptr weakView, + int count, + std::shared_ptr show, + Fn finished) { + const auto checkItem = [=] { + const auto item = session->data().message(itemId); + if (!item) { + if (const auto onstack = finished) { + onstack(false); + } + } + return item; + }; + + const auto item = checkItem(); + if (!item) { + return; + } + const auto done = [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Success) { + if (const auto item = checkItem()) { + item->addPaidReaction(count); + if (const auto view = weakView.get()) { + view->animateReaction({ + .id = Data::ReactionId::Paid(), + }); + } + if (const auto onstack = finished) { + onstack(true); + } + } + } else if (const auto onstack = finished) { + onstack(false); + } + }; + const auto channelId = peerToChannel(itemId.peer); + Settings::MaybeRequestBalanceIncrease( + Main::MakeSessionShow(show, session), + count, + Settings::SmallBalanceReaction{ .channelId = channelId }, + done); +} + +} // namespace + +void TryAddingPaidReaction( + not_null item, + HistoryView::Element *view, + int count, + std::shared_ptr show, + Fn finished) { + TryAddingPaidReaction( + &item->history()->session(), + item->fullId(), + view, + count, + std::move(show), + std::move(finished)); +} + +void ShowPaidReactionDetails( + std::shared_ptr show, + not_null item, + HistoryView::Element *view, + HistoryReactionSource source) { + Expects(item->history()->peer->isBroadcast()); + + const auto itemId = item->fullId(); + const auto session = &item->history()->session(); + const auto appConfig = &session->appConfig(); + + const auto min = 1; + const auto max = std::max( + appConfig->get( + u"stars_paid_reaction_amount_max"_q, + kMaxPerReactionFallback), + min); + const auto chosen = std::clamp(kDefaultPerReaction, min, max); + + struct State { + QPointer selectBox; + bool sending = false; + }; + const auto state = std::make_shared(); + session->credits().load(true); + + const auto weakView = base::make_weak(view); + const auto send = [=](int count, auto resend) -> void { + Expects(count > 0); + + const auto finish = [=](bool success) { + state->sending = false; + if (success) { + if (const auto strong = state->selectBox.data()) { + strong->closeBox(); + } + } + }; + if (state->sending) { + return; + } else if (const auto item = session->data().message(itemId)) { + state->sending = true; + TryAddingPaidReaction( + item, + weakView.get(), + count, + show, + finish); + } + }; + + auto submitText = [=](rpl::producer amount) { + auto nice = std::move(amount) | rpl::map([=](int count) { + return Ui::CreditsEmojiSmall(session).append( + Lang::FormatCountDecimal(count)); + }); + return tr::lng_paid_react_send( + lt_price, + std::move(nice), + Ui::Text::RichLangValue + ) | rpl::map([=](TextWithEntities &&text) { + return Ui::TextWithContext{ + .text = std::move(text), + .context = Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + }, + }; + }); + }; + state->selectBox = show->show(Ui::MakePaidReactionBox({ + .min = min, + .max = max, + .chosen = chosen, + .channel = item->history()->peer->name(), + .submit = std::move(submitText), + .balanceValue = session->credits().balanceValue(), + .send = [=](int count) { send(count, send); }, + })); + + if (const auto strong = state->selectBox.data()) { + session->data().itemRemoved( + ) | rpl::start_with_next([=](not_null removed) { + if (removed == item) { + strong->closeBox(); + } + }, strong->lifetime()); + } +} + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.h b/Telegram/SourceFiles/payments/payments_reaction_process.h new file mode 100644 index 000000000..9bd6c0457 --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_reaction_process.h @@ -0,0 +1,37 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +enum class HistoryReactionSource : char; + +class HistoryItem; + +namespace HistoryView { +class Element; +} // namespace HistoryView + +namespace Ui { +class Show; +} // namespace Ui + +namespace Payments { + +void TryAddingPaidReaction( + not_null item, + HistoryView::Element *view, + int count, + std::shared_ptr show, + Fn finished = nullptr); + +void ShowPaidReactionDetails( + std::shared_ptr show, + not_null item, + HistoryView::Element *view, + HistoryReactionSource source); + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp new file mode 100644 index 000000000..30550786f --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -0,0 +1,148 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/ui/payments_reaction_box.h" + +#include "lang/lang_keys.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/continuous_sliders.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" + +namespace Settings { +[[nodiscard]] not_null AddBalanceWidget( + not_null parent, + rpl::producer balanceValue, + bool rightAlign); +} // namespace Settings + +namespace Ui { +namespace { + +void PaidReactionSlider( + not_null container, + int min, + int current, + int max, + Fn changed) { + const auto top = st::boxTitleClose.height + st::creditsHistoryRightSkip; + const auto slider = container->add( + object_ptr(container, st::settingsScale), + st::boxRowPadding + QMargins(0, top, 0, 0)); + slider->resize(slider->width(), st::settingsScale.seekSize.height()); + slider->setPseudoDiscrete( + max + 1 - min, + [=](int index) { return min + index; }, + current - min, + changed, + changed); +} + +} // namespace + +void PaidReactionsBox( + not_null box, + PaidReactionBoxArgs &&args) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::boostBox); + box->setNoContentMargin(true); + + struct State { + rpl::variable chosen; + }; + const auto state = box->lifetime().make_state(); + state->chosen = args.chosen; + const auto changed = [=](int count) { + state->chosen = count; + }; + PaidReactionSlider( + box->verticalLayout(), + args.min, + args.chosen, + args.max, + changed); + + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + + box->addRow( + object_ptr( + box, + tr::lng_paid_react_title(), + st::boostCenteredTitle), + st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0)); + box->addRow( + object_ptr( + box, + tr::lng_paid_react_about( + lt_channel, + rpl::single(Text::Bold(args.channel)), + Text::RichLangValue), + st::boostText), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip))); + + const auto button = box->addButton(rpl::single(QString()), [=] { + args.send(state->chosen.current()); + }); + { + const auto buttonLabel = Ui::CreateChild( + button, + rpl::single(QString()), + st::creditsBoxButtonLabel); + args.submit( + state->chosen.value() + ) | rpl::start_with_next([=](const TextWithContext &text) { + buttonLabel->setMarkedText( + text.text, + text.context); + }, buttonLabel->lifetime()); + buttonLabel->setTextColorOverride( + box->getDelegate()->style().button.textFg->c); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + buttonLabel->moveToLeft( + (size.width() - buttonLabel->width()) / 2, + (size.height() - buttonLabel->height()) / 2); + }, buttonLabel->lifetime()); + buttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + } + + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::boostBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), button->y()); + }, button->lifetime()); + + { + const auto balance = Settings::AddBalanceWidget( + box->verticalLayout(), + std::move(args.balanceValue), + false); + rpl::combine( + balance->sizeValue(), + box->widthValue() + ) | rpl::start_with_next([=] { + balance->moveToLeft( + st::creditsHistoryRightSkip * 2, + st::creditsHistoryRightSkip); + balance->update(); + }, balance->lifetime()); + } +} + +object_ptr MakePaidReactionBox(PaidReactionBoxArgs &&args) { + return Box(PaidReactionsBox, std::move(args)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h new file mode 100644 index 000000000..9da69c368 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -0,0 +1,40 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" + +namespace Ui { + +class BoxContent; +class GenericBox; + +struct TextWithContext { + TextWithEntities text; + std::any context; +}; + +struct PaidReactionBoxArgs { + int min = 0; + int max = 0; + int chosen = 0; + + QString channel; + Fn(rpl::producer amount)> submit; + rpl::producer balanceValue; + Fn send; +}; + +void PaidReactionsBox( + not_null box, + PaidReactionBoxArgs &&args); + +[[nodiscard]] object_ptr MakePaidReactionBox( + PaidReactionBoxArgs &&args); + +} // namespace Ui diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 0d7d12490..d413551f0 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/gift_credits_box.h" #include "boxes/gift_premium_box.h" #include "core/click_handler_types.h" +#include "data/components/credits.h" #include "data/data_file_origin.h" #include "data/data_photo_media.h" #include "data/data_session.h" @@ -360,13 +361,9 @@ QPointer Credits::createPinnedToTop( { const auto balance = AddBalanceWidget( content, - _controller->session().creditsValue(), + _controller->session().credits().balanceValue(), true); - const auto api = balance->lifetime().make_state( - _controller->session().user()); - api->request({}, [=](Data::CreditsStatusSlice slice) { - _controller->session().setCredits(slice.balance); - }); + _controller->session().credits().load(true); rpl::combine( balance->sizeValue(), content->sizeValue() diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index c620f19d9..57c24f88a 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "core/click_handler_types.h" // UrlClickHandler #include "core/ui_integration.h" +#include "data/components/credits.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" @@ -416,16 +417,20 @@ not_null AddBalanceWidget( }); count->draw(p, { .position = QPoint( - balance->width() - count->maxWidth(), + (rightAlign + ? (balance->width() - count->maxWidth()) + : (starSize.width() + diffBetweenStarAndCount)), label->minHeight() + (starSize.height() - count->minHeight()) / 2), .availableWidth = balance->width(), }); p.drawImage( - balance->width() - - count->maxWidth() - - starSize.width() - - diffBetweenStarAndCount, + (rightAlign + ? (balance->width() + - count->maxWidth() + - starSize.width() + - diffBetweenStarAndCount) + : 0), label->minHeight(), *balanceStar); }, balance->lifetime()); @@ -859,9 +864,11 @@ object_ptr PaidMediaThumbnail( void SmallBalanceBox( not_null box, std::shared_ptr show, - int creditsNeeded, - UserId botId, + uint64 credits, + SmallBalanceSource source, Fn paid) { + Expects(show->session().credits().loaded()); + box->setWidth(st::boxWideWidth); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); const auto done = [=] { @@ -869,8 +876,17 @@ void SmallBalanceBox( paid(); }; - const auto bot = show->session().data().user(botId).get(); + const auto owner = &show->session().data(); + const auto peer = v::match(source, [&](SmallBalanceBot value) { + return owner->peer(peerFromUser(value.botId)); + }, [&](SmallBalanceReaction value) { + return owner->peer(peerFromChannel(value.channelId)); + }); + auto needed = show->session().credits().balanceValue( + ) | rpl::map([=](uint64 balance) { + return (balance < credits) ? (credits - balance) : 0; + }); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { return box->setPinnedToTopContent(object_ptr( box, @@ -878,11 +894,18 @@ void SmallBalanceBox( Ui::Premium::TopBarDescriptor{ .title = tr::lng_credits_small_balance_title( lt_count, - rpl::single(creditsNeeded) | tr::to_count()), - .about = tr::lng_credits_small_balance_about( - lt_bot, - rpl::single(TextWithEntities{ bot->name() }), - Ui::Text::RichLangValue), + rpl::duplicate( + needed + ) | rpl::filter(rpl::mappers::_1 > 0) | tr::to_count()), + .about = (peer->isBroadcast() + ? tr::lng_credits_small_balance_reaction( + lt_channel, + rpl::single(Ui::Text::Bold(peer->name())), + Ui::Text::RichLangValue) + : tr::lng_credits_small_balance_about( + lt_bot, + rpl::single(TextWithEntities{ peer->name() }), + Ui::Text::RichLangValue)), .light = true, .gradientStops = Ui::Premium::CreditsIconGradientStops(), })); @@ -892,8 +915,8 @@ void SmallBalanceBox( show, box->verticalLayout(), show->session().user(), - creditsNeeded, - done); + credits - show->session().credits().balance(), + [=] { show->session().credits().load(true); }); content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight); content->setMinimumHeight(st::infoLayerTopBarHeight); @@ -912,13 +935,10 @@ void SmallBalanceBox( { const auto balance = AddBalanceWidget( content, - show->session().creditsValue(), + show->session().credits().balanceValue(), true); - const auto api = balance->lifetime().make_state( - show->session().user()); - api->request({}, [=](Data::CreditsStatusSlice slice) { - show->session().setCredits(slice.balance); - }); + show->session().credits().load(true); + rpl::combine( balance->sizeValue(), content->sizeValue() @@ -929,6 +949,12 @@ void SmallBalanceBox( balance->update(); }, balance->lifetime()); } + + std::move( + needed + ) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::start_with_next(done, content->lifetime()); } void AddWithdrawalWidget( @@ -1229,4 +1255,58 @@ void AddWithdrawalWidget( Ui::AddSkip(container); } +void MaybeRequestBalanceIncrease( + std::shared_ptr show, + uint64 credits, + SmallBalanceSource source, + Fn done) { + struct State { + rpl::lifetime lifetime; + bool success = false; + }; + const auto state = std::make_shared(); + + const auto session = &show->session(); + session->credits().load(); + session->credits().loadedValue( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { + state->lifetime.destroy(); + + const auto balance = session->credits().balance(); + if (credits <= balance) { + if (const auto onstack = done) { + onstack(SmallBalanceResult::Success); + } + } else if (show->session().premiumPossible()) { + const auto success = [=] { + state->success = true; + if (const auto onstack = done) { + onstack(SmallBalanceResult::Success); + } + }; + const auto box = show->show(Box( + Settings::SmallBalanceBox, + show, + credits, + source, + success)); + box->boxClosing() | rpl::start_with_next([=] { + crl::on_main([=] { + if (!state->success) { + if (const auto onstack = done) { + onstack(SmallBalanceResult::Cancelled); + } + } + }); + }, box->lifetime()); + } else { + show->showToast( + tr::lng_credits_purchase_blocked(tr::now)); + if (const auto onstack = done) { + onstack(SmallBalanceResult::Blocked); + } + } + }, state->lifetime); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index e07262d6d..9a8f05e3e 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -87,12 +87,36 @@ void ShowRefundInfoBox( int totalCount, int photoSize); +struct SmallBalanceBot { + UserId botId = 0; +}; +struct SmallBalanceReaction { + ChannelId channelId = 0; +}; +struct SmallBalanceSource : std::variant< + SmallBalanceBot, + SmallBalanceReaction> { + using variant::variant; +}; + void SmallBalanceBox( not_null box, std::shared_ptr show, - int creditsNeeded, - UserId botId, + uint64 credits, + SmallBalanceSource source, Fn paid); +enum class SmallBalanceResult { + Success, + Blocked, + Cancelled, +}; + +void MaybeRequestBalanceIncrease( + std::shared_ptr show, + uint64 credits, + SmallBalanceSource source, + Fn done); + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 363ad342f..402b34159 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_emoji_status_panel.h" +#include "data/components/credits.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_cloud_themes.h" @@ -492,19 +493,21 @@ void SetupPremium( showOther(PremiumId()); }); { + controller->session().credits().load(); + const auto wrap = container->add( object_ptr>( container, object_ptr(container))); wrap->toggleOn( - controller->session().creditsValue( + controller->session().credits().balanceValue( ) | rpl::map(rpl::mappers::_1 > 0)); wrap->finishAnimating(); AddPremiumStar( AddButtonWithLabel( wrap->entity(), tr::lng_settings_credits(), - controller->session().creditsValue( + controller->session().credits().balanceValue( ) | rpl::map([=](uint64 c) { return c ? Lang::FormatCountToShort(c).string : QString{}; }), @@ -525,12 +528,6 @@ void SetupPremium( }); Ui::NewBadge::AddToRight(button); - const auto api = button->lifetime().make_state( - controller->session().user()); - api->request({}, [=](Data::CreditsStatusSlice slice) { - controller->session().setCredits(slice.balance); - }); - if (controller->session().premiumCanBuy()) { const auto button = AddButtonWithIcon( container, diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp index e26c8c9d1..a193bf32d 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp @@ -89,8 +89,8 @@ ReactionFlyAnimation::ReactionFlyAnimation( } else if (args.id.paid()) { const auto fake = owner->lookupPaid(); centerIcon = fake->centerIcon; - aroundAnimation = fake->aroundAnimation; - _centerSizeMultiplier = 1.;// fake->centerIcon ? 1. : 0.5; + aroundAnimation = owner->choosePaidReactionAnimation(); + _centerSizeMultiplier = 0.5; } else { const auto i = ranges::find(list, args.id, &::Data::Reaction::id); if (i == end(list)/* || !i->centerIcon*/) { diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 9bc3f6c76..1bb4eba5d 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -197,6 +197,8 @@ PRIVATE payments/ui/payments_panel.h payments/ui/payments_panel_data.h payments/ui/payments_panel_delegate.h + payments/ui/payments_reaction_box.cpp + payments/ui/payments_reaction_box.h platform/linux/current_geo_location_linux.cpp platform/linux/current_geo_location_linux.h