1834 lines
48 KiB
C++
1834 lines
48 KiB
C++
/*
|
|
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/data_message_reactions.h"
|
|
|
|
#include "history/history.h"
|
|
#include "history/history_item.h"
|
|
#include "history/history_item_components.h"
|
|
#include "main/main_session.h"
|
|
#include "main/main_app_config.h"
|
|
#include "main/session/send_as_peers.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_histories.h"
|
|
#include "data/data_changes.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_peer_values.h"
|
|
#include "data/data_saved_sublist.h"
|
|
#include "data/stickers/data_custom_emoji.h"
|
|
#include "storage/localimageloader.h"
|
|
#include "ui/image/image_location_factory.h"
|
|
#include "ui/animated_icon.h"
|
|
#include "mtproto/mtproto_config.h"
|
|
#include "base/timer_rpl.h"
|
|
#include "base/call_delayed.h"
|
|
#include "apiwrap.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
namespace Data {
|
|
namespace {
|
|
|
|
constexpr auto kRefreshFullListEach = 60 * 60 * crl::time(1000);
|
|
constexpr auto kPollEach = 20 * crl::time(1000);
|
|
constexpr auto kSizeForDownscale = 64;
|
|
constexpr auto kRecentRequestTimeout = 10 * crl::time(1000);
|
|
constexpr auto kRecentReactionsLimit = 40;
|
|
constexpr auto kMyTagsRequestTimeout = crl::time(1000);
|
|
constexpr auto kTopRequestDelay = 60 * crl::time(1000);
|
|
constexpr auto kTopReactionsLimit = 14;
|
|
|
|
[[nodiscard]] QString ReactionIdToLog(const ReactionId &id) {
|
|
if (const auto custom = id.custom()) {
|
|
return "custom:" + QString::number(custom);
|
|
}
|
|
return id.emoji();
|
|
}
|
|
|
|
[[nodiscard]] std::vector<ReactionId> ListFromMTP(
|
|
const MTPDmessages_reactions &data) {
|
|
const auto &list = data.vreactions().v;
|
|
auto result = std::vector<ReactionId>();
|
|
result.reserve(list.size());
|
|
for (const auto &reaction : list) {
|
|
const auto id = ReactionFromMTP(reaction);
|
|
if (id.empty()) {
|
|
LOG(("API Error: reactionEmpty in messages.reactions."));
|
|
} else {
|
|
result.push_back(id);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<MyTagInfo> ListFromMTP(
|
|
const MTPDmessages_savedReactionTags &data) {
|
|
const auto &list = data.vtags().v;
|
|
auto result = std::vector<MyTagInfo>();
|
|
result.reserve(list.size());
|
|
for (const auto &reaction : list) {
|
|
const auto &data = reaction.data();
|
|
const auto id = ReactionFromMTP(data.vreaction());
|
|
if (id.empty()) {
|
|
LOG(("API Error: reactionEmpty in messages.reactions."));
|
|
} else {
|
|
result.push_back({
|
|
.id = id,
|
|
.title = qs(data.vtitle().value_or_empty()),
|
|
.count = data.vcount().v,
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] Reaction CustomReaction(not_null<DocumentData*> document) {
|
|
return Reaction{
|
|
.id = { { document->id } },
|
|
.title = "Custom reaction",
|
|
.appearAnimation = document,
|
|
.selectAnimation = document,
|
|
.centerIcon = document,
|
|
.active = true,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] int SentReactionsLimit(not_null<HistoryItem*> item) {
|
|
const auto session = &item->history()->session();
|
|
const auto config = &session->appConfig();
|
|
return session->premium()
|
|
? config->get<int>("reactions_user_max_premium", 3)
|
|
: config->get<int>("reactions_user_max_default", 1);
|
|
}
|
|
|
|
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()) {
|
|
return true;
|
|
} else if (!ignoreChosen) {
|
|
return data.is_my();
|
|
}
|
|
const auto j = recent.find(id);
|
|
if (j == end(recent)) {
|
|
return false;
|
|
}
|
|
const auto k = ranges::find(
|
|
j->second,
|
|
peer,
|
|
&RecentReaction::peer);
|
|
return (k != end(j->second)) && k->my;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
PossibleItemReactionsRef LookupPossibleReactions(
|
|
not_null<HistoryItem*> item) {
|
|
if (!item->canReact()) {
|
|
return {};
|
|
}
|
|
auto result = PossibleItemReactionsRef();
|
|
auto peer = item->history()->peer;
|
|
if (item->isDiscussionPost()) {
|
|
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
|
|
if (forwarded->savedFromPeer) {
|
|
peer = forwarded->savedFromPeer;
|
|
}
|
|
}
|
|
}
|
|
const auto session = &peer->session();
|
|
const auto reactions = &session->data().reactions();
|
|
const auto &full = reactions->list(Reactions::Type::Active);
|
|
const auto &top = reactions->list(Reactions::Type::Top);
|
|
const auto &recent = reactions->list(Reactions::Type::Recent);
|
|
const auto &myTags = reactions->list(Reactions::Type::MyTags);
|
|
const auto &tags = reactions->list(Reactions::Type::Tags);
|
|
const auto &all = item->reactions();
|
|
const auto limit = UniqueReactionsLimit(peer);
|
|
const auto premiumPossible = session->premiumPossible();
|
|
const auto limited = (all.size() >= limit) && [&] {
|
|
const auto my = item->chosenReactions();
|
|
if (my.empty()) {
|
|
return true;
|
|
}
|
|
return true; // #TODO reactions
|
|
}();
|
|
auto added = base::flat_set<ReactionId>();
|
|
const auto add = [&](auto predicate) {
|
|
auto &&all = ranges::views::concat(top, recent, full);
|
|
for (const auto &reaction : all) {
|
|
if (predicate(reaction)) {
|
|
if (added.emplace(reaction.id).second) {
|
|
result.recent.push_back(&reaction);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
reactions->clearTemporary();
|
|
if (item->reactionsAreTags()) {
|
|
auto &&all = ranges::views::concat(myTags, tags);
|
|
result.recent.reserve(myTags.size() + tags.size());
|
|
for (const auto &reaction : all) {
|
|
if (premiumPossible
|
|
|| ranges::contains(tags, reaction.id, &Reaction::id)) {
|
|
if (added.emplace(reaction.id).second) {
|
|
result.recent.push_back(&reaction);
|
|
}
|
|
}
|
|
}
|
|
result.customAllowed = premiumPossible;
|
|
result.tags = true;
|
|
} else if (limited) {
|
|
result.recent.reserve(all.size());
|
|
add([&](const Reaction &reaction) {
|
|
return ranges::contains(all, reaction.id, &MessageReaction::id);
|
|
});
|
|
for (const auto &reaction : all) {
|
|
const auto id = reaction.id;
|
|
if (!added.contains(id)) {
|
|
if (const auto temp = reactions->lookupTemporary(id)) {
|
|
result.recent.push_back(temp);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const auto &allowed = PeerAllowedReactions(peer);
|
|
result.recent.reserve((allowed.type == AllowedReactionsType::Some)
|
|
? allowed.some.size()
|
|
: full.size());
|
|
add([&](const Reaction &reaction) {
|
|
const auto id = reaction.id;
|
|
if (id.custom() && !premiumPossible) {
|
|
return false;
|
|
} else if ((allowed.type == AllowedReactionsType::Some)
|
|
&& !ranges::contains(allowed.some, id)) {
|
|
return false;
|
|
} else if (id.custom()
|
|
&& allowed.type == AllowedReactionsType::Default) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (allowed.type == AllowedReactionsType::Some) {
|
|
for (const auto &id : allowed.some) {
|
|
if (!added.contains(id)) {
|
|
if (const auto temp = reactions->lookupTemporary(id)) {
|
|
result.recent.push_back(temp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result.customAllowed = (allowed.type == AllowedReactionsType::All)
|
|
&& 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);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
PossibleItemReactions::PossibleItemReactions(
|
|
const PossibleItemReactionsRef &other)
|
|
: recent(other.recent | ranges::views::transform([](const auto &value) {
|
|
return *value;
|
|
}) | ranges::to_vector)
|
|
, stickers(other.stickers | ranges::views::transform([](const auto &value) {
|
|
return *value;
|
|
}) | ranges::to_vector)
|
|
, customAllowed(other.customAllowed)
|
|
, tags(other.tags){
|
|
}
|
|
|
|
Reactions::Reactions(not_null<Session*> owner)
|
|
: _owner(owner)
|
|
, _topRefreshTimer([=] { refreshTop(); })
|
|
, _repaintTimer([=] { repaintCollected(); }) {
|
|
refreshDefault();
|
|
|
|
_myTags.emplace(nullptr);
|
|
|
|
base::timer_each(
|
|
kRefreshFullListEach
|
|
) | rpl::start_with_next([=] {
|
|
refreshDefault();
|
|
requestEffects();
|
|
}, _lifetime);
|
|
|
|
_owner->session().changes().messageUpdates(
|
|
MessageUpdate::Flag::Destroyed
|
|
) | rpl::start_with_next([=](const MessageUpdate &update) {
|
|
const auto item = update.item;
|
|
_pollingItems.remove(item);
|
|
_pollItems.remove(item);
|
|
_repaintItems.remove(item);
|
|
}, _lifetime);
|
|
|
|
crl::on_main(&owner->session(), [=] {
|
|
// applyFavorite accesses not yet constructed parts of session.
|
|
rpl::single(rpl::empty) | rpl::then(
|
|
_owner->session().mtp().config().updates()
|
|
) | rpl::map([=] {
|
|
const auto &config = _owner->session().mtp().configValues();
|
|
return config.reactionDefaultCustom
|
|
? ReactionId{ DocumentId(config.reactionDefaultCustom) }
|
|
: ReactionId{ config.reactionDefaultEmoji };
|
|
}) | rpl::filter([=](const ReactionId &id) {
|
|
return !_saveFaveRequestId;
|
|
}) | rpl::start_with_next([=](ReactionId &&id) {
|
|
applyFavorite(id);
|
|
}, _lifetime);
|
|
});
|
|
}
|
|
|
|
Reactions::~Reactions() = default;
|
|
|
|
Main::Session &Reactions::session() const {
|
|
return _owner->session();
|
|
}
|
|
|
|
void Reactions::refreshTop() {
|
|
requestTop();
|
|
}
|
|
|
|
void Reactions::refreshRecent() {
|
|
requestRecent();
|
|
}
|
|
|
|
void Reactions::refreshRecentDelayed() {
|
|
if (_recentRequestId || _recentRequestScheduled) {
|
|
return;
|
|
}
|
|
_recentRequestScheduled = true;
|
|
base::call_delayed(kRecentRequestTimeout, &_owner->session(), [=] {
|
|
if (_recentRequestScheduled) {
|
|
requestRecent();
|
|
}
|
|
});
|
|
}
|
|
|
|
void Reactions::refreshDefault() {
|
|
requestDefault();
|
|
}
|
|
|
|
void Reactions::refreshMyTags(SavedSublist *sublist) {
|
|
requestMyTags(sublist);
|
|
}
|
|
|
|
void Reactions::refreshMyTagsDelayed() {
|
|
auto &my = _myTags[nullptr];
|
|
if (my.requestId || my.requestScheduled) {
|
|
return;
|
|
}
|
|
my.requestScheduled = true;
|
|
base::call_delayed(kMyTagsRequestTimeout, &_owner->session(), [=] {
|
|
if (_myTags[nullptr].requestScheduled) {
|
|
requestMyTags();
|
|
}
|
|
});
|
|
}
|
|
|
|
void Reactions::refreshTags() {
|
|
requestTags();
|
|
}
|
|
|
|
void Reactions::refreshEffects() {
|
|
if (_effects.empty()) {
|
|
requestEffects();
|
|
}
|
|
}
|
|
|
|
const std::vector<Reaction> &Reactions::list(Type type) const {
|
|
switch (type) {
|
|
case Type::Active: return _active;
|
|
case Type::Recent: return _recent;
|
|
case Type::Top: return _top;
|
|
case Type::All: return _available;
|
|
case Type::MyTags:
|
|
return _myTags.find((SavedSublist*)nullptr)->second.tags;
|
|
case Type::Tags: return _tags;
|
|
case Type::Effects: return _effects;
|
|
}
|
|
Unexpected("Type in Reactions::list.");
|
|
}
|
|
|
|
const std::vector<MyTagInfo> &Reactions::myTagsInfo() const {
|
|
return _myTags.find((SavedSublist*)nullptr)->second.info;
|
|
}
|
|
|
|
const QString &Reactions::myTagTitle(const ReactionId &id) const {
|
|
const auto i = _myTags.find((SavedSublist*)nullptr);
|
|
if (i != end(_myTags)) {
|
|
const auto j = ranges::find(i->second.info, id, &MyTagInfo::id);
|
|
if (j != end(i->second.info)) {
|
|
return j->title;
|
|
}
|
|
}
|
|
static const auto kEmpty = QString();
|
|
return kEmpty;
|
|
}
|
|
|
|
ReactionId Reactions::favoriteId() const {
|
|
return _favoriteId;
|
|
}
|
|
|
|
const Reaction *Reactions::favorite() const {
|
|
return _favorite ? &*_favorite : nullptr;
|
|
}
|
|
|
|
void Reactions::setFavorite(const ReactionId &id) {
|
|
const auto api = &_owner->session().api();
|
|
if (_saveFaveRequestId) {
|
|
api->request(_saveFaveRequestId).cancel();
|
|
}
|
|
_saveFaveRequestId = api->request(MTPmessages_SetDefaultReaction(
|
|
ReactionToMTP(id)
|
|
)).done([=] {
|
|
_saveFaveRequestId = 0;
|
|
}).fail([=] {
|
|
_saveFaveRequestId = 0;
|
|
}).send();
|
|
|
|
applyFavorite(id);
|
|
}
|
|
|
|
void Reactions::incrementMyTag(const ReactionId &id, SavedSublist *sublist) {
|
|
if (sublist) {
|
|
incrementMyTag(id, nullptr);
|
|
}
|
|
auto &my = _myTags[sublist];
|
|
auto i = ranges::find(my.info, id, &MyTagInfo::id);
|
|
if (i == end(my.info)) {
|
|
my.info.push_back({ .id = id, .count = 0 });
|
|
i = end(my.info) - 1;
|
|
}
|
|
++i->count;
|
|
while (i != begin(my.info)) {
|
|
auto j = i - 1;
|
|
if (j->count >= i->count) {
|
|
break;
|
|
}
|
|
std::swap(*i, *j);
|
|
i = j;
|
|
}
|
|
scheduleMyTagsUpdate(sublist);
|
|
}
|
|
|
|
void Reactions::decrementMyTag(const ReactionId &id, SavedSublist *sublist) {
|
|
if (sublist) {
|
|
decrementMyTag(id, nullptr);
|
|
}
|
|
auto &my = _myTags[sublist];
|
|
auto i = ranges::find(my.info, id, &MyTagInfo::id);
|
|
if (i != end(my.info) && i->count > 0) {
|
|
--i->count;
|
|
while (i + 1 != end(my.info)) {
|
|
auto j = i + 1;
|
|
if (j->count <= i->count) {
|
|
break;
|
|
}
|
|
std::swap(*i, *j);
|
|
i = j;
|
|
}
|
|
}
|
|
scheduleMyTagsUpdate(sublist);
|
|
}
|
|
|
|
void Reactions::renameTag(const ReactionId &id, const QString &name) {
|
|
auto changed = false;
|
|
for (auto &[sublist, my] : _myTags) {
|
|
auto i = ranges::find(my.info, id, &MyTagInfo::id);
|
|
if (i == end(my.info) || i->title == name) {
|
|
continue;
|
|
}
|
|
i->title = name;
|
|
changed = true;
|
|
scheduleMyTagsUpdate(sublist);
|
|
}
|
|
if (!changed) {
|
|
return;
|
|
}
|
|
_myTagRenamed.fire_copy(id);
|
|
|
|
using Flag = MTPmessages_UpdateSavedReactionTag::Flag;
|
|
_owner->session().api().request(MTPmessages_UpdateSavedReactionTag(
|
|
MTP_flags(name.isEmpty() ? Flag(0) : Flag::f_title),
|
|
ReactionToMTP(id),
|
|
MTP_string(name)
|
|
)).send();
|
|
}
|
|
|
|
void Reactions::scheduleMyTagsUpdate(SavedSublist *sublist) {
|
|
auto &my = _myTags[sublist];
|
|
my.updateScheduled = true;
|
|
crl::on_main(&session(), [=] {
|
|
auto &my = _myTags[sublist];
|
|
if (!my.updateScheduled) {
|
|
return;
|
|
}
|
|
my.updateScheduled = false;
|
|
my.tags = resolveByInfos(my.info, _unresolvedMyTags, sublist);
|
|
_myTagsUpdated.fire_copy(sublist);
|
|
});
|
|
}
|
|
|
|
DocumentData *Reactions::chooseGenericAnimation(
|
|
not_null<DocumentData*> custom) const {
|
|
const auto sticker = custom->sticker();
|
|
const auto i = sticker
|
|
? ranges::find(
|
|
_available,
|
|
::Data::ReactionId{ { sticker->alt } },
|
|
&::Data::Reaction::id)
|
|
: end(_available);
|
|
if (i != end(_available) && i->aroundAnimation) {
|
|
const auto view = i->aroundAnimation->createMediaView();
|
|
view->checkStickerLarge();
|
|
if (view->loaded()) {
|
|
return i->aroundAnimation;
|
|
}
|
|
}
|
|
if (_genericAnimations.empty()) {
|
|
return nullptr;
|
|
}
|
|
auto copy = _genericAnimations;
|
|
ranges::shuffle(copy);
|
|
const auto first = copy.front();
|
|
const auto view = first->createMediaView();
|
|
view->checkStickerLarge();
|
|
if (view->loaded()) {
|
|
return first;
|
|
}
|
|
const auto k = ranges::find_if(copy, [&](not_null<DocumentData*> value) {
|
|
return value->createMediaView()->loaded();
|
|
});
|
|
return (k != end(copy)) ? (*k) : first;
|
|
}
|
|
|
|
void Reactions::applyFavorite(const ReactionId &id) {
|
|
if (_favoriteId != id) {
|
|
_favoriteId = id;
|
|
_favorite = resolveById(_favoriteId);
|
|
if (!_favorite && _unresolvedFavoriteId != _favoriteId) {
|
|
_unresolvedFavoriteId = _favoriteId;
|
|
resolve(_favoriteId);
|
|
}
|
|
_favoriteUpdated.fire({});
|
|
}
|
|
}
|
|
|
|
rpl::producer<> Reactions::topUpdates() const {
|
|
return _topUpdated.events();
|
|
}
|
|
|
|
rpl::producer<> Reactions::recentUpdates() const {
|
|
return _recentUpdated.events();
|
|
}
|
|
|
|
rpl::producer<> Reactions::defaultUpdates() const {
|
|
return _defaultUpdated.events();
|
|
}
|
|
|
|
rpl::producer<> Reactions::favoriteUpdates() const {
|
|
return _favoriteUpdated.events();
|
|
}
|
|
|
|
rpl::producer<> Reactions::myTagsUpdates() const {
|
|
return _myTagsUpdated.events(
|
|
) | rpl::filter(
|
|
!rpl::mappers::_1
|
|
) | rpl::to_empty;
|
|
}
|
|
|
|
rpl::producer<> Reactions::tagsUpdates() const {
|
|
return _tagsUpdated.events();
|
|
}
|
|
|
|
rpl::producer<ReactionId> Reactions::myTagRenamed() const {
|
|
return _myTagRenamed.events();
|
|
}
|
|
|
|
rpl::producer<> Reactions::effectsUpdates() const {
|
|
return _effectsUpdated.events();
|
|
}
|
|
|
|
void Reactions::preloadReactionImageFor(const ReactionId &emoji) {
|
|
if (!emoji.emoji().isEmpty()) {
|
|
preloadImageFor(emoji);
|
|
}
|
|
}
|
|
|
|
void Reactions::preloadEffectImageFor(EffectId id) {
|
|
if (id != kFakeEffectId) {
|
|
preloadImageFor({ DocumentId(id) });
|
|
}
|
|
}
|
|
|
|
void Reactions::preloadImageFor(const ReactionId &id) {
|
|
if (_images.contains(id)) {
|
|
return;
|
|
}
|
|
auto &set = _images.emplace(id).first->second;
|
|
set.effect = (id.custom() != 0);
|
|
auto &list = set.effect ? _effects : _available;
|
|
const auto i = ranges::find(list, id, &Reaction::id);
|
|
const auto document = (i == end(list))
|
|
? nullptr
|
|
: i->centerIcon
|
|
? i->centerIcon
|
|
: i->selectAnimation.get();
|
|
if (document || (set.effect && i != end(list))) {
|
|
if (!set.effect || i->centerIcon) {
|
|
loadImage(set, document, !i->centerIcon);
|
|
} else {
|
|
generateImage(set, i->title);
|
|
}
|
|
if (set.effect) {
|
|
preloadEffect(*i);
|
|
}
|
|
} else if (set.effect && !_waitingForEffects) {
|
|
_waitingForEffects = true;
|
|
refreshEffects();
|
|
} else if (!set.effect && !_waitingForReactions) {
|
|
_waitingForReactions = true;
|
|
refreshDefault();
|
|
}
|
|
}
|
|
|
|
void Reactions::preloadEffect(const Reaction &effect) {
|
|
if (effect.aroundAnimation) {
|
|
effect.aroundAnimation->createMediaView()->checkStickerLarge();
|
|
} else {
|
|
const auto premium = effect.selectAnimation;
|
|
premium->loadVideoThumbnail(premium->stickerSetOrigin());
|
|
}
|
|
}
|
|
|
|
void Reactions::preloadAnimationsFor(const ReactionId &id) {
|
|
const auto custom = id.custom();
|
|
const auto document = custom ? _owner->document(custom).get() : nullptr;
|
|
const auto customSticker = document ? document->sticker() : nullptr;
|
|
const auto findId = custom
|
|
? ReactionId{ { customSticker ? customSticker->alt : QString() } }
|
|
: id;
|
|
const auto i = ranges::find(_available, findId, &Reaction::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);
|
|
}
|
|
preload(i->aroundAnimation);
|
|
}
|
|
|
|
QImage Reactions::resolveReactionImageFor(const ReactionId &emoji) {
|
|
Expects(!emoji.custom());
|
|
|
|
return resolveImageFor(emoji);
|
|
}
|
|
|
|
QImage Reactions::resolveEffectImageFor(EffectId id) {
|
|
return (id == kFakeEffectId)
|
|
? QImage()
|
|
: resolveImageFor({ DocumentId(id) });
|
|
}
|
|
|
|
QImage Reactions::resolveImageFor(const ReactionId &id) {
|
|
auto i = _images.find(id);
|
|
if (i == end(_images)) {
|
|
preloadImageFor(id);
|
|
i = _images.find(id);
|
|
Assert(i != end(_images));
|
|
}
|
|
auto &set = i->second;
|
|
set.effect = (id.custom() != 0);
|
|
|
|
const auto resolve = [&](QImage &image, int size) {
|
|
const auto factor = style::DevicePixelRatio();
|
|
const auto frameSize = set.fromSelectAnimation
|
|
? (size / 2)
|
|
: size;
|
|
// Must not be colored to text.
|
|
image = set.icon->frame(QColor()).scaled(
|
|
frameSize * factor,
|
|
frameSize * factor,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
if (set.fromSelectAnimation) {
|
|
auto result = QImage(
|
|
size * factor,
|
|
size * factor,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
result.fill(Qt::transparent);
|
|
|
|
auto p = QPainter(&result);
|
|
p.drawImage(
|
|
(size - frameSize) * factor / 2,
|
|
(size - frameSize) * factor / 2,
|
|
image);
|
|
p.end();
|
|
|
|
std::swap(result, image);
|
|
}
|
|
image.setDevicePixelRatio(factor);
|
|
};
|
|
if (set.image.isNull() && set.icon) {
|
|
resolve(
|
|
set.image,
|
|
set.effect ? st::effectInfoImage : st::reactionInlineImage);
|
|
crl::async([icon = std::move(set.icon)]{});
|
|
}
|
|
return set.image;
|
|
}
|
|
|
|
void Reactions::resolveReactionImages() {
|
|
for (auto &[id, set] : _images) {
|
|
if (set.effect || !set.image.isNull() || set.icon || set.media) {
|
|
continue;
|
|
}
|
|
const auto i = ranges::find(_available, id, &Reaction::id);
|
|
const auto document = (i == end(_available))
|
|
? nullptr
|
|
: i->centerIcon
|
|
? i->centerIcon
|
|
: i->selectAnimation.get();
|
|
if (document) {
|
|
loadImage(set, document, !i->centerIcon);
|
|
} else {
|
|
LOG(("API Error: Reaction '%1' not found!"
|
|
).arg(ReactionIdToLog(id)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Reactions::resolveEffectImages() {
|
|
for (auto &[id, set] : _images) {
|
|
if (!set.effect || !set.image.isNull() || set.icon || set.media) {
|
|
continue;
|
|
}
|
|
const auto i = ranges::find(_effects, id, &Reaction::id);
|
|
const auto document = (i == end(_effects))
|
|
? nullptr
|
|
: i->centerIcon
|
|
? i->centerIcon
|
|
: nullptr;
|
|
if (document) {
|
|
loadImage(set, document, false);
|
|
} else if (i != end(_effects)) {
|
|
generateImage(set, i->title);
|
|
} else {
|
|
LOG(("API Error: Effect '%1' not found!"
|
|
).arg(ReactionIdToLog(id)));
|
|
}
|
|
if (i != end(_effects)) {
|
|
preloadEffect(*i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Reactions::loadImage(
|
|
ImageSet &set,
|
|
not_null<DocumentData*> document,
|
|
bool fromSelectAnimation) {
|
|
if (!set.image.isNull() || set.icon) {
|
|
return;
|
|
} else if (!set.media) {
|
|
if (!set.effect) {
|
|
set.fromSelectAnimation = fromSelectAnimation;
|
|
}
|
|
set.media = document->createMediaView();
|
|
set.media->checkStickerLarge();
|
|
}
|
|
if (set.media->loaded()) {
|
|
setAnimatedIcon(set);
|
|
} else if (!_imagesLoadLifetime) {
|
|
document->session().downloaderTaskFinished(
|
|
) | rpl::start_with_next([=] {
|
|
downloadTaskFinished();
|
|
}, _imagesLoadLifetime);
|
|
}
|
|
}
|
|
|
|
void Reactions::generateImage(ImageSet &set, const QString &emoji) {
|
|
Expects(set.effect);
|
|
|
|
const auto e = Ui::Emoji::Find(emoji);
|
|
Assert(e != nullptr);
|
|
|
|
const auto large = Ui::Emoji::GetSizeLarge();
|
|
const auto factor = style::DevicePixelRatio();
|
|
auto image = QImage(large, large, QImage::Format_ARGB32_Premultiplied);
|
|
image.setDevicePixelRatio(factor);
|
|
image.fill(Qt::transparent);
|
|
{
|
|
QPainter p(&image);
|
|
Ui::Emoji::Draw(p, e, large, 0, 0);
|
|
}
|
|
const auto size = st::effectInfoImage;
|
|
set.image = image.scaled(size * factor, size * factor);
|
|
set.image.setDevicePixelRatio(factor);
|
|
}
|
|
|
|
void Reactions::setAnimatedIcon(ImageSet &set) {
|
|
const auto size = style::ConvertScale(kSizeForDownscale);
|
|
set.icon = Ui::MakeAnimatedIcon({
|
|
.generator = DocumentIconFrameGenerator(set.media),
|
|
.sizeOverride = QSize(size, size),
|
|
.colorized = set.media->owner()->emojiUsesTextColor(),
|
|
});
|
|
set.media = nullptr;
|
|
}
|
|
|
|
void Reactions::downloadTaskFinished() {
|
|
auto hasOne = false;
|
|
for (auto &[emoji, set] : _images) {
|
|
if (!set.media) {
|
|
continue;
|
|
} else if (set.media->loaded()) {
|
|
setAnimatedIcon(set);
|
|
} else {
|
|
hasOne = true;
|
|
}
|
|
}
|
|
if (!hasOne) {
|
|
_imagesLoadLifetime.destroy();
|
|
}
|
|
}
|
|
|
|
void Reactions::requestTop() {
|
|
if (_topRequestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
_topRefreshTimer.cancel();
|
|
_topRequestId = api.request(MTPmessages_GetTopReactions(
|
|
MTP_int(kTopReactionsLimit),
|
|
MTP_long(_topHash)
|
|
)).done([=](const MTPmessages_Reactions &result) {
|
|
_topRequestId = 0;
|
|
result.match([&](const MTPDmessages_reactions &data) {
|
|
updateTop(data);
|
|
}, [](const MTPDmessages_reactionsNotModified&) {
|
|
});
|
|
}).fail([=] {
|
|
_topRequestId = 0;
|
|
_topHash = 0;
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::requestRecent() {
|
|
if (_recentRequestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
_recentRequestScheduled = false;
|
|
_recentRequestId = api.request(MTPmessages_GetRecentReactions(
|
|
MTP_int(kRecentReactionsLimit),
|
|
MTP_long(_recentHash)
|
|
)).done([=](const MTPmessages_Reactions &result) {
|
|
_recentRequestId = 0;
|
|
result.match([&](const MTPDmessages_reactions &data) {
|
|
updateRecent(data);
|
|
}, [](const MTPDmessages_reactionsNotModified&) {
|
|
});
|
|
}).fail([=] {
|
|
_recentRequestId = 0;
|
|
_recentHash = 0;
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::requestDefault() {
|
|
if (_defaultRequestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
_defaultRequestId = api.request(MTPmessages_GetAvailableReactions(
|
|
MTP_int(_defaultHash)
|
|
)).done([=](const MTPmessages_AvailableReactions &result) {
|
|
_defaultRequestId = 0;
|
|
result.match([&](const MTPDmessages_availableReactions &data) {
|
|
updateDefault(data);
|
|
}, [&](const MTPDmessages_availableReactionsNotModified &) {
|
|
});
|
|
}).fail([=] {
|
|
_defaultRequestId = 0;
|
|
_defaultHash = 0;
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::requestGeneric() {
|
|
if (_genericRequestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
_genericRequestId = api.request(MTPmessages_GetStickerSet(
|
|
MTP_inputStickerSetEmojiGenericAnimations(),
|
|
MTP_int(0) // hash
|
|
)).done([=](const MTPmessages_StickerSet &result) {
|
|
_genericRequestId = 0;
|
|
result.match([&](const MTPDmessages_stickerSet &data) {
|
|
updateGeneric(data);
|
|
}, [](const MTPDmessages_stickerSetNotModified &) {
|
|
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
|
|
});
|
|
}).fail([=] {
|
|
_genericRequestId = 0;
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::requestMyTags(SavedSublist *sublist) {
|
|
auto &my = _myTags[sublist];
|
|
if (my.requestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
my.requestScheduled = false;
|
|
using Flag = MTPmessages_GetSavedReactionTags::Flag;
|
|
my.requestId = api.request(MTPmessages_GetSavedReactionTags(
|
|
MTP_flags(sublist ? Flag::f_peer : Flag()),
|
|
(sublist ? sublist->peer()->input : MTP_inputPeerEmpty()),
|
|
MTP_long(my.hash)
|
|
)).done([=](const MTPmessages_SavedReactionTags &result) {
|
|
auto &my = _myTags[sublist];
|
|
my.requestId = 0;
|
|
result.match([&](const MTPDmessages_savedReactionTags &data) {
|
|
updateMyTags(sublist, data);
|
|
}, [](const MTPDmessages_savedReactionTagsNotModified&) {
|
|
});
|
|
}).fail([=] {
|
|
auto &my = _myTags[sublist];
|
|
my.requestId = 0;
|
|
my.hash = 0;
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::requestTags() {
|
|
if (_tagsRequestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
_tagsRequestId = api.request(MTPmessages_GetDefaultTagReactions(
|
|
MTP_long(_tagsHash)
|
|
)).done([=](const MTPmessages_Reactions &result) {
|
|
_tagsRequestId = 0;
|
|
result.match([&](const MTPDmessages_reactions &data) {
|
|
updateTags(data);
|
|
}, [](const MTPDmessages_reactionsNotModified&) {
|
|
});
|
|
}).fail([=] {
|
|
_tagsRequestId = 0;
|
|
_tagsHash = 0;
|
|
}).send();
|
|
|
|
}
|
|
|
|
void Reactions::requestEffects() {
|
|
if (_effectsRequestId) {
|
|
return;
|
|
}
|
|
auto &api = _owner->session().api();
|
|
_effectsRequestId = api.request(MTPmessages_GetAvailableEffects(
|
|
MTP_int(_effectsHash)
|
|
)).done([=](const MTPmessages_AvailableEffects &result) {
|
|
_effectsRequestId = 0;
|
|
result.match([&](const MTPDmessages_availableEffects &data) {
|
|
updateEffects(data);
|
|
}, [&](const MTPDmessages_availableEffectsNotModified &) {
|
|
});
|
|
}).fail([=] {
|
|
_effectsRequestId = 0;
|
|
_effectsHash = 0;
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::updateTop(const MTPDmessages_reactions &data) {
|
|
_topHash = data.vhash().v;
|
|
_topIds = ListFromMTP(data);
|
|
_top = resolveByIds(_topIds, _unresolvedTop);
|
|
_topUpdated.fire({});
|
|
}
|
|
|
|
void Reactions::updateRecent(const MTPDmessages_reactions &data) {
|
|
_recentHash = data.vhash().v;
|
|
_recentIds = ListFromMTP(data);
|
|
_recent = resolveByIds(_recentIds, _unresolvedRecent);
|
|
recentUpdated();
|
|
}
|
|
|
|
void Reactions::updateDefault(const MTPDmessages_availableReactions &data) {
|
|
_defaultHash = data.vhash().v;
|
|
|
|
const auto &list = data.vreactions().v;
|
|
const auto oldCache = base::take(_iconsCache);
|
|
const auto toCache = [&](DocumentData *document) {
|
|
if (document) {
|
|
_iconsCache.emplace(document, document->createMediaView());
|
|
}
|
|
};
|
|
_active.clear();
|
|
_available.clear();
|
|
_active.reserve(list.size());
|
|
_available.reserve(list.size());
|
|
_iconsCache.reserve(list.size() * 4);
|
|
for (const auto &reaction : list) {
|
|
if (const auto parsed = parse(reaction)) {
|
|
_available.push_back(*parsed);
|
|
if (parsed->active) {
|
|
_active.push_back(*parsed);
|
|
toCache(parsed->appearAnimation);
|
|
toCache(parsed->selectAnimation);
|
|
toCache(parsed->centerIcon);
|
|
toCache(parsed->aroundAnimation);
|
|
}
|
|
}
|
|
}
|
|
if (_waitingForReactions) {
|
|
_waitingForReactions = false;
|
|
resolveReactionImages();
|
|
}
|
|
defaultUpdated();
|
|
}
|
|
|
|
void Reactions::updateGeneric(const MTPDmessages_stickerSet &data) {
|
|
const auto oldCache = base::take(_genericCache);
|
|
const auto toCache = [&](not_null<DocumentData*> document) {
|
|
if (document->sticker()) {
|
|
_genericAnimations.push_back(document);
|
|
_genericCache.emplace(document, document->createMediaView());
|
|
}
|
|
};
|
|
const auto &list = data.vdocuments().v;
|
|
_genericAnimations.clear();
|
|
_genericAnimations.reserve(list.size());
|
|
_genericCache.reserve(list.size());
|
|
for (const auto &sticker : data.vdocuments().v) {
|
|
toCache(_owner->processDocument(sticker));
|
|
}
|
|
if (!_genericCache.empty()) {
|
|
_genericCache.front().second->checkStickerLarge();
|
|
}
|
|
}
|
|
|
|
void Reactions::updateMyTags(
|
|
SavedSublist *sublist,
|
|
const MTPDmessages_savedReactionTags &data) {
|
|
auto &my = _myTags[sublist];
|
|
my.hash = data.vhash().v;
|
|
auto list = ListFromMTP(data);
|
|
auto renamed = base::flat_set<ReactionId>();
|
|
if (!sublist) {
|
|
for (const auto &info : list) {
|
|
const auto j = ranges::find(my.info, info.id, &MyTagInfo::id);
|
|
const auto was = (j != end(my.info)) ? j->title : QString();
|
|
if (info.title != was) {
|
|
renamed.emplace(info.id);
|
|
}
|
|
}
|
|
}
|
|
my.info = std::move(list);
|
|
my.tags = resolveByInfos(my.info, _unresolvedMyTags, sublist);
|
|
_myTagsUpdated.fire_copy(sublist);
|
|
for (const auto &id : renamed) {
|
|
_myTagRenamed.fire_copy(id);
|
|
}
|
|
}
|
|
|
|
void Reactions::updateTags(const MTPDmessages_reactions &data) {
|
|
_tagsHash = data.vhash().v;
|
|
_tagsIds = ListFromMTP(data);
|
|
_tags = resolveByIds(_tagsIds, _unresolvedTags);
|
|
_tagsUpdated.fire({});
|
|
}
|
|
|
|
void Reactions::updateEffects(const MTPDmessages_availableEffects &data) {
|
|
_effectsHash = data.vhash().v;
|
|
|
|
const auto &list = data.veffects().v;
|
|
const auto toCache = [&](DocumentData *document) {
|
|
if (document) {
|
|
_iconsCache.emplace(document, document->createMediaView());
|
|
}
|
|
};
|
|
for (const auto &document : data.vdocuments().v) {
|
|
toCache(_owner->processDocument(document));
|
|
}
|
|
_effects.clear();
|
|
_effects.reserve(list.size());
|
|
for (const auto &effect : list) {
|
|
if (const auto parsed = parse(effect)) {
|
|
_effects.push_back(*parsed);
|
|
}
|
|
}
|
|
if (_waitingForEffects) {
|
|
_waitingForEffects = false;
|
|
resolveEffectImages();
|
|
}
|
|
effectsUpdated();
|
|
}
|
|
|
|
void Reactions::recentUpdated() {
|
|
_topRefreshTimer.callOnce(kTopRequestDelay);
|
|
_recentUpdated.fire({});
|
|
}
|
|
|
|
void Reactions::defaultUpdated() {
|
|
refreshTop();
|
|
refreshRecent();
|
|
if (_genericAnimations.empty()) {
|
|
requestGeneric();
|
|
}
|
|
refreshMyTags();
|
|
refreshTags();
|
|
refreshEffects();
|
|
_defaultUpdated.fire({});
|
|
}
|
|
|
|
void Reactions::myTagsUpdated() {
|
|
if (_genericAnimations.empty()) {
|
|
requestGeneric();
|
|
}
|
|
_myTagsUpdated.fire({});
|
|
}
|
|
|
|
void Reactions::tagsUpdated() {
|
|
if (_genericAnimations.empty()) {
|
|
requestGeneric();
|
|
}
|
|
_tagsUpdated.fire({});
|
|
}
|
|
|
|
void Reactions::effectsUpdated() {
|
|
_effectsUpdated.fire({});
|
|
}
|
|
|
|
not_null<CustomEmojiManager::Listener*> Reactions::resolveListener() {
|
|
return static_cast<CustomEmojiManager::Listener*>(this);
|
|
}
|
|
|
|
void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
|
|
const auto id = ReactionId{ { document->id } };
|
|
const auto favorite = (_unresolvedFavoriteId == id);
|
|
const auto i = _unresolvedTop.find(id);
|
|
const auto top = (i != end(_unresolvedTop));
|
|
const auto j = _unresolvedRecent.find(id);
|
|
const auto recent = (j != end(_unresolvedRecent));
|
|
const auto k = _unresolvedMyTags.find(id);
|
|
const auto myTagSublists = (k != end(_unresolvedMyTags))
|
|
? base::take(k->second)
|
|
: base::flat_set<SavedSublist*>();
|
|
const auto l = _unresolvedTags.find(id);
|
|
const auto tag = (l != end(_unresolvedTags));
|
|
if (favorite) {
|
|
_unresolvedFavoriteId = ReactionId();
|
|
_favorite = resolveById(_favoriteId);
|
|
}
|
|
if (top) {
|
|
_unresolvedTop.erase(i);
|
|
_top = resolveByIds(_topIds, _unresolvedTop);
|
|
}
|
|
if (recent) {
|
|
_unresolvedRecent.erase(j);
|
|
_recent = resolveByIds(_recentIds, _unresolvedRecent);
|
|
}
|
|
if (!myTagSublists.empty()) {
|
|
_unresolvedMyTags.erase(k);
|
|
for (const auto &sublist : myTagSublists) {
|
|
auto &my = _myTags[sublist];
|
|
my.tags = resolveByInfos(my.info, _unresolvedMyTags, sublist);
|
|
}
|
|
}
|
|
if (tag) {
|
|
_unresolvedTags.erase(l);
|
|
_tags = resolveByIds(_tagsIds, _unresolvedTags);
|
|
}
|
|
if (favorite) {
|
|
_favoriteUpdated.fire({});
|
|
}
|
|
if (top) {
|
|
_topUpdated.fire({});
|
|
}
|
|
if (recent) {
|
|
_recentUpdated.fire({});
|
|
}
|
|
for (const auto &sublist : myTagSublists) {
|
|
_myTagsUpdated.fire_copy(sublist);
|
|
}
|
|
if (tag) {
|
|
_tagsUpdated.fire({});
|
|
}
|
|
}
|
|
|
|
std::optional<Reaction> Reactions::resolveById(const ReactionId &id) {
|
|
if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
|
|
const auto i = ranges::find(_available, id, &Reaction::id);
|
|
if (i != end(_available)) {
|
|
return *i;
|
|
}
|
|
} else if (const auto customId = id.custom()) {
|
|
const auto document = _owner->document(customId);
|
|
if (document->sticker()) {
|
|
return CustomReaction(document);
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::vector<Reaction> Reactions::resolveByIds(
|
|
const std::vector<ReactionId> &ids,
|
|
base::flat_set<ReactionId> &unresolved) {
|
|
auto result = std::vector<Reaction>();
|
|
result.reserve(ids.size());
|
|
for (const auto &id : ids) {
|
|
if (const auto resolved = resolveById(id)) {
|
|
result.push_back(*resolved);
|
|
} else if (unresolved.emplace(id).second) {
|
|
resolve(id);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::optional<Reaction> Reactions::resolveByInfo(
|
|
const MyTagInfo &info,
|
|
SavedSublist *sublist) {
|
|
const auto withInfo = [&](Reaction reaction) {
|
|
reaction.count = info.count;
|
|
reaction.title = sublist ? myTagTitle(reaction.id) : info.title;
|
|
return reaction;
|
|
};
|
|
if (const auto emoji = info.id.emoji(); !emoji.isEmpty()) {
|
|
const auto i = ranges::find(_available, info.id, &Reaction::id);
|
|
if (i != end(_available)) {
|
|
return withInfo(*i);
|
|
}
|
|
} else if (const auto customId = info.id.custom()) {
|
|
const auto document = _owner->document(customId);
|
|
if (document->sticker()) {
|
|
return withInfo(CustomReaction(document));
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::vector<Reaction> Reactions::resolveByInfos(
|
|
const std::vector<MyTagInfo> &infos,
|
|
base::flat_map<
|
|
ReactionId,
|
|
base::flat_set<SavedSublist*>> &unresolved,
|
|
SavedSublist *sublist) {
|
|
auto result = std::vector<Reaction>();
|
|
result.reserve(infos.size());
|
|
for (const auto &tag : infos) {
|
|
if (auto resolved = resolveByInfo(tag, sublist)) {
|
|
result.push_back(*resolved);
|
|
} else if (const auto i = unresolved.find(tag.id)
|
|
; i != end(unresolved)) {
|
|
i->second.emplace(sublist);
|
|
} else {
|
|
unresolved[tag.id].emplace(sublist);
|
|
resolve(tag.id);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Reactions::resolve(const ReactionId &id) {
|
|
if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
|
|
refreshDefault();
|
|
} else if (const auto customId = id.custom()) {
|
|
_owner->customEmojiManager().resolve(
|
|
customId,
|
|
resolveListener());
|
|
}
|
|
}
|
|
|
|
std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
|
|
const auto &data = entry.data();
|
|
const auto emoji = qs(data.vreaction());
|
|
const auto known = (Ui::Emoji::Find(emoji) != nullptr);
|
|
if (!known) {
|
|
LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji));
|
|
return std::nullopt;
|
|
}
|
|
return std::make_optional(Reaction{
|
|
.id = ReactionId{ emoji },
|
|
.title = qs(data.vtitle()),
|
|
//.staticIcon = _owner->processDocument(data.vstatic_icon()),
|
|
.appearAnimation = _owner->processDocument(
|
|
data.vappear_animation()),
|
|
.selectAnimation = _owner->processDocument(
|
|
data.vselect_animation()),
|
|
//.activateAnimation = _owner->processDocument(
|
|
// data.vactivate_animation()),
|
|
//.activateEffects = _owner->processDocument(
|
|
// data.veffect_animation()),
|
|
.centerIcon = (data.vcenter_icon()
|
|
? _owner->processDocument(*data.vcenter_icon()).get()
|
|
: nullptr),
|
|
.aroundAnimation = (data.varound_animation()
|
|
? _owner->processDocument(*data.varound_animation()).get()
|
|
: nullptr),
|
|
.active = !data.is_inactive(),
|
|
});
|
|
}
|
|
|
|
std::optional<Reaction> Reactions::parse(const MTPAvailableEffect &entry) {
|
|
const auto &data = entry.data();
|
|
const auto emoji = qs(data.vemoticon());
|
|
const auto known = (Ui::Emoji::Find(emoji) != nullptr);
|
|
if (!known) {
|
|
LOG(("API Error: Unknown emoji in effects: %1").arg(emoji));
|
|
return std::nullopt;
|
|
}
|
|
const auto id = DocumentId(data.vid().v);
|
|
const auto stickerId = data.veffect_sticker_id().v;
|
|
const auto document = _owner->document(stickerId);
|
|
if (!document->sticker()) {
|
|
LOG(("API Error: Bad sticker in effects: %1").arg(stickerId));
|
|
return std::nullopt;
|
|
}
|
|
const auto aroundId = data.veffect_animation_id().value_or_empty();
|
|
const auto around = aroundId
|
|
? _owner->document(aroundId).get()
|
|
: nullptr;
|
|
if (around && !around->sticker()) {
|
|
LOG(("API Error: Bad sticker in effects around: %1").arg(aroundId));
|
|
return std::nullopt;
|
|
}
|
|
const auto iconId = data.vstatic_icon_id().value_or_empty();
|
|
const auto icon = iconId ? _owner->document(iconId).get() : nullptr;
|
|
if (icon && !icon->sticker()) {
|
|
LOG(("API Error: Bad sticker in effects icon: %1").arg(iconId));
|
|
return std::nullopt;
|
|
}
|
|
return std::make_optional(Reaction{
|
|
.id = ReactionId{ id },
|
|
.title = emoji,
|
|
.appearAnimation = document,
|
|
.selectAnimation = document,
|
|
.centerIcon = icon,
|
|
.aroundAnimation = around,
|
|
.active = true,
|
|
.effect = true,
|
|
.premium = data.is_premium_required(),
|
|
});
|
|
}
|
|
|
|
void Reactions::send(not_null<HistoryItem*> item, bool addToRecent) {
|
|
const auto id = item->fullId();
|
|
auto &api = _owner->session().api();
|
|
auto i = _sentRequests.find(id);
|
|
if (i != end(_sentRequests)) {
|
|
api.request(i->second).cancel();
|
|
} else {
|
|
i = _sentRequests.emplace(id).first;
|
|
}
|
|
const auto chosen = item->chosenReactions();
|
|
using Flag = MTPmessages_SendReaction::Flag;
|
|
const auto flags = (chosen.empty() ? Flag(0) : Flag::f_reaction)
|
|
| (addToRecent ? Flag::f_add_to_recent : Flag(0));
|
|
i->second = api.request(MTPmessages_SendReaction(
|
|
MTP_flags(flags),
|
|
item->history()->peer->input,
|
|
MTP_int(id.msg),
|
|
MTP_vector<MTPReaction>(chosen | ranges::views::transform(
|
|
ReactionToMTP
|
|
) | ranges::to<QVector<MTPReaction>>())
|
|
)).done([=](const MTPUpdates &result) {
|
|
_sentRequests.remove(id);
|
|
_owner->session().api().applyUpdates(result);
|
|
}).fail([=](const MTP::Error &error) {
|
|
_sentRequests.remove(id);
|
|
}).send();
|
|
}
|
|
|
|
void Reactions::poll(not_null<HistoryItem*> item, crl::time now) {
|
|
// Group them by one second.
|
|
const auto last = item->lastReactionsRefreshTime();
|
|
const auto grouped = ((last + 999) / 1000) * 1000;
|
|
if (!grouped || item->history()->peer->isUser()) {
|
|
// First reaction always edits message.
|
|
return;
|
|
} else if (const auto left = grouped + kPollEach - now; left > 0) {
|
|
if (!_repaintItems.contains(item)) {
|
|
_repaintItems.emplace(item, grouped + kPollEach);
|
|
if (!_repaintTimer.isActive()
|
|
|| _repaintTimer.remainingTime() > left) {
|
|
_repaintTimer.callOnce(left);
|
|
}
|
|
}
|
|
} else if (!_pollingItems.contains(item)) {
|
|
if (_pollItems.empty() && !_pollRequestId) {
|
|
crl::on_main(&_owner->session(), [=] {
|
|
pollCollected();
|
|
});
|
|
}
|
|
_pollItems.emplace(item);
|
|
}
|
|
}
|
|
|
|
void Reactions::updateAllInHistory(not_null<PeerData*> peer, bool enabled) {
|
|
if (const auto history = _owner->historyLoaded(peer)) {
|
|
history->reactionsEnabledChanged(enabled);
|
|
}
|
|
}
|
|
|
|
void Reactions::clearTemporary() {
|
|
_temporary.clear();
|
|
}
|
|
|
|
Reaction *Reactions::lookupTemporary(const ReactionId &id) {
|
|
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()) {
|
|
if (const auto i = _temporary.find(customId); i != end(_temporary)) {
|
|
return &i->second;
|
|
}
|
|
const auto document = _owner->document(customId);
|
|
if (document->sticker()) {
|
|
return &_temporary.emplace(
|
|
customId,
|
|
CustomReaction(document)).first->second;
|
|
}
|
|
_owner->customEmojiManager().resolve(
|
|
customId,
|
|
resolveListener());
|
|
return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
rpl::producer<std::vector<Reaction>> Reactions::myTagsValue(
|
|
SavedSublist *sublist) {
|
|
refreshMyTags(sublist);
|
|
const auto list = [=] {
|
|
return _myTags[sublist].tags;
|
|
};
|
|
return rpl::single(
|
|
list()
|
|
) | rpl::then(_myTagsUpdated.events(
|
|
) | rpl::filter(
|
|
rpl::mappers::_1 == sublist
|
|
) | rpl::map(list));
|
|
}
|
|
|
|
void Reactions::repaintCollected() {
|
|
const auto now = crl::now();
|
|
auto closest = crl::time();
|
|
for (auto i = begin(_repaintItems); i != end(_repaintItems);) {
|
|
if (i->second <= now) {
|
|
_owner->requestItemRepaint(i->first);
|
|
i = _repaintItems.erase(i);
|
|
} else {
|
|
if (!closest || i->second < closest) {
|
|
closest = i->second;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
if (closest) {
|
|
_repaintTimer.callOnce(closest - now);
|
|
}
|
|
}
|
|
|
|
void Reactions::pollCollected() {
|
|
auto toRequest = base::flat_map<not_null<PeerData*>, QVector<MTPint>>();
|
|
_pollingItems = std::move(_pollItems);
|
|
for (const auto &item : _pollingItems) {
|
|
toRequest[item->history()->peer].push_back(MTP_int(item->id));
|
|
}
|
|
auto &api = _owner->session().api();
|
|
for (const auto &[peer, ids] : toRequest) {
|
|
const auto finalize = [=] {
|
|
const auto now = crl::now();
|
|
for (const auto &item : base::take(_pollingItems)) {
|
|
const auto last = item->lastReactionsRefreshTime();
|
|
if (last && last + kPollEach <= now) {
|
|
item->updateReactions(nullptr);
|
|
}
|
|
}
|
|
_pollRequestId = 0;
|
|
if (!_pollItems.empty()) {
|
|
crl::on_main(&_owner->session(), [=] {
|
|
pollCollected();
|
|
});
|
|
}
|
|
};
|
|
_pollRequestId = api.request(MTPmessages_GetMessagesReactions(
|
|
peer->input,
|
|
MTP_vector<MTPint>(ids)
|
|
)).done([=](const MTPUpdates &result) {
|
|
_owner->session().api().applyUpdates(result);
|
|
finalize();
|
|
}).fail([=] {
|
|
finalize();
|
|
}).send();
|
|
}
|
|
}
|
|
|
|
bool Reactions::sending(not_null<HistoryItem*> item) const {
|
|
return _sentRequests.contains(item->fullId());
|
|
}
|
|
|
|
bool Reactions::HasUnread(const MTPMessageReactions &data) {
|
|
return data.match([&](const MTPDmessageReactions &data) {
|
|
if (const auto &recent = data.vrecent_reactions()) {
|
|
for (const auto &one : recent->v) {
|
|
if (one.match([&](const MTPDmessagePeerReaction &data) {
|
|
return data.is_unread();
|
|
})) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
void Reactions::CheckUnknownForUnread(
|
|
not_null<Session*> owner,
|
|
const MTPMessage &message) {
|
|
message.match([&](const MTPDmessage &data) {
|
|
if (data.vreactions() && HasUnread(*data.vreactions())) {
|
|
const auto peerId = peerFromMTP(data.vpeer_id());
|
|
if (const auto history = owner->historyLoaded(peerId)) {
|
|
owner->histories().requestDialogEntry(history);
|
|
}
|
|
}
|
|
}, [](const auto &) {
|
|
});
|
|
}
|
|
|
|
MessageReactions::MessageReactions(not_null<HistoryItem*> item)
|
|
: _item(item) {
|
|
}
|
|
|
|
void MessageReactions::add(const ReactionId &id, bool addToRecent) {
|
|
Expects(!id.empty());
|
|
|
|
const auto history = _item->history();
|
|
const auto myLimit = SentReactionsLimit(_item);
|
|
if (ranges::contains(chosen(), id)) {
|
|
return;
|
|
}
|
|
auto my = 0;
|
|
const auto tags = _item->reactionsAreTags();
|
|
if (tags) {
|
|
const auto sublist = _item->savedSublist();
|
|
history->owner().reactions().incrementMyTag(id, sublist);
|
|
}
|
|
_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
|
|
const auto removing = one.my && (my == myLimit || ++my == myLimit);
|
|
if (!removing) {
|
|
return false;
|
|
}
|
|
one.my = false;
|
|
const auto removed = !--one.count;
|
|
const auto j = _recent.find(one.id);
|
|
if (j != end(_recent)) {
|
|
if (removed) {
|
|
j->second.clear();
|
|
_recent.erase(j);
|
|
} else {
|
|
j->second.erase(
|
|
ranges::remove(j->second, true, &RecentReaction::my),
|
|
end(j->second));
|
|
if (j->second.empty()) {
|
|
_recent.erase(j);
|
|
}
|
|
}
|
|
}
|
|
if (tags) {
|
|
const auto sublist = _item->savedSublist();
|
|
history->owner().reactions().decrementMyTag(one.id, sublist);
|
|
}
|
|
return removed;
|
|
}), end(_list));
|
|
const auto peer = history->peer;
|
|
if (_item->canViewReactions() || peer->isUser()) {
|
|
auto &list = _recent[id];
|
|
const auto from = peer->session().sendAsPeers().resolveChosen(peer);
|
|
list.insert(begin(list), RecentReaction{
|
|
.peer = from,
|
|
.my = true,
|
|
});
|
|
}
|
|
const auto i = ranges::find(_list, id, &MessageReaction::id);
|
|
if (i != end(_list)) {
|
|
i->my = true;
|
|
++i->count;
|
|
std::rotate(i, i + 1, end(_list));
|
|
} else {
|
|
_list.push_back({ .id = id, .count = 1, .my = true });
|
|
}
|
|
auto &owner = history->owner();
|
|
owner.reactions().send(_item, addToRecent);
|
|
owner.notifyItemDataChange(_item);
|
|
}
|
|
|
|
void MessageReactions::remove(const ReactionId &id) {
|
|
const auto history = _item->history();
|
|
const auto self = history->session().user();
|
|
const auto i = ranges::find(_list, id, &MessageReaction::id);
|
|
const auto j = _recent.find(id);
|
|
if (i == end(_list)) {
|
|
Assert(j == end(_recent));
|
|
return;
|
|
} else if (!i->my) {
|
|
Assert(j == end(_recent)
|
|
|| !ranges::contains(j->second, self, &RecentReaction::peer));
|
|
return;
|
|
}
|
|
i->my = false;
|
|
const auto tags = _item->reactionsAreTags();
|
|
const auto removed = !--i->count;
|
|
if (removed) {
|
|
_list.erase(i);
|
|
}
|
|
if (j != end(_recent)) {
|
|
if (removed) {
|
|
j->second.clear();
|
|
_recent.erase(j);
|
|
} else {
|
|
j->second.erase(
|
|
ranges::remove(j->second, true, &RecentReaction::my),
|
|
end(j->second));
|
|
if (j->second.empty()) {
|
|
_recent.erase(j);
|
|
}
|
|
}
|
|
}
|
|
if (tags) {
|
|
const auto sublist = _item->savedSublist();
|
|
history->owner().reactions().decrementMyTag(id, sublist);
|
|
}
|
|
auto &owner = history->owner();
|
|
owner.reactions().send(_item, false);
|
|
owner.notifyItemDataChange(_item);
|
|
}
|
|
|
|
bool MessageReactions::checkIfChanged(
|
|
const QVector<MTPReactionCount> &list,
|
|
const QVector<MTPMessagePeerReaction> &recent,
|
|
bool min) const {
|
|
auto &owner = _item->history()->owner();
|
|
if (owner.reactions().sending(_item)) {
|
|
// We'll apply non-stale data from the request response.
|
|
return false;
|
|
}
|
|
auto existing = base::flat_set<ReactionId>();
|
|
for (const auto &count : list) {
|
|
const auto changed = count.match([&](const MTPDreactionCount &data) {
|
|
const auto id = ReactionFromMTP(data.vreaction());
|
|
const auto nowCount = data.vcount().v;
|
|
const auto i = ranges::find(_list, id, &MessageReaction::id);
|
|
const auto wasCount = (i != end(_list)) ? i->count : 0;
|
|
if (wasCount != nowCount) {
|
|
return true;
|
|
}
|
|
existing.emplace(id);
|
|
return false;
|
|
});
|
|
if (changed) {
|
|
return true;
|
|
}
|
|
}
|
|
for (const auto &reaction : _list) {
|
|
if (!existing.contains(reaction.id)) {
|
|
return true;
|
|
}
|
|
}
|
|
auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();
|
|
for (const auto &reaction : recent) {
|
|
reaction.match([&](const MTPDmessagePeerReaction &data) {
|
|
const auto id = ReactionFromMTP(data.vreaction());
|
|
if (!ranges::contains(_list, id, &MessageReaction::id)) {
|
|
return;
|
|
}
|
|
const auto peerId = peerFromMTP(data.vpeer_id());
|
|
const auto peer = owner.peer(peerId);
|
|
const auto my = IsMyRecent(data, id, peer, _recent, min);
|
|
parsed[id].push_back({
|
|
.peer = peer,
|
|
.unread = data.is_unread(),
|
|
.big = data.is_big(),
|
|
.my = my,
|
|
});
|
|
});
|
|
}
|
|
return !ranges::equal(_recent, parsed, [](
|
|
const auto &a,
|
|
const auto &b) {
|
|
return ranges::equal(a.second, b.second, [](
|
|
const RecentReaction &a,
|
|
const RecentReaction &b) {
|
|
return (a.peer == b.peer) && (a.big == b.big) && (a.my == b.my);
|
|
});
|
|
});
|
|
}
|
|
|
|
bool MessageReactions::change(
|
|
const QVector<MTPReactionCount> &list,
|
|
const QVector<MTPMessagePeerReaction> &recent,
|
|
bool min) {
|
|
auto &owner = _item->history()->owner();
|
|
if (owner.reactions().sending(_item)) {
|
|
// We'll apply non-stale data from the request response.
|
|
return false;
|
|
}
|
|
auto changed = false;
|
|
auto existing = base::flat_set<ReactionId>();
|
|
auto order = base::flat_map<ReactionId, int>();
|
|
for (const auto &count : list) {
|
|
count.match([&](const MTPDreactionCount &data) {
|
|
const auto id = ReactionFromMTP(data.vreaction());
|
|
const auto &chosen = data.vchosen_order();
|
|
if (!min && chosen) {
|
|
order[id] = chosen->v;
|
|
}
|
|
const auto i = ranges::find(_list, id, &MessageReaction::id);
|
|
const auto nowCount = data.vcount().v;
|
|
if (i == end(_list)) {
|
|
changed = true;
|
|
_list.push_back({
|
|
.id = id,
|
|
.count = nowCount,
|
|
.my = (!min && chosen)
|
|
});
|
|
} else {
|
|
const auto nowMy = min ? i->my : chosen.has_value();
|
|
if (i->count != nowCount || i->my != nowMy) {
|
|
i->count = nowCount;
|
|
i->my = nowMy;
|
|
changed = true;
|
|
}
|
|
}
|
|
existing.emplace(id);
|
|
});
|
|
}
|
|
if (!min && !order.empty()) {
|
|
const auto minimal = std::numeric_limits<int>::min();
|
|
const auto proj = [&](const MessageReaction &reaction) {
|
|
return reaction.my ? order[reaction.id] : minimal;
|
|
};
|
|
const auto correctOrder = [&] {
|
|
auto previousOrder = minimal;
|
|
for (const auto &reaction : _list) {
|
|
const auto nowOrder = proj(reaction);
|
|
if (nowOrder < previousOrder) {
|
|
return false;
|
|
}
|
|
previousOrder = nowOrder;
|
|
}
|
|
return true;
|
|
}();
|
|
if (!correctOrder) {
|
|
changed = true;
|
|
ranges::sort(_list, std::less(), proj);
|
|
}
|
|
}
|
|
if (_list.size() != existing.size()) {
|
|
changed = true;
|
|
for (auto i = begin(_list); i != end(_list);) {
|
|
if (!existing.contains(i->id)) {
|
|
i = _list.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();
|
|
for (const auto &reaction : recent) {
|
|
reaction.match([&](const MTPDmessagePeerReaction &data) {
|
|
const auto id = ReactionFromMTP(data.vreaction());
|
|
const auto i = ranges::find(_list, id, &MessageReaction::id);
|
|
if (i == end(_list)) {
|
|
return;
|
|
}
|
|
auto &list = parsed[id];
|
|
if (list.size() >= i->count) {
|
|
return;
|
|
}
|
|
const auto peerId = peerFromMTP(data.vpeer_id());
|
|
const auto peer = owner.peer(peerId);
|
|
const auto my = IsMyRecent(data, id, peer, _recent, min);
|
|
list.push_back({
|
|
.peer = peer,
|
|
.unread = data.is_unread(),
|
|
.big = data.is_big(),
|
|
.my = my,
|
|
});
|
|
});
|
|
}
|
|
if (_recent != parsed) {
|
|
_recent = std::move(parsed);
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
const std::vector<MessageReaction> &MessageReactions::list() const {
|
|
return _list;
|
|
}
|
|
|
|
auto MessageReactions::recent() const
|
|
-> const base::flat_map<ReactionId, std::vector<RecentReaction>> & {
|
|
return _recent;
|
|
}
|
|
|
|
bool MessageReactions::empty() const {
|
|
return _list.empty();
|
|
}
|
|
|
|
bool MessageReactions::hasUnread() const {
|
|
for (auto &[emoji, list] : _recent) {
|
|
if (ranges::contains(list, true, &RecentReaction::unread)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MessageReactions::markRead() {
|
|
for (auto &[emoji, list] : _recent) {
|
|
for (auto &reaction : list) {
|
|
reaction.unread = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<ReactionId> MessageReactions::chosen() const {
|
|
return _list
|
|
| ranges::views::filter(&MessageReaction::my)
|
|
| ranges::views::transform(&MessageReaction::id)
|
|
| ranges::to_vector;
|
|
}
|
|
|
|
} // namespace Data
|