Allow enabling paid reactions.

This commit is contained in:
John Preston 2024-08-05 11:53:42 +02:00
parent 126fd89bb7
commit bb3fc17489
33 changed files with 248 additions and 80 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1550,6 +1550,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_reactions_max_slider#other" = "{count} reactions per post";
"lng_manage_peer_reactions_max_about" = "Limit the number of different reactions that can be added to a post, including already published ones.";
"lng_manage_peer_reactions_paid" = "Enable Paid Reactions";
"lng_manage_peer_reactions_paid_about" = "Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. {link}";
"lng_manage_peer_reactions_paid_link" = "Learn more >";
"lng_manage_peer_antispam" = "Aggressive Anti-Spam";
"lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions.";
"lng_manage_peer_antispam_not_enough#one" = "Aggressive filtering can be enabled only in groups with more than **{count} member**.";

View file

@ -28,5 +28,20 @@
<file alias="collectible_phone.tgs">../../animations/collectible_phone.tgs</file>
<file alias="search.tgs">../../animations/search.tgs</file>
<file alias="noresults.tgs">../../animations/noresults.tgs</file>
<file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file>
<file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>
<file alias="bball_idle.tgs">../../animations/dice/bball_idle.tgs</file>
<file alias="fball_idle.tgs">../../animations/dice/fball_idle.tgs</file>
<file alias="slot_0_idle.tgs">../../animations/dice/slot_0_idle.tgs</file>
<file alias="slot_1_idle.tgs">../../animations/dice/slot_1_idle.tgs</file>
<file alias="slot_2_idle.tgs">../../animations/dice/slot_2_idle.tgs</file>
<file alias="slot_back.tgs">../../animations/dice/slot_back.tgs</file>
<file alias="slot_pull.tgs">../../animations/dice/slot_pull.tgs</file>
<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_select.tgs">../../animations/star_reaction/select.tgs</file>
</qresource>
</RCC>

View file

@ -7,16 +7,6 @@
<file alias="art/logo_256.png">../../art/logo_256.png</file>
<file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file>
<file alias="art/themeimage.jpg">../../art/themeimage.jpg</file>
<file alias="art/dice_idle.tgs">../../art/dice_idle.tgs</file>
<file alias="art/dart_idle.tgs">../../art/dart_idle.tgs</file>
<file alias="art/bball_idle.tgs">../../art/bball_idle.tgs</file>
<file alias="art/fball_idle.tgs">../../art/fball_idle.tgs</file>
<file alias="art/slot_0_idle.tgs">../../art/slot_0_idle.tgs</file>
<file alias="art/slot_1_idle.tgs">../../art/slot_1_idle.tgs</file>
<file alias="art/slot_2_idle.tgs">../../art/slot_2_idle.tgs</file>
<file alias="art/slot_back.tgs">../../art/slot_back.tgs</file>
<file alias="art/slot_pull.tgs">../../art/slot_pull.tgs</file>
<file alias="art/winners.tgs">../../art/winners.tgs</file>
<file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file>
<file alias="night.tdesktop-theme">../../night.tdesktop-theme</file>
<file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file>

View file

@ -608,6 +608,7 @@ void EditAllowedReactionsBox(
rpl::variable<SelectorState> selectorState;
std::vector<Data::ReactionId> selected;
rpl::variable<int> customCount;
bool paidEnabled = false;
};
const auto allowed = args.allowed;
const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
@ -617,6 +618,7 @@ void EditAllowedReactionsBox(
: Option::Some;
const auto state = box->lifetime().make_state<State>(State{
.option = optionInitial,
.paidEnabled = allowed.paidEnabled,
});
const auto container = box->verticalLayout();
@ -847,6 +849,29 @@ void EditAllowedReactionsBox(
) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));
Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());
Ui::AddSkip(inner);
const auto paid = inner->add(object_ptr<Ui::SettingsButton>(
inner,
tr::lng_manage_peer_reactions_paid(),
st::manageGroupNoIconButton.button));
paid->toggleOn(rpl::single(allowed.paidEnabled));
paid->toggledValue(
) | rpl::start_with_next([=](bool value) {
state->paidEnabled = value;
}, paid->lifetime());
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
tr::lng_manage_peer_reactions_paid_about(
lt_link,
tr::lng_manage_peer_reactions_paid_link([=](QString text) {
return Ui::Text::Link(
text,
u"https://telegram.org/tos/stars"_q);
}),
Ui::Text::WithEntities));
}
const auto collect = [=] {
auto result = AllowedReactions();
@ -856,6 +881,9 @@ void EditAllowedReactionsBox(
: (enabled->toggled())) {
result.some = state->selected;
}
if (!isGroup && enabled->toggled()) {
result.paidEnabled = state->paidEnabled;
}
auto some = result.some;
auto simple = all | ranges::views::transform(
&Data::Reaction::id
@ -907,16 +935,20 @@ void SaveAllowedReactions(
: allowed.some.empty()
? MTP_chatReactionsNone()
: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
const auto editPaidEnabled = peer->isBroadcast();
const auto paidEnabled = editPaidEnabled && allowed.paidEnabled;
const auto maxCount = allowed.maxCount;
peer->session().api().request(MTPmessages_SetChatAvailableReactions(
allowed.maxCount ? MTP_flags(Flag::f_reactions_limit) : MTP_flags(0),
MTP_flags(Flag()
| (maxCount ? Flag::f_reactions_limit : Flag())
| (editPaidEnabled ? Flag::f_paid_enabled : Flag())),
peer->input,
updated,
MTP_int(allowed.maxCount),
MTPbool() // paid_enabled
MTP_int(maxCount),
MTP_bool(paidEnabled)
)).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result);
auto parsed = Data::Parse(updated);
parsed.maxCount = allowed.maxCount;
auto parsed = Data::Parse(updated, maxCount, paidEnabled);
if (const auto chat = peer->asChat()) {
chat->setAllowedReactions(parsed);
} else if (const auto channel = peer->asChannel()) {

View file

@ -8,11 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/stickers_dice_pack.h"
#include "main/main_session.h"
#include "chat_helpers/stickers_lottie.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/image/image_location_factory.h"
#include "storage/localimageloader.h"
#include "base/unixtime.h"
#include "apiwrap.h"
@ -104,6 +102,11 @@ void DicePack::tryGenerateLocalZero() {
return;
}
const auto generateLocal = [&](int index, const QString &name) {
_map.emplace(
index,
ChatHelpers::GenerateLocalTgsSticker(_session, name));
};
if (_emoji == DicePacks::kDiceString) {
generateLocal(0, u"dice_idle"_q);
} else if (_emoji == DicePacks::kDartString) {
@ -123,32 +126,8 @@ void DicePack::tryGenerateLocalZero() {
}
}
void DicePack::generateLocal(int index, const QString &name) {
const auto path = u":/gui/art/"_q + name + u".tgs"_q;
auto task = FileLoadTask(
_session,
path,
QByteArray(),
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false);
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);
const auto document = _session->data().processDocument(
result->document,
Images::FromImageInMemory(result->thumb, "WEBP", result->thumbbytes));
document->setLocation(Core::FileLocation(path));
_map.emplace(index, document);
Ensures(document->sticker());
Ensures(document->sticker()->isLottie());
}
DicePacks::DicePacks(not_null<Main::Session*> session) : _session(session) {
DicePacks::DicePacks(not_null<Main::Session*> session)
: _session(session) {
}
DocumentData *DicePacks::lookup(const QString &emoji, int value) {

View file

@ -26,7 +26,6 @@ private:
void load();
void applySet(const MTPDmessages_stickerSet &data);
void tryGenerateLocalZero();
void generateLocal(int index, const QString &name);
const not_null<Main::Session*> _session;
QString _emoji;

View file

@ -15,9 +15,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "storage/cache/storage_cache_database.h"
#include "storage/localimageloader.h"
#include "history/view/media/history_view_media_common.h"
#include "media/clip/media_clip_reader.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/image/image_location_factory.h"
#include "ui/painter.h"
#include "main/main_session.h"
@ -312,4 +315,33 @@ QSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {
return HistoryView::NonEmptySize(request.size(dimensions, 8) / ratio);
}
not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name) {
const auto path = u":/animations/"_q + name + u".tgs"_q;
auto task = FileLoadTask(
session,
path,
QByteArray(),
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false);
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);
const auto document = session->data().processDocument(
result->document,
Images::FromImageInMemory(
result->thumb,
"WEBP",
result->thumbbytes));
document->setLocation(Core::FileLocation(path));
Ensures(document->sticker());
Ensures(document->sticker()->isLottie());
return document;
}
} // namespace ChatHelpers

View file

@ -130,4 +130,8 @@ bool PaintStickerThumbnailPath(
not_null<DocumentData*> document,
QSize box);
[[nodiscard]] not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name);
} // namespace ChatHelpers

View file

@ -1220,11 +1220,16 @@ void ApplyChannelUpdate(
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) {
auto parsed = Data::Parse(*allowed);
parsed.maxCount = reactionsLimit;
auto parsed = Data::Parse(
*allowed,
reactionsLimit,
update.is_paid_reactions_available());
channel->setAllowedReactions(std::move(parsed));
} else {
channel->setAllowedReactions({ .maxCount = reactionsLimit });
channel->setAllowedReactions({
.maxCount = reactionsLimit,
.paidEnabled = update.is_paid_reactions_available(),
});
}
channel->owner().stories().apply(channel, update.vstories());
channel->fullUpdated();

View file

@ -486,8 +486,8 @@ void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
chat->setTranslationDisabled(update.is_translations_disabled());
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) {
auto parsed = Data::Parse(*allowed);
parsed.maxCount = reactionsLimit;
const auto paidEnabled = false;
auto parsed = Data::Parse(*allowed, reactionsLimit, paidEnabled);
chat->setAllowedReactions(std::move(parsed));
} else {
chat->setAllowedReactions({ .maxCount = reactionsLimit });

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_message_reactions.h"
#include "chat_helpers/stickers_lottie.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_config.h"
#include "base/timer_rpl.h"
#include "base/call_delayed.h"
#include "base/unixtime.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
@ -204,9 +206,13 @@ PossibleItemReactionsRef LookupPossibleReactions(
}
} else {
const auto &allowed = PeerAllowedReactions(peer);
result.recent.reserve((allowed.type == AllowedReactionsType::Some)
? allowed.some.size()
: full.size());
result.recent.reserve((allowed.paidEnabled ? 1 : 0)
+ ((allowed.type == AllowedReactionsType::Some)
? allowed.some.size()
: full.size()));
if (allowed.paidEnabled) {
result.recent.push_back(reactions->lookupPaid());
}
add([&](const Reaction &reaction) {
const auto id = reaction.id;
if (id.custom() && !premiumPossible) {
@ -569,7 +575,7 @@ rpl::producer<> Reactions::effectsUpdates() const {
}
void Reactions::preloadReactionImageFor(const ReactionId &emoji) {
if (!emoji.emoji().isEmpty()) {
if (emoji.paid() || !emoji.emoji().isEmpty()) {
preloadImageFor(emoji);
}
}
@ -586,6 +592,10 @@ 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);
return;
}
auto &list = set.effect ? _effects : _available;
const auto i = ranges::find(list, id, &Reaction::id);
const auto document = (i == end(list))
@ -1356,7 +1366,10 @@ void Reactions::send(not_null<HistoryItem*> item, bool addToRecent) {
MTP_flags(flags),
item->history()->peer->input,
MTP_int(id.msg),
MTP_vector<MTPReaction>(chosen | ranges::views::transform(
MTP_vector<MTPReaction>(chosen | ranges::views::filter([](
const ReactionId &id) {
return !id.paid();
}) | ranges::views::transform(
ReactionToMTP
) | ranges::to<QVector<MTPReaction>>())
)).done([=](const MTPUpdates &result) {
@ -1367,6 +1380,21 @@ 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();
@ -1403,7 +1431,9 @@ void Reactions::clearTemporary() {
}
Reaction *Reactions::lookupTemporary(const ReactionId &id) {
if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
if (id.paid()) {
return lookupPaid();
} else if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
const auto i = ranges::find(_available, id, &Reaction::id);
return (i != end(_available)) ? &*i : nullptr;
} else if (const auto customId = id.custom()) {
@ -1424,6 +1454,26 @@ Reaction *Reactions::lookupTemporary(const ReactionId &id) {
return nullptr;
}
not_null<Reaction*> Reactions::lookupPaid() {
if (!_paid) {
const auto generate = [&](const QString &name) {
const auto session = &_owner->session();
return ChatHelpers::GenerateLocalTgsSticker(session, name);
};
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),
.selectAnimation = select,
.centerIcon = select,
//.aroundAnimation = generate(u"star_reaction_effect"_q),
.active = true,
});
}
return &*_paid;
}
rpl::producer<std::vector<Reaction>> Reactions::myTagsValue(
SavedSublist *sublist) {
refreshMyTags(sublist);
@ -1529,6 +1579,25 @@ 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 });
}
auto &owner = history->owner();
owner.reactions().sendPaid(_item, count);
owner.notifyItemDataChange(_item);
}
void MessageReactions::add(const ReactionId &id, bool addToRecent) {
Expects(!id.empty());

View file

@ -129,6 +129,7 @@ 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);
@ -137,6 +138,7 @@ public:
void clearTemporary();
[[nodiscard]] Reaction *lookupTemporary(const ReactionId &id);
[[nodiscard]] not_null<Reaction*> lookupPaid();
[[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue(
SavedSublist *sublist = nullptr);
@ -275,6 +277,7 @@ private:
// So we use std::map instead of base::flat_map here.
// Otherwise we could use flat_map<DocumentId, unique_ptr<Reaction>>.
std::map<DocumentId, Reaction> _temporary;
std::optional<Reaction> _paid;
base::Timer _topRefreshTimer;
mtpRequestId _topRequestId = 0;
@ -333,6 +336,7 @@ public:
explicit MessageReactions(not_null<HistoryItem*> item);
void add(const ReactionId &id, bool addToRecent);
void addPaid(int count);
void remove(const ReactionId &id);
bool change(
const QVector<MTPReactionCount> &list,

View file

@ -95,28 +95,22 @@ bool ApplyBotMenuButton(
return changed;
}
bool operator<(
const AllowedReactions &a,
const AllowedReactions &b) {
return (a.type < b.type) || ((a.type == b.type) && (a.some < b.some));
}
bool operator==(
const AllowedReactions &a,
const AllowedReactions &b) {
return (a.type == b.type)
&& (a.some == b.some)
&& (a.maxCount == b.maxCount);
}
AllowedReactions Parse(const MTPChatReactions &value) {
AllowedReactions Parse(
const MTPChatReactions &value,
int maxCount,
bool paidEnabled) {
return value.match([&](const MTPDchatReactionsNone &) {
return AllowedReactions();
return AllowedReactions{
.maxCount = maxCount,
.paidEnabled = paidEnabled,
};
}, [&](const MTPDchatReactionsAll &data) {
return AllowedReactions{
.maxCount = maxCount,
.type = (data.is_allow_custom()
? AllowedReactionsType::All
: AllowedReactionsType::Default),
.paidEnabled = paidEnabled,
};
}, [&](const MTPDchatReactionsSome &data) {
return AllowedReactions{
@ -125,7 +119,9 @@ AllowedReactions Parse(const MTPChatReactions &value) {
) | ranges::views::transform(
ReactionFromMTP
) | ranges::to_vector,
.maxCount = maxCount,
.type = AllowedReactionsType::Some,
.paidEnabled = paidEnabled,
};
});
}

View file

@ -101,7 +101,7 @@ bool ApplyBotMenuButton(
not_null<BotInfo*> info,
const MTPBotMenuButton *button);
enum class AllowedReactionsType {
enum class AllowedReactionsType : uchar {
All,
Default,
Some,
@ -109,14 +109,19 @@ enum class AllowedReactionsType {
struct AllowedReactions {
std::vector<ReactionId> some;
AllowedReactionsType type = AllowedReactionsType::Some;
int maxCount = 0;
AllowedReactionsType type = AllowedReactionsType::Some;
bool paidEnabled = false;
friend inline bool operator==(
const AllowedReactions &,
const AllowedReactions &) = default;
};
bool operator<(const AllowedReactions &a, const AllowedReactions &b);
bool operator==(const AllowedReactions &a, const AllowedReactions &b);
[[nodiscard]] AllowedReactions Parse(const MTPChatReactions &value);
[[nodiscard]] AllowedReactions Parse(
const MTPChatReactions &value,
int maxCount,
bool paidEnabled);
[[nodiscard]] PeerData *PeerFromInputMTP(
not_null<Session*> owner,
const MTPInputPeer &input);

View file

@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/reaction_fly_animation.h"
#include "ui/text/text_options.h"
#include "ui/text/text_isolated_emoji.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/edit_factcheck_box.h"
#include "ui/boxes/report_box.h"
#include "ui/layers/generic_box.h"
@ -488,6 +489,15 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) {
const auto item = session().data().message(reaction.context);
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();
},
}));
return;
} else if (Window::ShowReactPremiumError(
_controller,
item,

View file

@ -2514,6 +2514,16 @@ bool HistoryItem::canReact() const {
return true;
}
void HistoryItem::addPaidReaction(int count, ReactionSource source) {
Expects(_history->peer->isBroadcast());
if (!_reactions) {
_reactions = std::make_unique<Data::MessageReactions>(this);
}
_reactions->addPaid(count);
_history->owner().notifyItemDataChange(this);
}
void HistoryItem::toggleReaction(
const Data::ReactionId &reaction,
ReactionSource source) {
@ -2530,7 +2540,6 @@ void HistoryItem::toggleReaction(
if (_reactions->empty()) {
_reactions = nullptr;
_flags &= ~MessageFlag::CanViewReactions;
_history->owner().notifyItemDataChange(this);
}
} else {
_reactions->add(reaction, (source == ReactionSource::Selector));

View file

@ -443,6 +443,7 @@ public:
void toggleReaction(
const Data::ReactionId &reaction,
ReactionSource source);
void addPaidReaction(int count, ReactionSource source);
void updateReactionsUnknown();
[[nodiscard]] auto reactions() const
-> const std::vector<Data::MessageReaction> &;

View file

@ -3306,7 +3306,12 @@ void Message::refreshReactions() {
ClickContext context) {
if (const auto strong = weak.get()) {
const auto item = strong->data();
if (item->reactionsAreTags()) {
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);

View file

@ -144,6 +144,10 @@ void InlineList::layoutButtons() {
return true;
} else if (acount < bcount) {
return false;
} else if (b->id.paid()) {
return false;
} else if (a->id.paid()) {
return true;
}
return ranges::find(list, a->id, &::Data::Reaction::id)
< ranges::find(list, b->id, &::Data::Reaction::id);

View file

@ -86,6 +86,11 @@ ReactionFlyAnimation::ReactionFlyAnimation(
_customSize = esize;
_centerSizeMultiplier = _customSize / float64(size);
aroundAnimation = owner->chooseGenericAnimation(document);
} else if (args.id.paid()) {
const auto fake = owner->lookupPaid();
centerIcon = fake->centerIcon;
aroundAnimation = fake->aroundAnimation;
_centerSizeMultiplier = 1.;// fake->centerIcon ? 1. : 0.5;
} else {
const auto i = ranges::find(list, args.id, &::Data::Reaction::id);
if (i == end(list)/* || !i->centerIcon*/) {