Support anonymous paid reactions.

This commit is contained in:
John Preston 2024-08-12 17:51:31 +02:00
parent 37283a7a35
commit 284f1a5210
14 changed files with 443 additions and 108 deletions

View file

@ -3477,6 +3477,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_paid_react_toast_text#one" = "You reacted with **{count} Star**.";
"lng_paid_react_toast_text#other" = "You reacted with **{count} Stars**.";
"lng_paid_react_undo" = "Undo";
"lng_paid_react_show_in_top" = "Show me in Top Senders";
"lng_paid_react_anonymous" = "Anonymous";
"lng_translate_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}";

View file

@ -155,7 +155,8 @@ constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500;
} // namespace
PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item) {
not_null<HistoryItem*> item,
bool paidInFront) {
if (!item->canReact()) {
return {};
}
@ -258,12 +259,15 @@ PossibleItemReactionsRef LookupPossibleReactions(
&& premiumPossible;
}
if (!item->reactionsAreTags()) {
const auto i = ranges::find(
result.recent,
reactions->favoriteId(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
const auto toFront = [&](Data::ReactionId id) {
const auto i = ranges::find(result.recent, id, &Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
};
toFront(reactions->favoriteId());
if (paidInFront) {
toFront(Data::ReactionId::Paid());
}
}
return result;
@ -1704,31 +1708,71 @@ void Reactions::sendPaid() {
}
bool Reactions::sendPaid(not_null<HistoryItem*> item) {
const auto count = item->startPaidReactionSending();
if (!count) {
const auto send = item->startPaidReactionSending();
if (!send.valid) {
return false;
}
sendPaidRequest(item, count);
sendPaidRequest(item, send);
return true;
}
void Reactions::sendPaidRequest(not_null<HistoryItem*> item, int count) {
void Reactions::sendPaidPrivacyRequest(
not_null<HistoryItem*> item,
PaidReactionSend send) {
Expects(!_sendingPaid.contains(item));
Expects(!send.count);
const auto id = item->fullId();
auto &api = _owner->session().api();
using Flag = MTPmessages_SendPaidReaction::Flag;
const auto requestId = api.request(
MTPmessages_TogglePaidReactionPrivacy(
item->history()->peer->input,
MTP_int(id.msg),
MTP_bool(send.anonymous))
).done([=] {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
sendPaidFinish(item, send, true);
}
}
checkQuitPreventFinished();
}).fail([=](const MTP::Error &error) {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
sendPaidFinish(item, send, false);
}
}
checkQuitPreventFinished();
}).send();
_sendingPaid[item] = requestId;
}
void Reactions::sendPaidRequest(
not_null<HistoryItem*> item,
PaidReactionSend send) {
Expects(!_sendingPaid.contains(item));
if (!send.count) {
sendPaidPrivacyRequest(item, send);
return;
}
const auto id = item->fullId();
const auto randomId = base::unixtime::mtproto_msg_id();
auto &api = _owner->session().api();
using Flag = MTPmessages_SendPaidReaction::Flag;
const auto requestId = api.request(MTPmessages_SendPaidReaction(
MTP_flags(0),
MTP_flags(send.anonymous ? Flag::f_private : Flag()),
item->history()->peer->input,
MTP_int(id.msg),
MTP_int(count),
MTP_int(send.count),
MTP_long(randomId)
)).done([=](const MTPUpdates &result) {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
sendPaidFinish(item, count, true);
sendPaidFinish(item, send, true);
}
}
_owner->session().api().applyUpdates(result);
@ -1737,9 +1781,9 @@ void Reactions::sendPaidRequest(not_null<HistoryItem*> item, int count) {
if (const auto item = _owner->message(id)) {
_sendingPaid.remove(item);
if (error.type() == u"RANDOM_ID_EXPIRED"_q) {
sendPaidRequest(item, count);
sendPaidRequest(item, send);
} else {
sendPaidFinish(item, count, false);
sendPaidFinish(item, send, false);
}
}
checkQuitPreventFinished();
@ -1758,9 +1802,9 @@ void Reactions::checkQuitPreventFinished() {
void Reactions::sendPaidFinish(
not_null<HistoryItem*> item,
int count,
PaidReactionSend send,
bool success) {
item->finishPaidReactionSending(count, success);
item->finishPaidReactionSending(send, success);
sendPaid();
}
@ -1772,7 +1816,9 @@ MessageReactions::~MessageReactions() {
cancelScheduledPaid();
if (const auto paid = _paid.get()) {
if (paid->sending > 0) {
finishPaidSending(paid->sending, false);
finishPaidSending(
{ int(paid->sending), (paid->sendingAnonymous == 1) },
false);
}
}
}
@ -2063,7 +2109,7 @@ bool MessageReactions::change(
if (paidTop.empty()) {
if (_paid && !_paid->top.empty()) {
changed = true;
if (localPaidCount()) {
if (localPaidData()) {
_paid->top.clear();
} else {
_paid = nullptr;
@ -2133,14 +2179,18 @@ void MessageReactions::markRead() {
}
}
void MessageReactions::scheduleSendPaid(int count) {
Expects(count > 0);
void MessageReactions::scheduleSendPaid(int count, bool anonymous) {
Expects(count >= 0);
if (!_paid) {
_paid = std::make_unique<Paid>();
}
_paid->scheduled += count;
_item->history()->session().credits().lock(count);
_paid->scheduledFlag = 1;
_paid->scheduledAnonymous = anonymous ? 1 : 0;
if (count > 0) {
_item->history()->session().credits().lock(count);
}
_item->history()->owner().reactions().schedulePaid(_item);
}
@ -2150,50 +2200,100 @@ int MessageReactions::scheduledPaid() const {
void MessageReactions::cancelScheduledPaid() {
if (_paid) {
if (_paid->scheduled > 0) {
_item->history()->session().credits().unlock(
base::take(_paid->scheduled));
if (_paid->scheduledFlag) {
if (const auto amount = int(_paid->scheduled)) {
_item->history()->session().credits().unlock(amount);
}
_paid->scheduled = 0;
_paid->scheduledFlag = 0;
_paid->scheduledAnonymous = 0;
}
if (!_paid->sending && _paid->top.empty()) {
if (!_paid->sendingFlag && _paid->top.empty()) {
_paid = nullptr;
}
}
}
int MessageReactions::startPaidSending() {
if (!_paid || !_paid->scheduled || _paid->sending) {
return 0;
PaidReactionSend MessageReactions::startPaidSending() {
if (!_paid || !_paid->scheduledFlag || _paid->sendingFlag) {
return {};
}
_paid->sending = _paid->scheduled;
_paid->sendingFlag = _paid->scheduledFlag;
_paid->sendingAnonymous = _paid->scheduledAnonymous;
_paid->scheduled = 0;
return _paid->sending;
_paid->scheduledFlag = 0;
_paid->scheduledAnonymous = 0;
return {
.count = int(_paid->sending),
.valid = true,
.anonymous = (_paid->sendingAnonymous == 1),
};
}
void MessageReactions::finishPaidSending(int count, bool success) {
Expects(count > 0);
void MessageReactions::finishPaidSending(
PaidReactionSend send,
bool success) {
Expects(_paid != nullptr);
Expects(count == _paid->sending);
Expects(send.count == _paid->sending);
Expects(send.valid == (_paid->sendingFlag == 1));
Expects(send.anonymous == (_paid->sendingAnonymous == 1));
_paid->sending = 0;
if (!_paid->scheduled && _paid->top.empty()) {
_paid->sendingFlag = 0;
_paid->sendingAnonymous = 0;
if (!_paid->scheduledFlag && _paid->top.empty()) {
_paid = nullptr;
} else if (!send.count) {
const auto i = ranges::find_if(_paid->top, [](const TopPaid &top) {
return top.my;
});
if (i != end(_paid->top)) {
i->peer = send.anonymous
? nullptr
: _item->history()->session().user().get();
}
}
if (success) {
_item->history()->session().credits().withdrawLocked(count);
} else {
_item->history()->session().credits().unlock(count);
if (const auto amount = send.count) {
const auto credits = &_item->history()->session().credits();
if (success) {
credits->withdrawLocked(amount);
} else {
credits->unlock(amount);
}
}
}
bool MessageReactions::localPaidData() const {
return _paid && (_paid->scheduledFlag || _paid->sendingFlag);
}
int MessageReactions::localPaidCount() const {
return _paid ? (_paid->scheduled + _paid->sending) : 0;
}
bool MessageReactions::localPaidAnonymous() const {
const auto minePaidAnonymous = [&] {
for (const auto &entry : _paid->top) {
if (entry.my) {
return !entry.peer;
}
}
return false;
};
return _paid
&& (_paid->scheduledFlag
? (_paid->scheduledAnonymous == 1)
: _paid->sendingFlag
? (_paid->sendingAnonymous == 1)
: minePaidAnonymous());
}
bool MessageReactions::clearCloudData() {
const auto result = !_list.empty();
_recent.clear();
_list.clear();
if (localPaidCount()) {
if (localPaidData()) {
_paid->top.clear();
} else {
_paid = nullptr;

View file

@ -59,7 +59,8 @@ struct PossibleItemReactions {
};
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item);
not_null<HistoryItem*> item,
bool paidInFront = false);
struct MyTagInfo {
ReactionId id;
@ -67,6 +68,12 @@ struct MyTagInfo {
int count = 0;
};
struct PaidReactionSend {
int count = 0;
bool valid = false;
bool anonymous = false;
};
class Reactions final : private CustomEmojiManager::Listener {
public:
explicit Reactions(not_null<Session*> owner);
@ -250,10 +257,15 @@ private:
void sendPaid();
bool sendPaid(not_null<HistoryItem*> item);
void sendPaidRequest(not_null<HistoryItem*> item, int count);
void sendPaidRequest(
not_null<HistoryItem*> item,
PaidReactionSend send);
void sendPaidPrivacyRequest(
not_null<HistoryItem*> item,
PaidReactionSend send);
void sendPaidFinish(
not_null<HistoryItem*> item,
int count,
PaidReactionSend send,
bool success);
void checkQuitPreventFinished();
@ -397,21 +409,27 @@ public:
[[nodiscard]] bool hasUnread() const;
void markRead();
void scheduleSendPaid(int count);
void scheduleSendPaid(int count, bool anonymous);
[[nodiscard]] int scheduledPaid() const;
void cancelScheduledPaid();
int startPaidSending();
void finishPaidSending(int count, bool success);
[[nodiscard]] PaidReactionSend startPaidSending();
void finishPaidSending(PaidReactionSend send, bool success);
[[nodiscard]] bool localPaidData() const;
[[nodiscard]] int localPaidCount() const;
[[nodiscard]] bool localPaidAnonymous() const;
bool clearCloudData();
private:
struct Paid {
std::vector<TopPaid> top;
int scheduled = 0;
int sending = 0;
uint32 scheduled: 30 = 0;
uint32 scheduledFlag : 1 = 0;
uint32 scheduledAnonymous : 1 = 0;
uint32 sending : 30 = 0;
uint32 sendingFlag : 1 = 0;
uint32 sendingAnonymous : 1 = 0;
};
const not_null<HistoryItem*> _item;

View file

@ -492,7 +492,7 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) {
return;
} else if (reaction.id.paid()) {
Payments::ShowPaidReactionDetails(
_controller->uiShow(),
_controller,
item,
viewByItem(item),
HistoryReactionSource::Selector);

View file

@ -2521,15 +2521,17 @@ bool HistoryItem::canReact() const {
return true;
}
void HistoryItem::addPaidReaction(int count) {
Expects(count > 0);
void HistoryItem::addPaidReaction(int count, bool anonymous) {
Expects(count >= 0);
Expects(_history->peer->isBroadcast());
if (!_reactions) {
_reactions = std::make_unique<Data::MessageReactions>(this);
}
_reactions->scheduleSendPaid(count);
_history->owner().notifyItemDataChange(this);
_reactions->scheduleSendPaid(count, anonymous);
if (count > 0) {
_history->owner().notifyItemDataChange(this);
}
}
void HistoryItem::cancelScheduledPaidReaction() {
@ -2539,14 +2541,18 @@ void HistoryItem::cancelScheduledPaidReaction() {
}
}
int HistoryItem::startPaidReactionSending() {
return _reactions ? _reactions->startPaidSending() : 0;
Data::PaidReactionSend HistoryItem::startPaidReactionSending() {
return _reactions
? _reactions->startPaidSending()
: Data::PaidReactionSend();
}
void HistoryItem::finishPaidReactionSending(int count, bool success) {
void HistoryItem::finishPaidReactionSending(
Data::PaidReactionSend send,
bool success) {
Expects(_reactions != nullptr);
_reactions->finishPaidSending(count, success);
_reactions->finishPaidSending(send, success);
_history->owner().notifyItemDataChange(this);
}
@ -2586,12 +2592,15 @@ const std::vector<Data::MessageReaction> &HistoryItem::reactions() const {
}
std::vector<Data::MessageReaction> HistoryItem::reactionsWithLocal() const {
auto result = reactions();
if (!_reactions) {
return {};
}
auto result = _reactions->list();
const auto i = ranges::find(
result,
Data::ReactionId::Paid(),
&Data::MessageReaction::id);
if (const auto local = _reactions ? _reactions->localPaidCount() : 0) {
if (const auto local = _reactions->localPaidCount()) {
if (i != end(result)) {
i->my = true;
i->count += local;
@ -2629,10 +2638,41 @@ auto HistoryItem::recentReactions() const
return _reactions ? _reactions->recent() : kEmpty;
}
auto HistoryItem::topPaidReactions() const
-> const std::vector<Data::MessageReactionsTopPaid> & {
static const auto kEmpty = std::vector<Data::MessageReactionsTopPaid>();
return _reactions ? _reactions->topPaid() : kEmpty;
auto HistoryItem::topPaidReactionsWithLocal() const
-> std::vector<Data::MessageReactionsTopPaid> {
if (!_reactions) {
return {};
}
using TopPaid = Data::MessageReactionsTopPaid;
auto result = _reactions->topPaid();
const auto i = ranges::find_if(
result,
[](const TopPaid &entry) { return entry.my != 0; });
const auto peer = _reactions->localPaidAnonymous()
? nullptr
: history()->session().user().get();
if (const auto local = _reactions->localPaidCount()) {
const auto top = [&](int mine) {
return ranges::count_if(result, [&](const TopPaid &entry) {
return !entry.my && entry.count >= mine;
}) < 3;
};
if (i != end(result)) {
i->count += local;
i->peer = peer;
i->top = top(i->count) ? 1 : 0;
} else {
result.push_back({
.peer = peer,
.count = uint32(local),
.top = uint32(top(local) ? 1 : 0),
.my = uint32(1),
});
}
} else if (i != end(result)) {
i->peer = peer;
}
return result;
}
bool HistoryItem::canViewReactions() const {
@ -3834,7 +3874,7 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) {
const auto changeToEmpty = [&] {
if (!_reactions) {
return false;
} else if (!_reactions->localPaidCount()) {
} else if (!_reactions->localPaidData()) {
_reactions = nullptr;
return true;
}

View file

@ -67,6 +67,7 @@ class Thread;
struct SponsoredFrom;
class Story;
class SavedSublist;
struct PaidReactionSend;
} // namespace Data
namespace Main {
@ -445,10 +446,12 @@ public:
void toggleReaction(
const Data::ReactionId &reaction,
HistoryReactionSource source);
void addPaidReaction(int count);
void addPaidReaction(int count, bool anonymous);
void cancelScheduledPaidReaction();
[[nodiscard]] int startPaidReactionSending();
void finishPaidReactionSending(int count, bool success);
[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();
void finishPaidReactionSending(
Data::PaidReactionSend send,
bool success);
void updateReactionsUnknown();
[[nodiscard]] auto reactions() const
-> const std::vector<Data::MessageReaction> &;
@ -458,8 +461,8 @@ public:
-> const base::flat_map<
Data::ReactionId,
std::vector<Data::RecentReaction>> &;
[[nodiscard]] auto topPaidReactions() const
-> const std::vector<Data::MessageReactionsTopPaid> &;
[[nodiscard]] auto topPaidReactionsWithLocal() const
-> std::vector<Data::MessageReactionsTopPaid>;
[[nodiscard]] int reactionsPaidScheduled() const;
[[nodiscard]] bool canViewReactions() const;
[[nodiscard]] std::vector<Data::ReactionId> chosenReactions() const;

View file

@ -3283,6 +3283,7 @@ void Message::refreshReactions() {
item,
weak.get(),
1,
Payments::LookupMyPaidAnonymous(item),
controller->uiShow());
return;
} else {

View file

@ -1358,7 +1358,7 @@ AttachSelectorResult AttachSelectorToMenu(
desiredPosition,
st::reactPanelEmojiPan,
controller->uiShow(),
Data::LookupPossibleReactions(item),
Data::LookupPossibleReactions(item, true),
std::move(about),
std::move(iconFactory));
if (!result) {

View file

@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/show.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_thumbnails.h"
#include "window/window_session_controller.h"
namespace Payments {
namespace {
@ -41,6 +42,7 @@ void TryAddingPaidReaction(
FullMsgId itemId,
base::weak_ptr<HistoryView::Element> weakView,
int count,
bool anonymous,
std::shared_ptr<Ui::Show> show,
Fn<void(bool)> finished) {
const auto checkItem = [=] {
@ -60,8 +62,8 @@ void TryAddingPaidReaction(
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()) {
item->addPaidReaction(count, anonymous);
if (const auto view = count ? weakView.get() : nullptr) {
const auto history = view->history();
history->owner().notifyViewPaidReactionSent(view);
view->animateReaction({
@ -97,10 +99,20 @@ void TryAddingPaidReaction(
} // namespace
bool LookupMyPaidAnonymous(not_null<HistoryItem*> item) {
for (const auto &entry : item->topPaidReactionsWithLocal()) {
if (entry.my) {
return !entry.peer;
}
}
return false;
}
void TryAddingPaidReaction(
not_null<HistoryItem*> item,
HistoryView::Element *view,
int count,
bool anonymous,
std::shared_ptr<Ui::Show> show,
Fn<void(bool)> finished) {
TryAddingPaidReaction(
@ -108,17 +120,19 @@ void TryAddingPaidReaction(
item->fullId(),
view,
count,
anonymous,
std::move(show),
std::move(finished));
}
void ShowPaidReactionDetails(
std::shared_ptr<Ui::Show> show,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
HistoryView::Element *view,
HistoryReactionSource source) {
Expects(item->history()->peer->isBroadcast());
const auto show = controller->uiShow();
const auto itemId = item->fullId();
const auto session = &item->history()->session();
const auto appConfig = &session->appConfig();
@ -132,24 +146,26 @@ void ShowPaidReactionDetails(
struct State {
QPointer<Ui::BoxContent> selectBox;
bool ignoreAnonymousSwitch = false;
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 send = [=](int count, bool anonymous, auto resend) -> void {
Expects(count >= 0);
const auto finish = [=](bool success) {
state->sending = false;
if (success) {
if (success && count > 0) {
state->ignoreAnonymousSwitch = true;
if (const auto strong = state->selectBox.data()) {
strong->closeBox();
}
}
};
if (state->sending) {
if (state->sending || (!count && state->ignoreAnonymousSwitch)) {
return;
} else if (const auto item = session->data().message(itemId)) {
state->sending = true;
@ -157,6 +173,7 @@ void ShowPaidReactionDetails(
item,
weakView.get(),
count,
anonymous,
show,
finish);
}
@ -181,34 +198,57 @@ void ShowPaidReactionDetails(
};
});
};
auto already = 0;
auto top = std::vector<Ui::PaidReactionTop>();
const auto &topPaid = item->topPaidReactions();
top.reserve(topPaid.size());
for (const auto &entry : topPaid) {
if (entry.my) {
already = entry.count;
}
if (!entry.top) {
continue;
}
const auto add = [&](const Data::MessageReactionsTopPaid &entry) {
const auto peer = entry.peer;
const auto name = peer
? peer->shortName()
: tr::lng_paid_react_anonymous(tr::now);
const auto open = [=] {
controller->showPeerInfo(peer);
};
top.push_back({
.name = entry.peer->shortName(),
.photo = Ui::MakeUserpicThumbnail(entry.peer),
.name = name,
.photo = (peer
? Ui::MakeUserpicThumbnail(peer)
: Ui::MakeHiddenAuthorThumbnail()),
.count = int(entry.count),
.click = peer ? open : Fn<void()>(),
.my = (entry.my == 1),
});
};
const auto topPaid = item->topPaidReactionsWithLocal();
top.reserve(topPaid.size() + 2);
for (const auto &entry : topPaid) {
add(entry);
if (entry.my) {
auto copy = entry;
copy.peer = entry.peer ? nullptr : session->user().get();
add(copy);
}
}
if (!ranges::contains(top, true, &Ui::PaidReactionTop::my)) {
auto entry = Data::MessageReactionsTopPaid{
.peer = session->user(),
.count = 0,
.my = true,
};
add(entry);
entry.peer = nullptr;
add(entry);
}
ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
state->selectBox = show->show(Ui::MakePaidReactionBox({
.already = already + CountLocalPaid(item),
.chosen = chosen,
.max = max,
.top = std::move(top),
.channel = item->history()->peer->name(),
.submit = std::move(submitText),
.balanceValue = session->credits().balanceValue(),
.send = [=](int count) { send(count, send); },
.send = [=](int count, bool anonymous) {
send(count, anonymous, send);
},
}));
if (const auto strong = state->selectBox.data()) {

View file

@ -19,17 +19,24 @@ namespace Ui {
class Show;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Payments {
[[nodiscard]] bool LookupMyPaidAnonymous(not_null<HistoryItem*> item);
void TryAddingPaidReaction(
not_null<HistoryItem*> item,
HistoryView::Element *view,
int count,
bool anonymous,
std::shared_ptr<Ui::Show> show,
Fn<void(bool)> finished = nullptr);
void ShowPaidReactionDetails(
std::shared_ptr<Ui::Show> show,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
HistoryView::Element *view,
HistoryReactionSource source);

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
@ -178,8 +179,13 @@ void PaidReactionSlider(
[[nodiscard]] not_null<RpWidget*> MakeTopReactor(
not_null<QWidget*> parent,
const PaidReactionTop &data) {
const auto result = CreateChild<RpWidget>(parent);
const auto result = CreateChild<AbstractButton>(parent);
result->show();
if (data.click && !data.my) {
result->setClickedCallback(data.click);
} else {
result->setAttribute(Qt::WA_TransparentForMouseEvents);
}
struct State {
QImage badge;
@ -224,7 +230,9 @@ void PaidReactionSlider(
void FillTopReactors(
not_null<VerticalLayout*> container,
std::vector<PaidReactionTop> top) {
std::vector<PaidReactionTop> top,
rpl::producer<int> chosen,
rpl::producer<bool> anonymous) {
container->add(
MakeBoostFeaturesBadge(
container,
@ -238,20 +246,53 @@ void FillTopReactors(
st::paidReactTopMargin);
struct State {
std::vector<not_null<RpWidget*>> widgets;
rpl::event_stream<> updated;
};
const auto state = wrap->lifetime().make_state<State>();
const auto topCount = std::min(int(top.size()), kMaxTopPaidShown);
for (auto i = 0; i != topCount; ++i) {
state->widgets.push_back(MakeTopReactor(wrap, top[i]));
}
rpl::combine(
std::move(chosen),
std::move(anonymous)
) | rpl::start_with_next([=](int chosen, bool anonymous) {
for (const auto &widget : state->widgets) {
delete widget;
}
state->widgets.clear();
wrap->widthValue() | rpl::start_with_next([=](int width) {
auto list = std::vector<PaidReactionTop>();
list.reserve(kMaxTopPaidShown + 1);
for (const auto &entry : top) {
if (!entry.my) {
list.push_back(entry);
} else if (!entry.click == anonymous) {
auto copy = entry;
copy.count += chosen;
list.push_back(copy);
}
}
ranges::stable_sort(
list,
ranges::greater(),
&PaidReactionTop::count);
while (list.size() > kMaxTopPaidShown) {
list.pop_back();
}
for (const auto &entry : list) {
state->widgets.push_back(MakeTopReactor(wrap, entry));
}
state->updated.fire({});
}, wrap->lifetime());
rpl::combine(
state->updated.events_starting_with({}),
wrap->widthValue()
) | rpl::start_with_next([=](auto, int width) {
const auto single = width / 4;
if (single <= st::paidReactTopUserpic) {
return;
}
auto left = (width - single * topCount) / 2;
const auto count = int(state->widgets.size());
auto left = (width - single * count) / 2;
for (const auto widget : state->widgets) {
widget->setGeometry(left, 0, single, height);
left += single;
@ -264,6 +305,8 @@ void FillTopReactors(
void PaidReactionsBox(
not_null<GenericBox*> box,
PaidReactionBoxArgs &&args) {
Expects(!args.top.empty());
args.max = std::max(args.max, 2);
args.chosen = std::clamp(args.chosen, 1, args.max);
@ -273,13 +316,22 @@ void PaidReactionsBox(
struct State {
rpl::variable<int> chosen;
rpl::variable<bool> anonymous;
};
const auto state = box->lifetime().make_state<State>();
state->chosen = args.chosen;
const auto changed = [=](int count) {
state->chosen = count;
};
const auto initialAnonymous = ranges::find(
args.top,
true,
&PaidReactionTop::my
)->click == nullptr;
state->anonymous = initialAnonymous;
const auto content = box->verticalLayout();
AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop);
@ -307,6 +359,10 @@ void PaidReactionsBox(
&st::paidReactBubbleIcon,
st::boxRowPadding);
const auto already = ranges::find(
args.top,
true,
&PaidReactionTop::my)->count;
PaidReactionSlider(content, args.chosen, args.max, changed);
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
@ -323,10 +379,10 @@ void PaidReactionsBox(
+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));
const auto label = CreateChild<FlatLabel>(
labelWrap,
(args.already
(already
? tr::lng_paid_react_already(
lt_count,
rpl::single(args.already) | tr::to_count(),
rpl::single(already) | tr::to_count(),
Text::RichLangValue)
: tr::lng_paid_react_about(
lt_channel,
@ -343,13 +399,31 @@ void PaidReactionsBox(
label->moveToLeft(0, skip);
}, label->lifetime());
if (!args.top.empty()) {
FillTopReactors(content, std::move(args.top));
}
FillTopReactors(
content,
std::move(args.top),
state->chosen.value(),
state->anonymous.value());
const auto named = box->addRow(object_ptr<CenterWrap<Checkbox>>(
box,
object_ptr<Checkbox>(
box,
tr::lng_paid_react_show_in_top(tr::now),
!state->anonymous.current())));
state->anonymous = named->entity()->checkedValue(
) | rpl::map(!rpl::mappers::_1);
const auto button = box->addButton(rpl::single(QString()), [=] {
args.send(state->chosen.current());
args.send(state->chosen.current(), !named->entity()->checked());
});
box->boxClosing() | rpl::filter([=] {
return state->anonymous.current() != initialAnonymous;
}) | rpl::start_with_next([=] {
args.send(0, state->anonymous.current());
}, box->lifetime());
{
const auto buttonLabel = CreateChild<FlatLabel>(
button,

View file

@ -24,10 +24,11 @@ struct PaidReactionTop {
QString name;
std::shared_ptr<DynamicImage> photo;
int count = 0;
Fn<void()> click;
bool my = false;
};
struct PaidReactionBoxArgs {
int already = 0;
int chosen = 0;
int max = 0;
@ -36,7 +37,7 @@ struct PaidReactionBoxArgs {
QString channel;
Fn<rpl::producer<TextWithContext>(rpl::producer<int> amount)> submit;
rpl::producer<uint64> balanceValue;
Fn<void(int)> send;
Fn<void(int, bool)> send;
};
void PaidReactionsBox(

View file

@ -157,6 +157,19 @@ private:
};
class HiddenAuthorUserpic final : public DynamicImage {
public:
std::shared_ptr<DynamicImage> clone() override;
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
QImage _frame;
int _paletteVersion = 0;
};
class IconThumbnail final : public DynamicImage {
public:
explicit IconThumbnail(const style::icon &icon);
@ -476,6 +489,37 @@ void RepliesUserpic::subscribeToUpdates(Fn<void()> callback) {
}
}
std::shared_ptr<DynamicImage> HiddenAuthorUserpic::clone() {
return std::make_shared<HiddenAuthorUserpic>();
}
QImage HiddenAuthorUserpic::image(int size) {
const auto good = (_frame.width() == size * _frame.devicePixelRatio());
const auto paletteVersion = style::PaletteVersion();
if (!good || _paletteVersion != paletteVersion) {
_paletteVersion = paletteVersion;
const auto ratio = style::DevicePixelRatio();
if (!good) {
_frame = QImage(
QSize(size, size) * ratio,
QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
}
_frame.fill(Qt::transparent);
auto p = Painter(&_frame);
Ui::EmptyUserpic::PaintHiddenAuthor(p, 0, 0, size, size);
}
return _frame;
}
void HiddenAuthorUserpic::subscribeToUpdates(Fn<void()> callback) {
if (!callback) {
_frame = {};
}
}
IconThumbnail::IconThumbnail(const style::icon &icon) : _icon(icon) {
}
@ -573,6 +617,10 @@ std::shared_ptr<DynamicImage> MakeRepliesThumbnail() {
return std::make_shared<RepliesUserpic>();
}
std::shared_ptr<DynamicImage> MakeHiddenAuthorThumbnail() {
return std::make_shared<HiddenAuthorUserpic>();
}
std::shared_ptr<DynamicImage> MakeStoryThumbnail(
not_null<Data::Story*> story) {
using Result = std::shared_ptr<DynamicImage>;

View file

@ -23,6 +23,7 @@ class DynamicImage;
bool forceRound = false);
[[nodiscard]] std::shared_ptr<DynamicImage> MakeSavedMessagesThumbnail();
[[nodiscard]] std::shared_ptr<DynamicImage> MakeRepliesThumbnail();
[[nodiscard]] std::shared_ptr<DynamicImage> MakeHiddenAuthorThumbnail();
[[nodiscard]] std::shared_ptr<DynamicImage> MakeStoryThumbnail(
not_null<Data::Story*> story);
[[nodiscard]] std::shared_ptr<DynamicImage> MakeIconThumbnail(