Support multiple reactions from one user.

This commit is contained in:
John Preston 2022-08-26 00:56:38 +04:00
parent 31db1804c8
commit 8a6b3027f5
29 changed files with 354 additions and 186 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

View File

@ -479,21 +479,26 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
: Ui::WhoReadType::Reacted; : Ui::WhoReadType::Reacted;
if (resolveWhoReacted) { if (resolveWhoReacted) {
const auto &list = item->reactions(); const auto &list = item->reactions();
state->current.fullReactionsCount = reaction.empty() state->current.fullReactionsCount = [&] {
? ranges::accumulate( if (reaction.empty()) {
return ranges::accumulate(
list,
0,
ranges::plus{},
&Data::MessageReaction::count);
}
const auto i = ranges::find(
list, list,
0, reaction,
ranges::plus{}, &Data::MessageReaction::id);
[](const auto &pair) { return pair.second; }) return (i != end(list)) ? i->count : 0;
: list.contains(reaction) }();
? list.find(reaction)->second
: 0;
// #TODO reactions // #TODO reactions
state->current.singleReaction = (!reaction.empty() state->current.singleReaction = (!reaction.empty()
? reaction ? reaction
: (list.size() == 1) : (list.size() == 1)
? list.front().first ? list.front().id
: ReactionId()).emoji(); : ReactionId()).emoji();
} }
std::move( std::move(

View File

@ -26,6 +26,12 @@ struct ReactionId {
} }
}; };
struct MessageReaction {
ReactionId id;
int count = 0;
bool my = false;
};
inline bool operator<(const ReactionId &a, const ReactionId &b) { inline bool operator<(const ReactionId &a, const ReactionId &b) {
return a.data < b.data; return a.data < b.data;
} }

View File

@ -63,6 +63,18 @@ constexpr auto kTopReactionsLimit = 10;
return result; 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,
};
}
} // namespace } // namespace
PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) { PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
@ -74,22 +86,43 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
const auto session = &peer->session(); const auto session = &peer->session();
const auto reactions = &session->data().reactions(); const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Reactions::Type::Active); 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 &all = item->reactions(); const auto &all = item->reactions();
const auto my = item->chosenReaction();
auto myIsUnique = false;
for (const auto &[id, count] : all) {
if (count == 1 && id == my) {
myIsUnique = true;
}
}
const auto notMineCount = int(all.size()) - (myIsUnique ? 1 : 0);
const auto limit = UniqueReactionsLimit(peer); const auto limit = UniqueReactionsLimit(peer);
if (limit > 0 && notMineCount >= limit) { 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 addOne = [&](const Reaction &reaction) {
if (added.emplace(reaction.id).second) {
result.recent.push_back(&reaction);
}
};
const auto add = [&](auto predicate) {
auto &&all = ranges::views::concat(top, recent, full);
for (const auto &reaction : all) {
if (predicate(reaction)) {
addOne(reaction);
}
}
};
reactions->clearTemporary();
if (limited) {
result.recent.reserve(all.size()); result.recent.reserve(all.size());
for (const auto &reaction : full) { add([&](const Reaction &reaction) {
return ranges::contains(all, reaction.id, &MessageReaction::id);
});
for (const auto &reaction : all) {
const auto id = reaction.id; const auto id = reaction.id;
if (all.contains(id)) { if (!added.contains(id)) {
result.recent.push_back(&reaction); if (const auto temp = reactions->lookupTemporary(id)) {
result.recent.push_back(temp);
}
} }
} }
} else { } else {
@ -97,22 +130,21 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
result.recent.reserve((allowed.type == AllowedReactionsType::Some) result.recent.reserve((allowed.type == AllowedReactionsType::Some)
? allowed.some.size() ? allowed.some.size()
: full.size()); : full.size());
for (const auto &reaction : full) { add([&](const Reaction &reaction) {
const auto id = reaction.id; const auto id = reaction.id;
if ((allowed.type == AllowedReactionsType::Some) if ((allowed.type == AllowedReactionsType::Some)
&& !ranges::contains(allowed.some, id)) { && !ranges::contains(allowed.some, id)) {
continue; return false;
} else if (reaction.premium } else if (reaction.premium
&& !session->premium() && !session->premium()
&& !all.contains(id)) { && !ranges::contains(all, id, &MessageReaction::id)) {
if (session->premiumPossible()) { if (session->premiumPossible()) {
result.morePremiumAvailable = true; result.morePremiumAvailable = true;
} }
continue; return false;
} else {
result.recent.push_back(&reaction);
} }
} return true;
});
result.customAllowed = (allowed.type == AllowedReactionsType::All); result.customAllowed = (allowed.type == AllowedReactionsType::All);
} }
const auto i = ranges::find( const auto i = ranges::find(
@ -564,14 +596,7 @@ std::optional<Reaction> Reactions::resolveById(const ReactionId &id) {
} else if (const auto customId = id.custom()) { } else if (const auto customId = id.custom()) {
const auto document = _owner->document(customId); const auto document = _owner->document(customId);
if (document->sticker()) { if (document->sticker()) {
return Reaction{ return CustomReaction(document);
.id = id,
.title = "Custom reaction",
.appearAnimation = document,
.selectAnimation = document,
.centerIcon = document,
.active = true,
};
} }
} }
return {}; return {};
@ -637,7 +662,7 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
}); });
} }
void Reactions::send(not_null<HistoryItem*> item, const ReactionId &chosen) { void Reactions::send(not_null<HistoryItem*> item, bool addToRecent) {
const auto id = item->fullId(); const auto id = item->fullId();
auto &api = _owner->session().api(); auto &api = _owner->session().api();
auto i = _sentRequests.find(id); auto i = _sentRequests.find(id);
@ -646,14 +671,17 @@ void Reactions::send(not_null<HistoryItem*> item, const ReactionId &chosen) {
} else { } else {
i = _sentRequests.emplace(id).first; i = _sentRequests.emplace(id).first;
} }
const auto flags = chosen.empty() const auto chosen = item->chosenReactions();
? MTPmessages_SendReaction::Flag(0) using Flag = MTPmessages_SendReaction::Flag;
: MTPmessages_SendReaction::Flag::f_reaction; const auto flags = (chosen.empty() ? Flag(0) : Flag::f_reaction)
| (addToRecent ? Flag::f_add_to_recent : Flag(0));
i->second = api.request(MTPmessages_SendReaction( i->second = api.request(MTPmessages_SendReaction(
MTP_flags(flags), MTP_flags(flags),
item->history()->peer->input, item->history()->peer->input,
MTP_int(id.msg), MTP_int(id.msg),
MTP_vector<MTPReaction>(1, ReactionToMTP(chosen)) MTP_vector<MTPReaction>(chosen | ranges::views::transform(
ReactionToMTP
) | ranges::to<QVector<MTPReaction>>())
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
_sentRequests.remove(id); _sentRequests.remove(id);
_owner->session().api().applyUpdates(result); _owner->session().api().applyUpdates(result);
@ -693,6 +721,32 @@ void Reactions::updateAllInHistory(not_null<PeerData*> peer, bool 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;
}
void Reactions::repaintCollected() { void Reactions::repaintCollected() {
const auto now = crl::now(); const auto now = crl::now();
auto closest = crl::time(); auto closest = crl::time();
@ -784,45 +838,84 @@ MessageReactions::MessageReactions(not_null<HistoryItem*> item)
: _item(item) { : _item(item) {
} }
void MessageReactions::add(const ReactionId &reaction) { void MessageReactions::add(const ReactionId &id, bool addToRecent) {
if (_chosen == reaction) { Expects(!id.empty());
return;
}
const auto history = _item->history(); const auto history = _item->history();
const auto self = history->session().user(); const auto self = history->session().user();
if (!_chosen.empty()) { const auto myLimit = self->isPremium() ? 5 : 1; // #TODO reactions
const auto i = _list.find(_chosen); if (ranges::contains(chosen(), id)) {
Assert(i != end(_list)); return;
--i->second; }
const auto removed = !i->second; auto my = 0;
if (removed) { _list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
_list.erase(i); const auto removing = one.my && (my == myLimit || ++my == myLimit);
if (!removing) {
return false;
} }
const auto j = _recent.find(_chosen); one.my = false;
const auto removed = !--one.count;
const auto j = _recent.find(one.id);
if (j != end(_recent)) { if (j != end(_recent)) {
j->second.erase( j->second.erase(
ranges::remove(j->second, self, &RecentReaction::peer), ranges::remove(j->second, self, &RecentReaction::peer),
end(j->second)); end(j->second));
if (j->second.empty() || removed) { if (j->second.empty()) {
_recent.erase(j); _recent.erase(j);
} else {
Assert(!removed);
} }
} }
return removed;
}), end(_list));
if (_item->canViewReactions()) {
auto &list = _recent[id];
list.insert(begin(list), RecentReaction{ self });
} }
_chosen = reaction; const auto i = ranges::find(_list, id, &MessageReaction::id);
if (!reaction.empty()) { if (i != end(_list)) {
if (_item->canViewReactions()) { i->my = true;
auto &list = _recent[reaction]; ++i->count;
list.insert(begin(list), RecentReaction{ self }); std::rotate(i, i + 1, end(_list));
} } else {
++_list[reaction]; _list.push_back({ .id = id, .count = 1, .my = true });
} }
auto &owner = history->owner(); auto &owner = history->owner();
owner.reactions().send(_item, _chosen); owner.reactions().send(_item, addToRecent);
owner.notifyItemDataChange(_item); owner.notifyItemDataChange(_item);
} }
void MessageReactions::remove() { void MessageReactions::remove(const ReactionId &id) {
add(ReactionId()); 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 removed = !--i->count;
if (removed) {
_list.erase(i);
}
if (j != end(_recent)) {
j->second.erase(
ranges::remove(j->second, self, &RecentReaction::peer),
end(j->second));
if (j->second.empty()) {
_recent.erase(j);
} else {
Assert(!removed);
}
}
auto &owner = history->owner();
owner.reactions().send(_item, false);
owner.notifyItemDataChange(_item);
} }
bool MessageReactions::checkIfChanged( bool MessageReactions::checkIfChanged(
@ -836,31 +929,31 @@ bool MessageReactions::checkIfChanged(
auto existing = base::flat_set<ReactionId>(); auto existing = base::flat_set<ReactionId>();
for (const auto &count : list) { for (const auto &count : list) {
const auto changed = count.match([&](const MTPDreactionCount &data) { const auto changed = count.match([&](const MTPDreactionCount &data) {
const auto reaction = ReactionFromMTP(data.vreaction()); const auto id = ReactionFromMTP(data.vreaction());
const auto nowCount = data.vcount().v; const auto nowCount = data.vcount().v;
const auto i = _list.find(reaction); const auto i = ranges::find(_list, id, &MessageReaction::id);
const auto wasCount = (i != end(_list)) ? i->second : 0; const auto wasCount = (i != end(_list)) ? i->count : 0;
if (wasCount != nowCount) { if (wasCount != nowCount) {
return true; return true;
} }
existing.emplace(reaction); existing.emplace(id);
return false; return false;
}); });
if (changed) { if (changed) {
return true; return true;
} }
} }
for (const auto &[reaction, count] : _list) { for (const auto &reaction : _list) {
if (!existing.contains(reaction)) { if (!existing.contains(reaction.id)) {
return true; return true;
} }
} }
auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>(); auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();
for (const auto &reaction : recent) { for (const auto &reaction : recent) {
reaction.match([&](const MTPDmessagePeerReaction &data) { reaction.match([&](const MTPDmessagePeerReaction &data) {
const auto emoji = ReactionFromMTP(data.vreaction()); const auto id = ReactionFromMTP(data.vreaction());
if (_list.contains(emoji)) { if (ranges::contains(_list, id, &MessageReaction::id)) {
parsed[emoji].push_back(RecentReaction{ parsed[id].push_back(RecentReaction{
.peer = owner.peer(peerFromMTP(data.vpeer_id())), .peer = owner.peer(peerFromMTP(data.vpeer_id())),
.unread = data.is_unread(), .unread = data.is_unread(),
.big = data.is_big(), .big = data.is_big(),
@ -890,50 +983,79 @@ bool MessageReactions::change(
} }
auto changed = false; auto changed = false;
auto existing = base::flat_set<ReactionId>(); auto existing = base::flat_set<ReactionId>();
auto order = base::flat_map<ReactionId, int>();
for (const auto &count : list) { for (const auto &count : list) {
count.match([&](const MTPDreactionCount &data) { count.match([&](const MTPDreactionCount &data) {
const auto reaction = ReactionFromMTP(data.vreaction()); const auto id = ReactionFromMTP(data.vreaction());
if (!ignoreChosen) { const auto &chosen = data.vchosen_order();
if (data.vchosen_order() && _chosen != reaction) { if (!ignoreChosen && chosen) {
_chosen = reaction; order[id] = chosen->v;
changed = true; }
} else if (!data.vchosen_order() && _chosen == reaction) { const auto i = ranges::find(_list, id, &MessageReaction::id);
_chosen = ReactionId(); const auto nowCount = data.vcount().v;
if (i == end(_list)) {
changed = true;
_list.push_back({
.id = id,
.count = nowCount,
.my = (!ignoreChosen && chosen)
});
} else {
const auto nowMy = ignoreChosen ? i->my : chosen.has_value();
if (i->count != nowCount || i->my != nowMy) {
i->count = nowCount;
i->my = nowMy;
changed = true; changed = true;
} }
} }
const auto nowCount = data.vcount().v; existing.emplace(id);
auto &wasCount = _list[reaction];
if (wasCount != nowCount) {
wasCount = nowCount;
changed = true;
}
existing.emplace(reaction);
}); });
} }
if (!ignoreChosen && !order.empty()) {
const auto min = std::numeric_limits<int>::min();
const auto proj = [&](const MessageReaction &reaction) {
return reaction.my ? order[reaction.id] : min;
};
const auto correctOrder = [&] {
auto previousOrder = min;
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()) { if (_list.size() != existing.size()) {
changed = true; 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->id)) {
i = _list.erase(i); i = _list.erase(i);
} else { } else {
++i; ++i;
} }
} }
if (!_chosen.empty() && !_list.contains(_chosen)) {
_chosen = ReactionId();
}
} }
auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>(); auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();
for (const auto &reaction : recent) { for (const auto &reaction : recent) {
reaction.match([&](const MTPDmessagePeerReaction &data) { reaction.match([&](const MTPDmessagePeerReaction &data) {
const auto emoji = ReactionFromMTP(data.vreaction()); const auto id = ReactionFromMTP(data.vreaction());
if (_list.contains(emoji)) { const auto i = ranges::find(_list, id, &MessageReaction::id);
parsed[emoji].push_back(RecentReaction{ if (i != end(_list)) {
.peer = owner.peer(peerFromMTP(data.vpeer_id())), auto &list = parsed[id];
.unread = data.is_unread(), if (list.size() < i->count) {
.big = data.is_big(), list.push_back(RecentReaction{
}); .peer = owner.peer(peerFromMTP(data.vpeer_id())),
.unread = data.is_unread(),
.big = data.is_big(),
});
}
} }
}); });
} }
@ -944,7 +1066,7 @@ bool MessageReactions::change(
return changed; return changed;
} }
const base::flat_map<ReactionId, int> &MessageReactions::list() const { const std::vector<MessageReaction> &MessageReactions::list() const {
return _list; return _list;
} }
@ -974,8 +1096,11 @@ void MessageReactions::markRead() {
} }
} }
ReactionId MessageReactions::chosen() const { std::vector<ReactionId> MessageReactions::chosen() const {
return _chosen; return _list
| ranges::views::filter(&MessageReaction::my)
| ranges::views::transform(&MessageReaction::id)
| ranges::to_vector;
} }
} // namespace Data } // namespace Data

View File

@ -83,13 +83,16 @@ public:
const ReactionId &emoji, const ReactionId &emoji,
ImageSize size); ImageSize size);
void send(not_null<HistoryItem*> item, const ReactionId &chosen); void send(not_null<HistoryItem*> item, bool addToRecent);
[[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); void poll(not_null<HistoryItem*> item, crl::time now);
void updateAllInHistory(not_null<PeerData*> peer, bool enabled); void updateAllInHistory(not_null<PeerData*> peer, bool enabled);
void clearTemporary();
[[nodiscard]] Reaction *lookupTemporary(const ReactionId &id);
[[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data);
static void CheckUnknownForUnread( static void CheckUnknownForUnread(
not_null<Session*> owner, not_null<Session*> owner,
@ -160,6 +163,11 @@ private:
rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _defaultUpdated;
rpl::event_stream<> _favoriteUpdated; rpl::event_stream<> _favoriteUpdated;
// We need &i->second stay valid while inserting new items.
// 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;
base::Timer _topRefreshTimer; base::Timer _topRefreshTimer;
mtpRequestId _topRequestId = 0; mtpRequestId _topRequestId = 0;
bool _topRequestScheduled = false; bool _topRequestScheduled = false;
@ -208,8 +216,8 @@ class MessageReactions final {
public: public:
explicit MessageReactions(not_null<HistoryItem*> item); explicit MessageReactions(not_null<HistoryItem*> item);
void add(const ReactionId &reaction); void add(const ReactionId &id, bool addToRecent);
void remove(); void remove(const ReactionId &id);
bool change( bool change(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent, const QVector<MTPMessagePeerReaction> &recent,
@ -217,10 +225,10 @@ public:
[[nodiscard]] bool checkIfChanged( [[nodiscard]] bool checkIfChanged(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent) const; const QVector<MTPMessagePeerReaction> &recent) const;
[[nodiscard]] const base::flat_map<ReactionId, int> &list() const; [[nodiscard]] const std::vector<MessageReaction> &list() const;
[[nodiscard]] auto recent() const [[nodiscard]] auto recent() const
-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &; -> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;
[[nodiscard]] ReactionId chosen() const; [[nodiscard]] std::vector<ReactionId> chosen() const;
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
[[nodiscard]] bool hasUnread() const; [[nodiscard]] bool hasUnread() const;
@ -229,8 +237,7 @@ public:
private: private:
const not_null<HistoryItem*> _item; const not_null<HistoryItem*> _item;
ReactionId _chosen; std::vector<MessageReaction> _list;
base::flat_map<ReactionId, int> _list;
base::flat_map<ReactionId, std::vector<RecentReaction>> _recent; base::flat_map<ReactionId, std::vector<RecentReaction>> _recent;
}; };

View File

@ -471,8 +471,8 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) {
reaction.id)) { reaction.id)) {
return; return;
} }
item->toggleReaction(reaction.id); item->toggleReaction(reaction.id, HistoryItem::ReactionSource::Selector);
if (item->chosenReaction() != reaction.id) { if (!ranges::contains(item->chosenReactions(), reaction.id)) {
return; return;
} else if (const auto view = item->mainView()) { } else if (const auto view = item->mainView()) {
if (const auto top = itemTop(view); top >= 0) { if (const auto top = itemTop(view); top >= 0) {
@ -1948,12 +1948,12 @@ void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
&Data::Reaction::id) &Data::Reaction::id)
|| Window::ShowReactPremiumError(_controller, item, favorite)) { || Window::ShowReactPremiumError(_controller, item, favorite)) {
return; return;
} else if (item->chosenReaction() != favorite) { } else if (!ranges::contains(item->chosenReactions(), favorite)) {
if (const auto top = itemTop(view); top >= 0) { if (const auto top = itemTop(view); top >= 0) {
view->animateReaction({ .id = favorite }); view->animateReaction({ .id = favorite });
} }
} }
item->toggleReaction(favorite); item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
} }
void HistoryInner::contextMenuEvent(QContextMenuEvent *e) { void HistoryInner::contextMenuEvent(QContextMenuEvent *e) {

View File

@ -883,15 +883,9 @@ bool HistoryItem::canReact() const {
return true; return true;
} }
void HistoryItem::addReaction(const Data::ReactionId &reaction) { void HistoryItem::toggleReaction(
if (!_reactions) { const Data::ReactionId &reaction,
_reactions = std::make_unique<Data::MessageReactions>(this); ReactionSource source) {
}
_reactions->add(reaction);
history()->owner().notifyItemDataChange(this);
}
void HistoryItem::toggleReaction(const Data::ReactionId &reaction) {
if (!_reactions) { if (!_reactions) {
_reactions = std::make_unique<Data::MessageReactions>(this); _reactions = std::make_unique<Data::MessageReactions>(this);
const auto canViewReactions = !isDiscussionPost() const auto canViewReactions = !isDiscussionPost()
@ -899,16 +893,16 @@ void HistoryItem::toggleReaction(const Data::ReactionId &reaction) {
if (canViewReactions) { if (canViewReactions) {
_flags |= MessageFlag::CanViewReactions; _flags |= MessageFlag::CanViewReactions;
} }
_reactions->add(reaction); _reactions->add(reaction, (source == ReactionSource::Selector));
} else if (_reactions->chosen() == reaction) { } else if (ranges::contains(_reactions->chosen(), reaction)) {
_reactions->remove(); _reactions->remove(reaction);
if (_reactions->empty()) { if (_reactions->empty()) {
_reactions = nullptr; _reactions = nullptr;
_flags &= ~MessageFlag::CanViewReactions; _flags &= ~MessageFlag::CanViewReactions;
history()->owner().notifyItemDataChange(this); history()->owner().notifyItemDataChange(this);
} }
} else { } else {
_reactions->add(reaction); _reactions->add(reaction, (source == ReactionSource::Selector));
} }
history()->owner().notifyItemDataChange(this); history()->owner().notifyItemDataChange(this);
} }
@ -977,8 +971,8 @@ void HistoryItem::updateReactionsUnknown() {
_reactionsLastRefreshed = 1; _reactionsLastRefreshed = 1;
} }
const base::flat_map<Data::ReactionId, int> &HistoryItem::reactions() const { const std::vector<Data::MessageReaction> &HistoryItem::reactions() const {
static const auto kEmpty = base::flat_map<Data::ReactionId, int>(); static const auto kEmpty = std::vector<Data::MessageReaction>();
return _reactions ? _reactions->list() : kEmpty; return _reactions ? _reactions->list() : kEmpty;
} }
@ -998,8 +992,10 @@ bool HistoryItem::canViewReactions() const {
&& !_reactions->list().empty(); && !_reactions->list().empty();
} }
Data::ReactionId HistoryItem::chosenReaction() const { std::vector<Data::ReactionId> HistoryItem::chosenReactions() const {
return _reactions ? _reactions->chosen() : Data::ReactionId(); return _reactions
? _reactions->chosen()
: std::vector<Data::ReactionId>();
} }
Data::ReactionId HistoryItem::lookupUnreadReaction( Data::ReactionId HistoryItem::lookupUnreadReaction(

View File

@ -44,6 +44,7 @@ struct MessagePosition;
struct RecentReaction; struct RecentReaction;
struct ReactionId; struct ReactionId;
class Media; class Media;
struct MessageReaction;
class MessageReactions; class MessageReactions;
} // namespace Data } // namespace Data
@ -373,18 +374,24 @@ public:
[[nodiscard]] bool suggestDeleteAllReport() const; [[nodiscard]] bool suggestDeleteAllReport() const;
[[nodiscard]] bool canReact() const; [[nodiscard]] bool canReact() const;
void addReaction(const Data::ReactionId &reaction); enum class ReactionSource {
void toggleReaction(const Data::ReactionId &reaction); Selector,
Quick,
Existing,
};
void toggleReaction(
const Data::ReactionId &reaction,
ReactionSource source);
void updateReactions(const MTPMessageReactions *reactions); void updateReactions(const MTPMessageReactions *reactions);
void updateReactionsUnknown(); void updateReactionsUnknown();
[[nodiscard]] auto reactions() const [[nodiscard]] auto reactions() const
-> const base::flat_map<Data::ReactionId, int> &; -> const std::vector<Data::MessageReaction> &;
[[nodiscard]] auto recentReactions() const [[nodiscard]] auto recentReactions() const
-> const base::flat_map< -> const base::flat_map<
Data::ReactionId, Data::ReactionId,
std::vector<Data::RecentReaction>> &; std::vector<Data::RecentReaction>> &;
[[nodiscard]] bool canViewReactions() const; [[nodiscard]] bool canViewReactions() const;
[[nodiscard]] Data::ReactionId chosenReaction() const; [[nodiscard]] std::vector<Data::ReactionId> chosenReactions() const;
[[nodiscard]] Data::ReactionId lookupUnreadReaction( [[nodiscard]] Data::ReactionId lookupUnreadReaction(
not_null<UserData*> from) const; not_null<UserData*> from) const;
[[nodiscard]] crl::time lastReactionsRefreshTime() const; [[nodiscard]] crl::time lastReactionsRefreshTime() const;

View File

@ -160,7 +160,7 @@ ClickHandlerPtr BottomInfo::revokeReactionLink(
auto y = top; auto y = top;
auto widthLeft = available; auto widthLeft = available;
for (const auto &reaction : _reactions) { for (const auto &reaction : _reactions) {
const auto chosen = (reaction.id == _data.chosenReaction); const auto chosen = reaction.chosen;
const auto add = (reaction.countTextWidth > 0) const auto add = (reaction.countTextWidth > 0)
? st::reactionInfoDigitSkip ? st::reactionInfoDigitSkip
: st::reactionInfoBetween; : st::reactionInfoBetween;
@ -201,9 +201,11 @@ ClickHandlerPtr BottomInfo::revokeReactionLink(
if (controller->session().uniqueId() == sessionId) { if (controller->session().uniqueId() == sessionId) {
auto &owner = controller->session().data(); auto &owner = controller->session().data();
if (const auto item = owner.message(itemId)) { if (const auto item = owner.message(itemId)) {
const auto chosen = item->chosenReaction(); const auto chosen = item->chosenReactions();
if (!chosen.empty()) { if (!chosen.empty()) {
item->toggleReaction(chosen); item->toggleReaction(
chosen.front(),
HistoryItem::ReactionSource::Existing);
} }
} }
} }
@ -483,22 +485,23 @@ void BottomInfo::layoutReactionsText() {
} }
auto sorted = ranges::view::all( auto sorted = ranges::view::all(
_data.reactions _data.reactions
) | ranges::view::transform([](const auto &pair) { ) | ranges::view::transform([](const MessageReaction &reaction) {
return std::make_pair(pair.first, pair.second); return not_null{ &reaction };
}) | ranges::to_vector; }) | ranges::to_vector;
ranges::sort( ranges::sort(
sorted, sorted,
std::greater<>(), std::greater<>(),
&std::pair<ReactionId, int>::second); &MessageReaction::count);
auto reactions = std::vector<Reaction>(); auto reactions = std::vector<Reaction>();
reactions.reserve(sorted.size()); reactions.reserve(sorted.size());
for (const auto &[id, count] : sorted) { for (const auto &reaction : sorted) {
const auto &id = reaction->id;
const auto i = ranges::find(_reactions, id, &Reaction::id); const auto i = ranges::find(_reactions, id, &Reaction::id);
reactions.push_back((i != end(_reactions)) reactions.push_back((i != end(_reactions))
? std::move(*i) ? std::move(*i)
: prepareReactionWithId(id)); : prepareReactionWithId(id));
setReactionCount(reactions.back(), count); setReactionCount(reactions.back(), reaction->count);
} }
_reactions = std::move(reactions); _reactions = std::move(reactions);
} }
@ -593,7 +596,6 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
result.date = message->dateTime(); result.date = message->dateTime();
if (message->embedReactionsInBottomInfo()) { if (message->embedReactionsInBottomInfo()) {
result.reactions = item->reactions(); result.reactions = item->reactions();
result.chosenReaction = item->chosenReaction();
} }
if (message->hasOutLayout()) { if (message->hasOutLayout()) {
result.flags |= Flag::OutLayout; result.flags |= Flag::OutLayout;

View File

@ -45,6 +45,7 @@ struct TextState;
class BottomInfo final : public Object { class BottomInfo final : public Object {
public: public:
using ReactionId = ::Data::ReactionId; using ReactionId = ::Data::ReactionId;
using MessageReaction = ::Data::MessageReaction;
struct Data { struct Data {
enum class Flag : uchar { enum class Flag : uchar {
Edited = 0x01, Edited = 0x01,
@ -62,8 +63,7 @@ public:
QDateTime date; QDateTime date;
QString author; QString author;
base::flat_map<ReactionId, int> reactions; std::vector<MessageReaction> reactions;
ReactionId chosenReaction;
std::optional<int> views; std::optional<int> views;
std::optional<int> replies; std::optional<int> replies;
Flags flags; Flags flags;
@ -105,6 +105,7 @@ private:
QString countText; QString countText;
int count = 0; int count = 0;
int countTextWidth = 0; int countTextWidth = 0;
bool chosen = false;
}; };
void layout(); void layout();

View File

@ -364,8 +364,10 @@ ListWidget::ListWidget(
reaction.id)) { reaction.id)) {
return; return;
} }
item->toggleReaction(reaction.id); item->toggleReaction(
if (item->chosenReaction() != reaction.id) { reaction.id,
HistoryItem::ReactionSource::Selector);
if (!ranges::contains(item->chosenReactions(), reaction.id)) {
return; return;
} else if (const auto view = viewForItem(item)) { } else if (const auto view = viewForItem(item)) {
if (const auto top = itemTop(view); top >= 0) { if (const auto top = itemTop(view); top >= 0) {
@ -2129,12 +2131,12 @@ void ListWidget::toggleFavoriteReaction(not_null<Element*> view) const {
&Data::Reaction::id) &Data::Reaction::id)
|| Window::ShowReactPremiumError(_controller, item, favorite)) { || Window::ShowReactPremiumError(_controller, item, favorite)) {
return; return;
} else if (item->chosenReaction() != favorite) { } else if (!ranges::contains(item->chosenReactions(), favorite)) {
if (const auto top = itemTop(view); top >= 0) { if (const auto top = itemTop(view); top >= 0) {
view->animateReaction({ .id = favorite }); view->animateReaction({ .id = favorite });
} }
} }
item->toggleReaction(favorite); item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
} }
void ListWidget::trySwitchToWordSelection() { void ListWidget::trySwitchToWordSelection() {

View File

@ -2188,9 +2188,12 @@ void Message::refreshReactions() {
const auto weak = base::make_weak(this); const auto weak = base::make_weak(this);
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>([=] {
if (const auto strong = weak.get()) { if (const auto strong = weak.get()) {
strong->data()->toggleReaction(id); strong->data()->toggleReaction(
id,
HistoryItem::ReactionSource::Existing);
if (const auto now = weak.get()) { if (const auto now = weak.get()) {
if (now->data()->chosenReaction() == id) { const auto chosen = now->data()->chosenReactions();
if (ranges::contains(chosen, id)) {
now->animateReaction({ now->animateReaction({
.id = id, .id = id,
}); });

View File

@ -45,6 +45,7 @@ struct InlineList::Button {
QString countText; QString countText;
int count = 0; int count = 0;
int countTextWidth = 0; int countTextWidth = 0;
bool chosen = false;
}; };
InlineList::InlineList( InlineList::InlineList(
@ -87,34 +88,39 @@ void InlineList::layoutButtons() {
} }
auto sorted = ranges::view::all( auto sorted = ranges::view::all(
_data.reactions _data.reactions
) | ranges::view::transform([](const auto &pair) { ) | ranges::view::transform([](const MessageReaction &reaction) {
return std::make_pair(pair.first, pair.second); return not_null{ &reaction };
}) | ranges::to_vector; }) | ranges::to_vector;
const auto &list = _owner->list(::Data::Reactions::Type::All); const auto &list = _owner->list(::Data::Reactions::Type::All);
ranges::sort(sorted, [&](const auto &p1, const auto &p2) { ranges::sort(sorted, [&](
if (p1.second > p2.second) { not_null<const MessageReaction*> a,
not_null<const MessageReaction*> b) {
const auto acount = a->count - (a->my ? 1 : 0);
const auto bcount = b->count - (b->my ? 1 : 0);
if (acount > bcount) {
return true; return true;
} else if (p1.second < p2.second) { } else if (acount < bcount) {
return false; return false;
} }
return ranges::find(list, p1.first, &::Data::Reaction::id) return ranges::find(list, a->id, &::Data::Reaction::id)
< ranges::find(list, p2.first, &::Data::Reaction::id); < ranges::find(list, b->id, &::Data::Reaction::id);
}); });
auto buttons = std::vector<Button>(); auto buttons = std::vector<Button>();
buttons.reserve(sorted.size()); buttons.reserve(sorted.size());
for (const auto &[id, count] : sorted) { for (const auto &reaction : sorted) {
const auto &id = reaction->id;
const auto i = ranges::find(_buttons, id, &Button::id); const auto i = ranges::find(_buttons, id, &Button::id);
buttons.push_back((i != end(_buttons)) buttons.push_back((i != end(_buttons))
? std::move(*i) ? std::move(*i)
: prepareButtonWithId(id)); : prepareButtonWithId(id));
const auto add = (id == _data.chosenReaction) ? 1 : 0;
const auto j = _data.recent.find(id); const auto j = _data.recent.find(id);
if (j != end(_data.recent) && !j->second.empty()) { if (j != end(_data.recent) && !j->second.empty()) {
setButtonUserpics(buttons.back(), j->second); setButtonUserpics(buttons.back(), j->second);
} else { } else {
setButtonCount(buttons.back(), count + add); setButtonCount(buttons.back(), reaction->count);
} }
buttons.back().chosen = reaction->my;
} }
_buttons = std::move(buttons); _buttons = std::move(buttons);
} }
@ -302,7 +308,7 @@ void InlineList::paint(
} }
const auto animating = (button.animation != nullptr); const auto animating = (button.animation != nullptr);
const auto &geometry = button.geometry; const auto &geometry = button.geometry;
const auto mine = (_data.chosenReaction == button.id); const auto mine = button.chosen;
const auto withoutMine = button.count - (mine ? 1 : 0); const auto withoutMine = button.count - (mine ? 1 : 0);
const auto skipImage = animating const auto skipImage = animating
&& (withoutMine < 1 || !button.animation->flying()); && (withoutMine < 1 || !button.animation->flying());
@ -518,10 +524,10 @@ InlineListData InlineListDataFromMessage(not_null<Message*> message) {
} }
auto b = begin(recent); auto b = begin(recent);
auto sum = 0; auto sum = 0;
for (const auto &[emoji, count] : result.reactions) { for (const auto &reaction : result.reactions) {
sum += count; sum += reaction.count;
if (emoji != b->first if (reaction.id != b->first
|| count != b->second.size() || reaction.count != b->second.size()
|| sum > kMaxRecentUserpics) { || sum > kMaxRecentUserpics) {
return false; return false;
} }
@ -537,10 +543,6 @@ InlineListData InlineListDataFromMessage(not_null<Message*> message) {
| ranges::to_vector; | ranges::to_vector;
} }
} }
result.chosenReaction = item->chosenReaction();
if (!result.chosenReaction.empty()) {
--result.reactions[result.chosenReaction];
}
result.flags = (message->hasOutLayout() ? Flag::OutLayout : Flag()) result.flags = (message->hasOutLayout() ? Flag::OutLayout : Flag())
| (message->embedReactionsInBubble() ? Flag::InBubble : Flag()); | (message->embedReactionsInBubble() ? Flag::InBubble : Flag());
return result; return result;

View File

@ -30,6 +30,7 @@ struct ReactionAnimationArgs;
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
using ::Data::ReactionId; using ::Data::ReactionId;
using ::Data::MessageReaction;
class Animation; class Animation;
struct InlineListData { struct InlineListData {
@ -41,9 +42,8 @@ struct InlineListData {
friend inline constexpr bool is_flag_type(Flag) { return true; }; friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>; using Flags = base::flags<Flag>;
base::flat_map<ReactionId, int> reactions; std::vector<MessageReaction> reactions;
base::flat_map<ReactionId, std::vector<not_null<PeerData*>>> recent; base::flat_map<ReactionId, std::vector<not_null<PeerData*>>> recent;
ReactionId chosenReaction;
Flags flags = {}; Flags flags = {};
}; };

View File

@ -315,7 +315,10 @@ object_ptr<Ui::BoxContent> FullListBox(
std::shared_ptr<Api::WhoReadList> whoReadIds) { std::shared_ptr<Api::WhoReadList> whoReadIds) {
Expects(IsServerMsgId(item->id)); Expects(IsServerMsgId(item->id));
if (!item->reactions().contains(selected)) { if (!ranges::contains(
item->reactions(),
selected,
&Data::MessageReaction::id)) {
selected = {}; selected = {};
} }
if (selected.empty() && whoReadIds && !whoReadIds->list.empty()) { if (selected.empty() && whoReadIds && !whoReadIds->list.empty()) {
@ -328,9 +331,10 @@ object_ptr<Ui::BoxContent> FullListBox(
auto map = item->reactions(); auto map = item->reactions();
if (whoReadIds && !whoReadIds->list.empty()) { if (whoReadIds && !whoReadIds->list.empty()) {
map.emplace( map.push_back({
Data::ReactionId{ u"read"_q }, .id = Data::ReactionId{ u"read"_q },
int(whoReadIds->list.size())); .count = int(whoReadIds->list.size()),
});
} }
const auto tabs = CreateTabs( const auto tabs = CreateTabs(
box, box,

View File

@ -275,7 +275,9 @@ void Strip::paintExpandIcon(
p.translate(-target.center()); p.translate(-target.center());
} }
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
st::reactionExpandPanel.paintInCenter(p, to); ((_finalSize == st::reactionCornerImage)
? st::reactionsExpandDropdown
: st::reactionExpandPanel).paintInCenter(p, to);
if (scale != 1.) { if (scale != 1.) {
p.restore(); p.restore();
} }

View File

@ -105,7 +105,7 @@ not_null<Ui::AbstractButton*> CreateTab(
not_null<Tabs*> CreateTabs( not_null<Tabs*> CreateTabs(
not_null<QWidget*> parent, not_null<QWidget*> parent,
const base::flat_map<ReactionId, int> &items, const std::vector<Data::MessageReaction> &items,
const ReactionId &selected, const ReactionId &selected,
Ui::WhoReadType whoReadType) { Ui::WhoReadType whoReadType) {
struct State { struct State {
@ -133,11 +133,11 @@ not_null<Tabs*> CreateTabs(
state->tabs.push_back(tab); state->tabs.push_back(tab);
}; };
auto sorted = std::vector<Entry>(); auto sorted = std::vector<Entry>();
for (const auto &[reaction, count] : items) { for (const auto &reaction : items) {
if (reaction.emoji() == u"read"_q) { if (reaction.id.emoji() == u"read"_q) {
append(reaction, count); append(reaction.id, reaction.count);
} else { } else {
sorted.emplace_back(count, reaction); sorted.emplace_back(reaction.count, reaction.id);
} }
} }
ranges::sort(sorted, std::greater<>(), &Entry::first); ranges::sort(sorted, std::greater<>(), &Entry::first);

View File

@ -13,6 +13,7 @@ enum class WhoReadType;
namespace Data { namespace Data {
struct ReactionId; struct ReactionId;
struct MessageReaction;
} // namespace Data } // namespace Data
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
@ -26,7 +27,7 @@ struct Tabs {
not_null<Tabs*> CreateTabs( not_null<Tabs*> CreateTabs(
not_null<QWidget*> parent, not_null<QWidget*> parent,
const base::flat_map<Data::ReactionId, int> &items, const std::vector<Data::MessageReaction> &items,
const Data::ReactionId &selected, const Data::ReactionId &selected,
Ui::WhoReadType whoReadType); Ui::WhoReadType whoReadType);

View File

@ -1068,8 +1068,12 @@ reactionPremiumLocked: icon{
{ "chat/reactions_premium_star", historyPeerUserpicFg }, { "chat/reactions_premium_star", historyPeerUserpicFg },
}; };
reactionExpandPanel: icon{ reactionExpandPanel: icon{
{ "chat/reactions_expand_bg", historyPeerArchiveUserpicBg }, { "chat/reactions_round_big", windowSubTextFg },
{ "chat/reactions_expand_panel", historyPeerUserpicFg }, { "chat/reactions_expand_panel", windowBg },
};
reactionsExpandDropdown: icon{
{ "chat/reactions_round_small", windowSubTextFg },
{ "chat/reactions_expand_panel", windowBg },
}; };
searchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) { searchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) {

View File

@ -367,7 +367,8 @@ bool ShowReactPremiumError(
not_null<SessionController*> controller, not_null<SessionController*> controller,
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const Data::ReactionId &id) { const Data::ReactionId &id) {
if (item->chosenReaction() == id || controller->session().premium()) { if (controller->session().premium()
|| ranges::contains(item->chosenReactions(), id)) {
return false; return false;
} }
const auto &list = controller->session().data().reactions().list( const auto &list = controller->session().data().reactions().list(

@ -1 +1 @@
Subproject commit 0d234b5aabf43d598e0cb0867566ee570d9e2755 Subproject commit 36fb95c4de1339d2c8921ad6b2911858c3d0e0fa