Request top and recent reactions.

This commit is contained in:
John Preston 2022-08-24 11:42:01 +03:00
parent f8c962712b
commit 31db1804c8
11 changed files with 348 additions and 75 deletions

View File

@ -2387,6 +2387,14 @@ void Updates::feedUpdate(const MTPUpdate &update) {
}
} break;
case mtpc_updateRecentEmojiStatuses: {
// #TODO emoji_status
} break;
case mtpc_updateRecentReactions: {
session().data().reactions().refreshRecentDelayed();
} break;
////// Cloud saved GIFs
case mtpc_updateSavedGifs: {
session().data().stickers().setLastSavedGifsUpdate(0);

View File

@ -174,7 +174,7 @@ void SaveAllowedReactions(
}).fail([=](const MTP::Error &error) {
if (error.type() == qstr("REACTION_INVALID")) {
peer->updateFullForced();
peer->owner().reactions().refresh();
peer->owner().reactions().refreshDefault();
}
}).send();
}

View File

@ -393,8 +393,8 @@ void ReactionsSettingsBox(
const auto &reactions = controller->session().data().reactions();
const auto state = box->lifetime().make_state<State>();
state->selectedEmoji = v::is<QString>(reactions.favorite().data)
? v::get<QString>(reactions.favorite().data)
state->selectedEmoji = v::is<QString>(reactions.favoriteId().data)
? v::get<QString>(reactions.favoriteId().data)
: QString();
const auto pinnedToTop = box->setPinnedToTopContent(
@ -485,7 +485,7 @@ void ReactionsSettingsBox(
box->addButton(tr::lng_settings_save(), [=] {
const auto &data = controller->session().data();
const auto selected = state->selectedEmoji.current();
if (data.reactions().favorite() != Data::ReactionId{ selected }) {
if (data.reactions().favoriteId() != Data::ReactionId{ selected }) {
data.reactions().setFavorite(Data::ReactionId{ selected });
}
box->closeBox();

View File

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/image/image_location_factory.h"
#include "mtproto/mtproto_config.h"
#include "base/timer_rpl.h"
#include "base/call_delayed.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
@ -34,6 +35,10 @@ 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 kTopRequestDelay = 60 * crl::time(1000);
constexpr auto kTopReactionsLimit = 10;
[[nodiscard]] QString ReactionIdToLog(const ReactionId &id) {
if (const auto custom = id.custom()) {
@ -42,6 +47,22 @@ constexpr auto kSizeForDownscale = 64;
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;
}
} // namespace
PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
@ -96,7 +117,7 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
}
const auto i = ranges::find(
result.recent,
reactions->favorite(),
reactions->favoriteId(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
@ -106,13 +127,14 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _topRefreshTimer([=] { refreshTop(); })
, _repaintTimer([=] { repaintCollected(); }) {
refresh();
refreshDefault();
base::timer_each(
kRefreshFullListEach
) | rpl::start_with_next([=] {
refresh();
refreshDefault();
}, _lifetime);
_owner->session().changes().messageUpdates(
@ -132,56 +154,102 @@ Reactions::Reactions(not_null<Session*> owner)
? ReactionId{ DocumentId(config.reactionDefaultCustom) }
: ReactionId{ config.reactionDefaultEmoji };
}) | rpl::filter([=](const ReactionId &id) {
return (_favorite != id) && !_saveFaveRequestId;
return !_saveFaveRequestId;
}) | rpl::start_with_next([=](ReactionId &&id) {
_favorite = std::move(id);
_updated.fire({});
applyFavorite(id);
}, _lifetime);
}
Reactions::~Reactions() = default;
void Reactions::refresh() {
request();
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();
}
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;
}
Unexpected("Type in Reactions::list.");
}
ReactionId Reactions::favorite() const {
return _favorite;
ReactionId Reactions::favoriteId() const {
return _favoriteId;
}
void Reactions::setFavorite(const ReactionId &emoji) {
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(emoji)
ReactionToMTP(id)
)).done([=] {
_saveFaveRequestId = 0;
}).fail([=] {
_saveFaveRequestId = 0;
}).send();
if (_favorite != emoji) {
_favorite = emoji;
_updated.fire({});
applyFavorite(id);
}
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::updates() const {
return _updated.events();
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();
}
void Reactions::preloadImageFor(const ReactionId &id) {
if (_images.contains(id)) {
if (_images.contains(id) || id.emoji().isEmpty()) {
return;
}
auto &set = _images.emplace(id).first->second;
@ -195,7 +263,7 @@ void Reactions::preloadImageFor(const ReactionId &id) {
loadImage(set, document, !i->centerIcon);
} else if (!_waitingForList) {
_waitingForList = true;
refresh();
refreshRecent();
}
}
@ -265,16 +333,6 @@ QImage Reactions::resolveImageFor(
Unexpected("ImageSize in Reactions::resolveImageFor.");
}
std::unique_ptr<Ui::Text::CustomEmoji> Reactions::resolveCustomFor(
const ReactionId &emoji,
ImageSize size) {
const auto custom = std::get_if<DocumentId>(&emoji.data);
if (!custom) {
return nullptr;
}
return _owner->customEmojiManager().create(*custom, [] {});
}
void Reactions::resolveImages() {
for (auto &[id, set] : _images) {
if (!set.bottomInfo.isNull() || set.icon || set.media) {
@ -343,27 +401,83 @@ void Reactions::downloadTaskFinished() {
}
}
void Reactions::request() {
auto &api = _owner->session().api();
if (_requestId) {
void Reactions::requestTop() {
if (_topRequestId) {
return;
}
_requestId = api.request(MTPmessages_GetAvailableReactions(
MTP_int(_hash)
)).done([=](const MTPmessages_AvailableReactions &result) {
_requestId = 0;
result.match([&](const MTPDmessages_availableReactions &data) {
updateFromData(data);
}, [&](const MTPDmessages_availableReactionsNotModified &) {
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([=] {
_requestId = 0;
_hash = 0;
_topRequestId = 0;
_topHash = 0;
}).send();
}
void Reactions::updateFromData(const MTPDmessages_availableReactions &data) {
_hash = data.vhash().v;
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::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);
@ -393,7 +507,99 @@ void Reactions::updateFromData(const MTPDmessages_availableReactions &data) {
_waitingForList = false;
resolveImages();
}
_updated.fire({});
defaultUpdated();
}
void Reactions::recentUpdated() {
_topRefreshTimer.callOnce(kTopRequestDelay);
_recentUpdated.fire({});
}
void Reactions::defaultUpdated() {
refreshTop();
refreshRecent();
_defaultUpdated.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));
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 (favorite) {
_favoriteUpdated.fire({});
}
if (top) {
_topUpdated.fire({});
}
if (recent) {
_recentUpdated.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 Reaction{
.id = id,
.title = "Custom reaction",
.appearAnimation = document,
.selectAnimation = document,
.centerIcon = document,
.active = true,
};
}
}
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;
}
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) {
@ -409,7 +615,7 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
? std::make_optional(Reaction{
.id = ReactionId{ emoji },
.title = qs(data.vtitle()),
.staticIcon = _owner->processDocument(data.vstatic_icon()),
//.staticIcon = _owner->processDocument(data.vstatic_icon()),
.appearAnimation = _owner->processDocument(
data.vappear_animation()),
.selectAnimation = selectAnimation,

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer.h"
#include "data/data_message_reaction_id.h"
#include "data/stickers/data_custom_emoji.h"
namespace Ui::Text {
class CustomEmoji;
@ -26,7 +27,7 @@ class Session;
struct Reaction {
ReactionId id;
QString title;
not_null<DocumentData*> staticIcon;
//not_null<DocumentData*> staticIcon;
not_null<DocumentData*> appearAnimation;
not_null<DocumentData*> selectAnimation;
//not_null<DocumentData*> activateAnimation;
@ -46,22 +47,31 @@ struct PossibleItemReactions {
[[nodiscard]] PossibleItemReactions LookupPossibleReactions(
not_null<HistoryItem*> item);
class Reactions final {
class Reactions final : private CustomEmojiManager::Listener {
public:
explicit Reactions(not_null<Session*> owner);
~Reactions();
void refresh();
void refreshTop();
void refreshRecent();
void refreshRecentDelayed();
void refreshDefault();
enum class Type {
Active,
Recent,
Top,
All,
};
[[nodiscard]] const std::vector<Reaction> &list(Type type) const;
[[nodiscard]] ReactionId favorite() const;
void setFavorite(const ReactionId &emoji);
[[nodiscard]] ReactionId favoriteId() const;
[[nodiscard]] const Reaction *favorite() const;
void setFavorite(const ReactionId &id);
[[nodiscard]] rpl::producer<> updates() const;
[[nodiscard]] rpl::producer<> topUpdates() const;
[[nodiscard]] rpl::producer<> recentUpdates() const;
[[nodiscard]] rpl::producer<> defaultUpdates() const;
[[nodiscard]] rpl::producer<> favoriteUpdates() const;
enum class ImageSize {
BottomInfo,
@ -72,9 +82,6 @@ public:
[[nodiscard]] QImage resolveImageFor(
const ReactionId &emoji,
ImageSize size);
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> resolveCustomFor(
const ReactionId &emoji,
ImageSize size);
void send(not_null<HistoryItem*> item, const ReactionId &chosen);
[[nodiscard]] bool sending(not_null<HistoryItem*> item) const;
@ -97,8 +104,26 @@ private:
bool fromAppearAnimation = false;
};
void request();
void updateFromData(const MTPDmessages_availableReactions &data);
[[nodiscard]] not_null<CustomEmojiManager::Listener*> resolveListener();
void customEmojiResolveDone(not_null<DocumentData*> document) override;
void requestTop();
void requestRecent();
void requestDefault();
void updateTop(const MTPDmessages_reactions &data);
void updateRecent(const MTPDmessages_reactions &data);
void updateDefault(const MTPDmessages_availableReactions &data);
void recentUpdated();
void defaultUpdated();
[[nodiscard]] std::optional<Reaction> resolveById(const ReactionId &id);
[[nodiscard]] std::vector<Reaction> resolveByIds(
const std::vector<ReactionId> &ids,
base::flat_set<ReactionId> &unresolved);
void resolve(const ReactionId &id);
void applyFavorite(const ReactionId &id);
[[nodiscard]] std::optional<Reaction> parse(
const MTPAvailableReaction &entry);
@ -118,14 +143,34 @@ private:
std::vector<Reaction> _active;
std::vector<Reaction> _available;
ReactionId _favorite;
std::vector<Reaction> _recent;
std::vector<ReactionId> _recentIds;
base::flat_set<ReactionId> _unresolvedRecent;
std::vector<Reaction> _top;
std::vector<ReactionId> _topIds;
base::flat_set<ReactionId> _unresolvedTop;
ReactionId _favoriteId;
ReactionId _unresolvedFavoriteId;
std::optional<Reaction> _favorite;
base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _iconsCache;
rpl::event_stream<> _updated;
rpl::event_stream<> _topUpdated;
rpl::event_stream<> _recentUpdated;
rpl::event_stream<> _defaultUpdated;
rpl::event_stream<> _favoriteUpdated;
mtpRequestId _requestId = 0;
int32 _hash = 0;
base::Timer _topRefreshTimer;
mtpRequestId _topRequestId = 0;
bool _topRequestScheduled = false;
uint64 _topHash = 0;
mtpRequestId _recentRequestId = 0;
bool _recentRequestScheduled = false;
uint64 _recentHash = 0;
mtpRequestId _defaultRequestId = 0;
int32 _defaultHash = 0;
base::flat_map<ReactionId, ImageSet> _images;
rpl::lifetime _imagesLoadLifetime;

View File

@ -1941,7 +1941,7 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
const auto item = view->data();
const auto favorite = session().data().reactions().favorite();
const auto favorite = session().data().reactions().favoriteId();
if (!ranges::contains(
Data::LookupPossibleReactions(item).recent,
favorite,
@ -1972,7 +1972,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
&& _reactionsManager->showContextMenu(
this,
e,
session().data().reactions().favorite())) {
session().data().reactions().favoriteId())) {
return;
}
auto selectedState = getSelectionState();

View File

@ -1187,7 +1187,7 @@ void ShowWhoReactedMenu(
const auto reactions = &controller->session().data().reactions();
const auto &list = reactions->list(
Data::Reactions::Type::Active);
const auto activeNonQuick = (id != reactions->favorite())
const auto activeNonQuick = (id != reactions->favoriteId())
&& ranges::contains(list, id, &Data::Reaction::id);
const auto filler = lifetime.make_state<Ui::WhoReactedListMenu>(
participantChosen,

View File

@ -2122,7 +2122,7 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
void ListWidget::toggleFavoriteReaction(not_null<Element*> view) const {
const auto item = view->data();
const auto favorite = session().data().reactions().favorite();
const auto favorite = session().data().reactions().favoriteId();
if (!ranges::contains(
Data::LookupPossibleReactions(item).recent,
favorite,
@ -2199,7 +2199,7 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
&& _reactionsManager->showContextMenu(
this,
e,
session().data().reactions().favorite())) {
session().data().reactions().favoriteId())) {
return;
}
const auto overItem = _overItemExact

View File

@ -37,6 +37,7 @@ constexpr auto kButtonExpandDelay = crl::time(25);
constexpr auto kButtonHideDelay = crl::time(300);
constexpr auto kButtonExpandedHideDelay = crl::time(0);
constexpr auto kMaxReactionsScrollAtOnce = 2;
constexpr auto kRefreshListDelay = crl::time(100);
[[nodiscard]] QPoint LocalPosition(not_null<QWheelEvent*> e) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
@ -840,6 +841,7 @@ void SetupManagerList(
Main::Session *session = nullptr;
rpl::lifetime sessionLifetime;
rpl::lifetime peerLifetime;
base::Timer timer;
};
const auto state = manager->lifetime().make_state<State>();
@ -857,10 +859,12 @@ void SetupManagerList(
const auto peerChanged = (state->peer != peer);
const auto sessionChanged = (state->session != session);
const auto push = [=] {
state->timer.cancel();
if (const auto item = state->item) {
manager->applyList(Data::LookupPossibleReactions(item));
}
};
state->timer.setCallback(push);
if (sessionChanged) {
state->sessionLifetime.destroy();
state->session = session;
@ -875,6 +879,7 @@ void SetupManagerList(
) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
if (update.item == state->item) {
state->item = nullptr;
state->timer.cancel();
}
}, state->sessionLifetime);
@ -883,8 +888,17 @@ void SetupManagerList(
return (item == state->item);
}) | rpl::start_with_next(push, state->sessionLifetime);
session->data().reactions().updates(
) | rpl::start_with_next(push, state->sessionLifetime);
const auto &reactions = session->data().reactions();
rpl::merge(
reactions.topUpdates(),
reactions.recentUpdates(),
reactions.defaultUpdates(),
reactions.favoriteUpdates()
) | rpl::start_with_next([=] {
if (!state->timer.isActive()) {
state->timer.callOnce(kRefreshListDelay);
}
}, state->sessionLifetime);
}
if (peerChanged) {
state->peer = peer;

View File

@ -449,7 +449,7 @@ rpl::producer<int> FullReactionsCountValue(
not_null<Main::Session*> session) {
const auto reactions = &session->data().reactions();
return rpl::single(rpl::empty) | rpl::then(
reactions->updates()
reactions->defaultUpdates()
) | rpl::map([=] {
return int(reactions->list(Data::Reactions::Type::Active).size());
}) | rpl::distinct_until_changed();

View File

@ -906,10 +906,10 @@ void SetupMessages(
const auto &reactions = controller->session().data().reactions();
auto idValue = rpl::single(
reactions.favorite()
reactions.favoriteId()
) | rpl::then(
reactions.updates() | rpl::map([=] {
return controller->session().data().reactions().favorite();
reactions.favoriteUpdates() | rpl::map([=] {
return controller->session().data().reactions().favoriteId();
})
) | rpl::filter([](const Data::ReactionId &id) {
return !id.empty();