Poll reactions for visible messages.

This commit is contained in:
John Preston 2021-12-28 21:20:32 +03:00
parent aafc24008b
commit ec16ca7df7
7 changed files with 164 additions and 15 deletions

View file

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "data/data_changes.h"
#include "base/timer_rpl.h" #include "base/timer_rpl.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
@ -22,18 +23,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data { namespace Data {
namespace { namespace {
constexpr auto kRefreshEach = 60 * 60 * crl::time(1000); constexpr auto kRefreshFullListEach = 60 * 60 * crl::time(1000);
constexpr auto kPollEach = 20 * crl::time(1000);
} // namespace } // namespace
Reactions::Reactions(not_null<Session*> owner) : _owner(owner) { Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _repaintTimer([=] { repaintCollected(); }) {
refresh(); refresh();
base::timer_each( base::timer_each(
kRefreshEach kRefreshFullListEach
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
refresh(); refresh();
}, _lifetime); }, _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);
} }
void Reactions::refresh() { void Reactions::refresh() {
@ -279,6 +292,85 @@ void Reactions::send(not_null<HistoryItem*> item, const QString &chosen) {
}).send(); }).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::repaintCollected() {
const auto now = crl::now();
auto closest = 0;
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 { bool Reactions::sending(not_null<HistoryItem*> item) const {
return _sentRequests.contains(item->fullId()); return _sentRequests.contains(item->fullId());
} }
@ -305,7 +397,7 @@ void MessageReactions::add(const QString &reaction) {
} }
auto &owner = _item->history()->owner(); auto &owner = _item->history()->owner();
owner.reactions().send(_item, _chosen); owner.reactions().send(_item, _chosen);
owner.requestItemResize(_item); owner.notifyItemDataChange(_item);
} }
void MessageReactions::remove() { void MessageReactions::remove() {
@ -315,22 +407,33 @@ void MessageReactions::remove() {
void MessageReactions::set( void MessageReactions::set(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
bool ignoreChosen) { bool ignoreChosen) {
_lastRefreshTime = crl::now();
if (_item->history()->owner().reactions().sending(_item)) { if (_item->history()->owner().reactions().sending(_item)) {
// We'll apply non-stale data from the request response. // We'll apply non-stale data from the request response.
return; return;
} }
auto changed = false;
auto existing = base::flat_set<QString>(); auto existing = base::flat_set<QString>();
for (const auto &count : list) { for (const auto &count : list) {
count.match([&](const MTPDreactionCount &data) { count.match([&](const MTPDreactionCount &data) {
const auto reaction = qs(data.vreaction()); const auto reaction = qs(data.vreaction());
if (data.is_chosen() && !ignoreChosen) { if (data.is_chosen() && !ignoreChosen) {
_chosen = reaction; if (_chosen != reaction) {
_chosen = reaction;
changed = true;
}
}
const auto nowCount = data.vcount().v;
auto &wasCount = _list[reaction];
if (wasCount != nowCount) {
wasCount = nowCount;
changed = true;
} }
_list[reaction] = data.vcount().v;
existing.emplace(reaction); existing.emplace(reaction);
}); });
} }
if (_list.size() != existing.size()) { if (_list.size() != existing.size()) {
changed = true;
for (auto i = begin(_list); i != end(_list);) { for (auto i = begin(_list); i != end(_list);) {
if (!existing.contains(i->first)) { if (!existing.contains(i->first)) {
i = _list.erase(i); i = _list.erase(i);
@ -342,7 +445,9 @@ void MessageReactions::set(
_chosen = QString(); _chosen = QString();
} }
} }
_item->history()->owner().requestItemResize(_item); if (changed) {
_item->history()->owner().notifyItemDataChange(_item);
}
} }
const base::flat_map<QString, int> &MessageReactions::list() const { const base::flat_map<QString, int> &MessageReactions::list() const {
@ -353,6 +458,10 @@ bool MessageReactions::empty() const {
return _list.empty(); return _list.empty();
} }
crl::time MessageReactions::lastRefreshTime() const {
return _lastRefreshTime;
}
QString MessageReactions::chosen() const { QString MessageReactions::chosen() const {
return _chosen; return _chosen;
} }

View file

@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/timer.h"
namespace Data { namespace Data {
class DocumentMedia; class DocumentMedia;
@ -59,6 +61,8 @@ public:
void send(not_null<HistoryItem*> item, const QString &chosen); void send(not_null<HistoryItem*> item, const QString &chosen);
[[nodiscard]] bool sending(not_null<HistoryItem*> item) const; [[nodiscard]] bool sending(not_null<HistoryItem*> item) const;
void poll(not_null<HistoryItem*> item, crl::time now);
private: private:
struct ImageSet { struct ImageSet {
QImage bottomInfo; QImage bottomInfo;
@ -76,6 +80,9 @@ private:
void resolveImages(); void resolveImages();
void downloadTaskFinished(); void downloadTaskFinished();
void repaintCollected();
void pollCollected();
const not_null<Session*> _owner; const not_null<Session*> _owner;
std::vector<Reaction> _active; std::vector<Reaction> _active;
@ -91,6 +98,12 @@ private:
base::flat_map<FullMsgId, mtpRequestId> _sentRequests; base::flat_map<FullMsgId, mtpRequestId> _sentRequests;
base::flat_map<not_null<HistoryItem*>, crl::time> _repaintItems;
base::Timer _repaintTimer;
base::flat_set<not_null<HistoryItem*>> _pollItems;
base::flat_set<not_null<HistoryItem*>> _pollingItems;
mtpRequestId _pollRequestId = 0;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
}; };
@ -105,12 +118,14 @@ public:
[[nodiscard]] const base::flat_map<QString, int> &list() const; [[nodiscard]] const base::flat_map<QString, int> &list() const;
[[nodiscard]] QString chosen() const; [[nodiscard]] QString chosen() const;
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
[[nodiscard]] crl::time lastRefreshTime() const;
private: private:
const not_null<HistoryItem*> _item; const not_null<HistoryItem*> _item;
QString _chosen; QString _chosen;
base::flat_map<QString, int> _list; base::flat_map<QString, int> _list;
crl::time _lastRefreshTime = 0;
}; };

View file

@ -708,6 +708,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
width(), width(),
std::min(st::msgMaxWidth / 2, width() / 2)); std::min(st::msgMaxWidth / 2, width() / 2));
const auto now = crl::now();
const auto historyDisplayedEmpty = _history->isDisplayedEmpty() const auto historyDisplayedEmpty = _history->isDisplayedEmpty()
&& (!_migrated || _migrated->isDisplayedEmpty()); && (!_migrated || _migrated->isDisplayedEmpty());
bool noHistoryDisplayed = historyDisplayedEmpty; bool noHistoryDisplayed = historyDisplayedEmpty;
@ -783,6 +784,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
readMentions.insert(item); readMentions.insert(item);
_widget->enqueueMessageHighlight(view); _widget->enqueueMessageHighlight(view);
} }
session().data().reactions().poll(item, now);
} }
top += height; top += height;
@ -826,17 +828,19 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
seltoy - htop); seltoy - htop);
view->draw(p, context); view->draw(p, context);
const auto item = view->data();
const auto middle = top + height / 2; const auto middle = top + height / 2;
const auto bottom = top + height; const auto bottom = top + height;
if (_visibleAreaBottom >= bottom) { if (_visibleAreaBottom >= bottom) {
const auto item = view->data();
if (!item->out() && item->unread()) { if (!item->out() && item->unread()) {
readTill = item; readTill = item;
} }
if (item->isSponsored()) { }
session().data().sponsoredMessages().view( if (item->isSponsored()
item->fullId()); && view->markSponsoredViewed(
} _visibleAreaBottom - top)) {
session().data().sponsoredMessages().view(
item->fullId());
} }
if (_visibleAreaBottom >= middle if (_visibleAreaBottom >= middle
&& _visibleAreaTop <= middle) { && _visibleAreaTop <= middle) {
@ -848,6 +852,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
_widget->enqueueMessageHighlight(view); _widget->enqueueMessageHighlight(view);
} }
} }
session().data().reactions().poll(item, now);
} }
top += height; top += height;
context.translate(0, -height); context.translate(0, -height);

View file

@ -765,6 +765,7 @@ void HistoryItem::toggleReaction(const QString &reaction) {
_reactions->remove(); _reactions->remove();
if (_reactions->empty()) { if (_reactions->empty()) {
_reactions = nullptr; _reactions = nullptr;
history()->owner().notifyItemDataChange(this);
} }
} else { } else {
_reactions->add(reaction); _reactions->add(reaction);
@ -775,7 +776,10 @@ void HistoryItem::toggleReaction(const QString &reaction) {
void HistoryItem::updateReactions(const MTPMessageReactions *reactions) { void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
if (!reactions) { if (!reactions) {
_flags &= ~MessageFlag::CanViewReactions; _flags &= ~MessageFlag::CanViewReactions;
_reactions = nullptr; if (_reactions) {
_reactions = nullptr;
history()->owner().notifyItemDataChange(this);
}
return; return;
} }
reactions->match([&](const MTPDmessageReactions &data) { reactions->match([&](const MTPDmessageReactions &data) {
@ -785,13 +789,15 @@ void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
_flags &= ~MessageFlag::CanViewReactions; _flags &= ~MessageFlag::CanViewReactions;
} }
if (data.vresults().v.isEmpty()) { if (data.vresults().v.isEmpty()) {
_reactions = nullptr; if (_reactions) {
_reactions = nullptr;
history()->owner().notifyItemDataChange(this);
}
return; return;
} else if (!_reactions) { } else if (!_reactions) {
_reactions = std::make_unique<Data::MessageReactions>(this); _reactions = std::make_unique<Data::MessageReactions>(this);
} }
_reactions->set(data.vresults().v, data.is_min()); _reactions->set(data.vresults().v, data.is_min());
history()->owner().notifyItemDataChange(this);
}); });
} }
@ -810,6 +816,10 @@ QString HistoryItem::chosenReaction() const {
return _reactions ? _reactions->chosen() : QString(); return _reactions ? _reactions->chosen() : QString();
} }
crl::time HistoryItem::lastReactionsRefreshTime() const {
return _reactions ? _reactions->lastRefreshTime() : 0;
}
bool HistoryItem::hasDirectLink() const { bool HistoryItem::hasDirectLink() const {
return isRegular() && _history->peer->isChannel(); return isRegular() && _history->peer->isChannel();
} }

View file

@ -367,6 +367,7 @@ public:
[[nodiscard]] const base::flat_map<QString, int> &reactions() const; [[nodiscard]] const base::flat_map<QString, int> &reactions() const;
[[nodiscard]] bool canViewReactions() const; [[nodiscard]] bool canViewReactions() const;
[[nodiscard]] QString chosenReaction() const; [[nodiscard]] QString chosenReaction() const;
[[nodiscard]] crl::time lastReactionsRefreshTime() const;
[[nodiscard]] bool hasDirectLink() const; [[nodiscard]] bool hasDirectLink() const;

View file

@ -547,6 +547,13 @@ void Element::nextInBlocksRemoved() {
setAttachToNext(false); setAttachToNext(false);
} }
bool Element::markSponsoredViewed(int shownFromTop) const {
const auto sponsoredTextTop = height()
- st::msgPadding.bottom()
- st::historyViewButtonHeight;
return shownFromTop >= sponsoredTextTop;
}
void Element::refreshDataId() { void Element::refreshDataId() {
if (const auto media = this->media()) { if (const auto media = this->media()) {
media->refreshParentId(data()); media->refreshParentId(data());

View file

@ -408,6 +408,8 @@ public:
return fromLink(); return fromLink();
} }
[[nodiscard]] bool markSponsoredViewed(int shownFromTop) const;
virtual ~Element(); virtual ~Element();
protected: protected: