2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-09-05 08:55:59 +00:00

Allow sending paid reactions.

This commit is contained in:
John Preston
2024-08-06 12:48:06 +02:00
parent bb3fc17489
commit 9bb1fa8782
36 changed files with 1257 additions and 275 deletions

View File

@@ -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<int>("reactions_user_max_default", 1);
}
bool IsMyRecent(
[[nodiscard]] bool IsMyRecent(
const MTPDmessagePeerReaction &data,
const ReactionId &id,
not_null<PeerData*> peer,
const base::flat_map<
ReactionId,
std::vector<RecentReaction>> &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<PeerData*> peer,
const std::vector<MessageReactionsTopPaid> &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<Session*> owner)
: _owner(owner)
, _topRefreshTimer([=] { refreshTop(); })
, _repaintTimer([=] { repaintCollected(); }) {
, _repaintTimer([=] { repaintCollected(); })
, _sendPaidTimer([=] { sendPaid(); }) {
refreshDefault();
_myTags.emplace(nullptr);
@@ -284,6 +303,14 @@ Reactions::Reactions(not_null<Session*> 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<not_null<DocumentData*>> 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<DocumentData*> value) {
const auto k = ranges::find_if(list, [&](not_null<DocumentData*> 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<HistoryItem*> item, bool addToRecent) {
}).send();
}
void Reactions::sendPaid(not_null<HistoryItem*> 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<HistoryItem*> item, crl::time now) {
// Group them by one second.
const auto last = item->lastReactionsRefreshTime();
@@ -1460,16 +1505,22 @@ not_null<Reaction*> 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<std::vector<Reaction>> Reactions::myTagsValue(
) | rpl::map(list));
}
void Reactions::schedulePaid(not_null<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> 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<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent,
const QVector<MTPMessageReactor> &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<TopPaid>();
const auto &paindTopNow = _paid ? _paid->top : std::vector<TopPaid>();
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<Paid>();
}
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>();
}
_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<ReactionId> MessageReactions::chosen() const {
return _list
| ranges::views::filter(&MessageReaction::my)