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

@ -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

Binary file not shown.

View file

@ -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}";

View file

@ -41,7 +41,10 @@
<file alias="winners.tgs">../../animations/dice/winners.tgs</file>
<file alias="star_reaction_appear.tgs">../../animations/star_reaction/appear.tgs</file>
<file alias="star_reaction_effect.tgs">../../animations/star_reaction/effect.tgs</file>
<file alias="star_reaction_center.tgs">../../animations/star_reaction/center.tgs</file>
<file alias="star_reaction_select.tgs">../../animations/star_reaction/select.tgs</file>
<file alias="star_reaction_effect1.tgs">../../animations/star_reaction/effect1.tgs</file>
<file alias="star_reaction_effect2.tgs">../../animations/star_reaction/effect2.tgs</file>
<file alias="star_reaction_effect3.tgs">../../animations/star_reaction/effect3.tgs</file>
</qresource>
</RCC>

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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<Api::CreditsStatus>(
session->user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
session->setCredits(slice.balance);
});
rpl::combine(
balance->sizeValue(),
content->sizeValue()

View file

@ -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<Main::Session*> 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<Api::CreditsStatus>(_session->user());
_loader->request({}, [=](Data::CreditsStatusSlice slice) {
_loader = nullptr;
apply(slice.balance);
});
}
bool Credits::loaded() const {
return _lastLoaded != 0;
}
rpl::producer<bool> 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<uint64> 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

View file

@ -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<Main::Session*> session);
~Credits();
void load(bool force = false);
void apply(uint64 balance);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] uint64 balance() const;
[[nodiscard]] rpl::producer<uint64> 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<Main::Session*> _session;
std::unique_ptr<Api::CreditsStatus> _loader;
rpl::variable<uint64> _balance;
rpl::variable<uint64> _locked;
rpl::event_stream<> _loadedChanges;
crl::time _lastLoaded = 0;
SingleQueuedInvokation _reload;
};
} // namespace Data

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)

View file

@ -106,6 +106,7 @@ public:
void renameTag(const ReactionId &id, const QString &name);
[[nodiscard]] DocumentData *chooseGenericAnimation(
not_null<DocumentData*> 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<HistoryItem*> item, bool addToRecent);
void sendPaid(not_null<HistoryItem*> item, int count);
[[nodiscard]] bool sending(not_null<HistoryItem*> item) const;
void poll(not_null<HistoryItem*> item, crl::time now);
@ -143,6 +143,8 @@ public:
[[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue(
SavedSublist *sublist = nullptr);
void schedulePaid(not_null<HistoryItem*> item);
[[nodiscard]] static bool HasUnread(const MTPMessageReactions &data);
static void CheckUnknownForUnread(
not_null<Session*> owner,
@ -233,9 +235,18 @@ private:
void resolveEffectImages();
void downloadTaskFinished();
void fillPaidReactionAnimations() const;
[[nodiscard]] DocumentData *randomLoadedFrom(
std::vector<not_null<DocumentData*>> list) const;
void repaintCollected();
void pollCollected();
void sendPaid();
bool sendPaid(not_null<HistoryItem*> item);
void sendPaidRequest(int count);
void sendPaidFinish(FullMsgId id, int count, bool success);
const not_null<Session*> _owner;
std::vector<Reaction> _active;
@ -254,6 +265,7 @@ private:
std::vector<ReactionId> _topIds;
base::flat_set<ReactionId> _unresolvedTop;
std::vector<not_null<DocumentData*>> _genericAnimations;
mutable std::vector<not_null<DocumentData*>> _paidReactionAnimations;
std::vector<Reaction> _effects;
ReactionId _favoriteId;
ReactionId _unresolvedFavoriteId;
@ -264,6 +276,9 @@ private:
base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _genericCache;
mutable base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _paidReactionCache;
rpl::event_stream<> _topUpdated;
rpl::event_stream<> _recentUpdated;
rpl::event_stream<> _defaultUpdated;
@ -311,6 +326,10 @@ private:
base::flat_set<not_null<HistoryItem*>> _pollingItems;
mtpRequestId _pollRequestId = 0;
base::flat_map<not_null<HistoryItem*>, 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<PeerData*> 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<HistoryItem*> 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<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent,
bool ignoreChosen);
const QVector<MTPMessageReactor> &top,
bool min);
[[nodiscard]] bool checkIfChanged(
const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent,
bool ignoreChosen) const;
bool min) const;
[[nodiscard]] const std::vector<MessageReaction> &list() const;
[[nodiscard]] auto recent() const
-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;
@ -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<TopPaid> top;
int scheduled = 0;
int sending = 0;
};
const not_null<HistoryItem*> _item;
std::vector<MessageReaction> _list;
base::flat_map<ReactionId, std::vector<RecentReaction>> _recent;
std::unique_ptr<Paid> _paid;
};

View file

@ -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<void()> 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<Element*> view) const {
view->animateReaction({ .id = favorite });
}
}
item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
item->toggleReaction(favorite, HistoryReactionSource::Quick);
}
HistoryView::SelectedQuote HistoryInner::selectedQuote(

View file

@ -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<Data::MessageReactions>(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<Data::MessageReactions>(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<Data::MessageReaction> &HistoryItem::reactions() const {
return _reactions ? _reactions->list() : kEmpty;
}
std::vector<Data::MessageReaction> 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<Data::MessageReactions>(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) {

View file

@ -107,6 +107,12 @@ struct HistoryItemCommonFields {
HistoryMessageMarkupData markup;
};
enum class HistoryReactionSource : char {
Selector,
Quick,
Existing,
};
class HistoryItem final : public RuntimeComposer<HistoryItem> {
public:
[[nodiscard]] static std::unique_ptr<Data::Media> 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<Data::MessageReaction> &;
[[nodiscard]] auto reactionsWithLocal() const
-> std::vector<Data::MessageReaction>;
[[nodiscard]] auto recentReactions() const
-> const base::flat_map<
Data::ReactionId,

View file

@ -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);
}
}
};

View file

@ -2650,7 +2650,7 @@ void ListWidget::toggleFavoriteReaction(not_null<Element*> 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)) {

View file

@ -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<LambdaClickHandler>([=](
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<InlineList>(
&item->history()->owner().reactions(),
&history()->owner().reactions(),
handlerFactory,
[=] { customEmojiRepaint(); },
std::move(reactionsData)));

View file

@ -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;

View file

@ -791,7 +791,7 @@ InlineListData InlineListDataFromMessage(not_null<Message*> 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());

View file

@ -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<Data::TopPeers>(this, Data::TopPeerType::BotApp))
, _factchecks(std::make_unique<Data::Factchecks>(this))
, _locationPickers(std::make_unique<Data::LocationPickers>())
, _credits(std::make_unique<Data::Credits>(this))
, _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
, _supportHelper(Support::Helper::Create(this))
, _saveSettingsTimer([=] { saveSettings(); }) {
@ -290,14 +292,6 @@ bool Session::premiumCanBuy() const {
return _premiumPossible.current();
}
rpl::producer<uint64> Session::creditsValue() const {
return _credits.value();
}
void Session::setCredits(uint64 credits) {
_credits = credits;
}
bool Session::isTestMode() const {
return mtp().isTestMode();
}

View file

@ -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<uint64> 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<Data::TopPeers> _topBotApps;
const std::unique_ptr<Data::Factchecks> _factchecks;
const std::unique_ptr<Data::LocationPickers> _locationPickers;
const std::unique_ptr<Data::Credits> _credits;
using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
const std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;
@ -275,7 +277,6 @@ private:
const std::unique_ptr<Support::Helper> _supportHelper;
std::shared_ptr<QImage> _selfUserpicView;
rpl::variable<uint64> _credits = 0;
rpl::variable<bool> _premiumPossible = false;
rpl::event_stream<bool> _termsLockChanges;

View file

@ -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<HistoryItem*> item) {
}
void ProcessCreditsPayment(
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto lifetime = std::make_shared<rpl::lifetime>();
const auto api = lifetime->make_state<Api::CreditsStatus>(
show->session().user());
const auto sendBox = [=] {
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> 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<bool>(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(

View file

@ -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<Main::Session*> session,
FullMsgId itemId,
base::weak_ptr<HistoryView::Element> weakView,
int count,
std::shared_ptr<Ui::Show> show,
Fn<void(bool)> 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<HistoryItem*> item,
HistoryView::Element *view,
int count,
std::shared_ptr<Ui::Show> show,
Fn<void(bool)> finished) {
TryAddingPaidReaction(
&item->history()->session(),
item->fullId(),
view,
count,
std::move(show),
std::move(finished));
}
void ShowPaidReactionDetails(
std::shared_ptr<Ui::Show> show,
not_null<HistoryItem*> 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<int>(
u"stars_paid_reaction_amount_max"_q,
kMaxPerReactionFallback),
min);
const auto chosen = std::clamp(kDefaultPerReaction, min, max);
struct State {
QPointer<Ui::BoxContent> selectBox;
bool sending = false;
};
const auto state = std::make_shared<State>();
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<int> 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<const HistoryItem*> removed) {
if (removed == item) {
strong->closeBox();
}
}, strong->lifetime());
}
}
} // namespace Payments

View file

@ -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<HistoryItem*> item,
HistoryView::Element *view,
int count,
std::shared_ptr<Ui::Show> show,
Fn<void(bool)> finished = nullptr);
void ShowPaidReactionDetails(
std::shared_ptr<Ui::Show> show,
not_null<HistoryItem*> item,
HistoryView::Element *view,
HistoryReactionSource source);
} // namespace Payments

View file

@ -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<Ui::RpWidget*> AddBalanceWidget(
not_null<Ui::RpWidget*> parent,
rpl::producer<uint64> balanceValue,
bool rightAlign);
} // namespace Settings
namespace Ui {
namespace {
void PaidReactionSlider(
not_null<VerticalLayout*> container,
int min,
int current,
int max,
Fn<void(int)> changed) {
const auto top = st::boxTitleClose.height + st::creditsHistoryRightSkip;
const auto slider = container->add(
object_ptr<MediaSlider>(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<GenericBox*> box,
PaidReactionBoxArgs &&args) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
box->setNoContentMargin(true);
struct State {
rpl::variable<int> chosen;
};
const auto state = box->lifetime().make_state<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<Ui::FlatLabel>(
box,
tr::lng_paid_react_title(),
st::boostCenteredTitle),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
box->addRow(
object_ptr<Ui::FlatLabel>(
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<Ui::FlatLabel>(
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<BoxContent> MakePaidReactionBox(PaidReactionBoxArgs &&args) {
return Box(PaidReactionsBox, std::move(args));
}
} // namespace Ui

View file

@ -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<TextWithContext>(rpl::producer<int> amount)> submit;
rpl::producer<uint64> balanceValue;
Fn<void(int)> send;
};
void PaidReactionsBox(
not_null<GenericBox*> box,
PaidReactionBoxArgs &&args);
[[nodiscard]] object_ptr<BoxContent> MakePaidReactionBox(
PaidReactionBoxArgs &&args);
} // namespace Ui

View file

@ -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<Ui::RpWidget> Credits::createPinnedToTop(
{
const auto balance = AddBalanceWidget(
content,
_controller->session().creditsValue(),
_controller->session().credits().balanceValue(),
true);
const auto api = balance->lifetime().make_state<Api::CreditsStatus>(
_controller->session().user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
_controller->session().setCredits(slice.balance);
});
_controller->session().credits().load(true);
rpl::combine(
balance->sizeValue(),
content->sizeValue()

View file

@ -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<Ui::RpWidget*> 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<Ui::RpWidget> PaidMediaThumbnail(
void SmallBalanceBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
int creditsNeeded,
UserId botId,
uint64 credits,
SmallBalanceSource source,
Fn<void()> 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<Ui::Premium::TopBar>(
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<Api::CreditsStatus>(
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<Main::SessionShow> show,
uint64 credits,
SmallBalanceSource source,
Fn<void(SmallBalanceResult)> done) {
struct State {
rpl::lifetime lifetime;
bool success = false;
};
const auto state = std::make_shared<State>();
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

View file

@ -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<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
int creditsNeeded,
UserId botId,
uint64 credits,
SmallBalanceSource source,
Fn<void()> paid);
enum class SmallBalanceResult {
Success,
Blocked,
Cancelled,
};
void MaybeRequestBalanceIncrease(
std::shared_ptr<Main::SessionShow> show,
uint64 credits,
SmallBalanceSource source,
Fn<void(SmallBalanceResult)> done);
} // namespace Settings

View file

@ -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<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(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<Api::CreditsStatus>(
controller->session().user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
controller->session().setCredits(slice.balance);
});
if (controller->session().premiumCanBuy()) {
const auto button = AddButtonWithIcon(
container,

View file

@ -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*/) {

View file

@ -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