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