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;
if (resolveWhoReacted) {
const auto &list = item->reactions();
state->current.fullReactionsCount = reaction.empty()
? ranges::accumulate(
state->current.fullReactionsCount = [&] {
if (reaction.empty()) {
return ranges::accumulate(
list,
0,
ranges::plus{},
&Data::MessageReaction::count);
}
const auto i = ranges::find(
list,
0,
ranges::plus{},
[](const auto &pair) { return pair.second; })
: list.contains(reaction)
? list.find(reaction)->second
: 0;
reaction,
&Data::MessageReaction::id);
return (i != end(list)) ? i->count : 0;
}();
// #TODO reactions
state->current.singleReaction = (!reaction.empty()
? reaction
: (list.size() == 1)
? list.front().first
? list.front().id
: ReactionId()).emoji();
}
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) {
return a.data < b.data;
}

View File

@ -63,6 +63,18 @@ constexpr auto kTopReactionsLimit = 10;
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
PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
@ -74,22 +86,43 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
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 &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);
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());
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;
if (all.contains(id)) {
result.recent.push_back(&reaction);
if (!added.contains(id)) {
if (const auto temp = reactions->lookupTemporary(id)) {
result.recent.push_back(temp);
}
}
}
} else {
@ -97,22 +130,21 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
result.recent.reserve((allowed.type == AllowedReactionsType::Some)
? allowed.some.size()
: full.size());
for (const auto &reaction : full) {
add([&](const Reaction &reaction) {
const auto id = reaction.id;
if ((allowed.type == AllowedReactionsType::Some)
&& !ranges::contains(allowed.some, id)) {
continue;
return false;
} else if (reaction.premium
&& !session->premium()
&& !all.contains(id)) {
&& !ranges::contains(all, id, &MessageReaction::id)) {
if (session->premiumPossible()) {
result.morePremiumAvailable = true;
}
continue;
} else {
result.recent.push_back(&reaction);
return false;
}
}
return true;
});
result.customAllowed = (allowed.type == AllowedReactionsType::All);
}
const auto i = ranges::find(
@ -564,14 +596,7 @@ std::optional<Reaction> Reactions::resolveById(const ReactionId &id) {
} 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 CustomReaction(document);
}
}
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();
auto &api = _owner->session().api();
auto i = _sentRequests.find(id);
@ -646,14 +671,17 @@ void Reactions::send(not_null<HistoryItem*> item, const ReactionId &chosen) {
} else {
i = _sentRequests.emplace(id).first;
}
const auto flags = chosen.empty()
? MTPmessages_SendReaction::Flag(0)
: MTPmessages_SendReaction::Flag::f_reaction;
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>(1, ReactionToMTP(chosen))
MTP_vector<MTPReaction>(chosen | ranges::views::transform(
ReactionToMTP
) | ranges::to<QVector<MTPReaction>>())
)).done([=](const MTPUpdates &result) {
_sentRequests.remove(id);
_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() {
const auto now = crl::now();
auto closest = crl::time();
@ -784,45 +838,84 @@ MessageReactions::MessageReactions(not_null<HistoryItem*> item)
: _item(item) {
}
void MessageReactions::add(const ReactionId &reaction) {
if (_chosen == reaction) {
return;
}
void MessageReactions::add(const ReactionId &id, bool addToRecent) {
Expects(!id.empty());
const auto history = _item->history();
const auto self = history->session().user();
if (!_chosen.empty()) {
const auto i = _list.find(_chosen);
Assert(i != end(_list));
--i->second;
const auto removed = !i->second;
if (removed) {
_list.erase(i);
const auto myLimit = self->isPremium() ? 5 : 1; // #TODO reactions
if (ranges::contains(chosen(), id)) {
return;
}
auto my = 0;
_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
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)) {
j->second.erase(
ranges::remove(j->second, self, &RecentReaction::peer),
end(j->second));
if (j->second.empty() || removed) {
if (j->second.empty()) {
_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;
if (!reaction.empty()) {
if (_item->canViewReactions()) {
auto &list = _recent[reaction];
list.insert(begin(list), RecentReaction{ self });
}
++_list[reaction];
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, _chosen);
owner.reactions().send(_item, addToRecent);
owner.notifyItemDataChange(_item);
}
void MessageReactions::remove() {
add(ReactionId());
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 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(
@ -836,31 +929,31 @@ bool MessageReactions::checkIfChanged(
auto existing = base::flat_set<ReactionId>();
for (const auto &count : list) {
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 i = _list.find(reaction);
const auto wasCount = (i != end(_list)) ? i->second : 0;
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(reaction);
existing.emplace(id);
return false;
});
if (changed) {
return true;
}
}
for (const auto &[reaction, count] : _list) {
if (!existing.contains(reaction)) {
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 emoji = ReactionFromMTP(data.vreaction());
if (_list.contains(emoji)) {
parsed[emoji].push_back(RecentReaction{
const auto id = ReactionFromMTP(data.vreaction());
if (ranges::contains(_list, id, &MessageReaction::id)) {
parsed[id].push_back(RecentReaction{
.peer = owner.peer(peerFromMTP(data.vpeer_id())),
.unread = data.is_unread(),
.big = data.is_big(),
@ -890,50 +983,79 @@ bool MessageReactions::change(
}
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 reaction = ReactionFromMTP(data.vreaction());
if (!ignoreChosen) {
if (data.vchosen_order() && _chosen != reaction) {
_chosen = reaction;
changed = true;
} else if (!data.vchosen_order() && _chosen == reaction) {
_chosen = ReactionId();
const auto id = ReactionFromMTP(data.vreaction());
const auto &chosen = data.vchosen_order();
if (!ignoreChosen && 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 = (!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;
}
}
const auto nowCount = data.vcount().v;
auto &wasCount = _list[reaction];
if (wasCount != nowCount) {
wasCount = nowCount;
changed = true;
}
existing.emplace(reaction);
existing.emplace(id);
});
}
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()) {
changed = true;
for (auto i = begin(_list); i != end(_list);) {
if (!existing.contains(i->first)) {
if (!existing.contains(i->id)) {
i = _list.erase(i);
} else {
++i;
}
}
if (!_chosen.empty() && !_list.contains(_chosen)) {
_chosen = ReactionId();
}
}
auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();
for (const auto &reaction : recent) {
reaction.match([&](const MTPDmessagePeerReaction &data) {
const auto emoji = ReactionFromMTP(data.vreaction());
if (_list.contains(emoji)) {
parsed[emoji].push_back(RecentReaction{
.peer = owner.peer(peerFromMTP(data.vpeer_id())),
.unread = data.is_unread(),
.big = data.is_big(),
});
const auto id = ReactionFromMTP(data.vreaction());
const auto i = ranges::find(_list, id, &MessageReaction::id);
if (i != end(_list)) {
auto &list = parsed[id];
if (list.size() < i->count) {
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;
}
const base::flat_map<ReactionId, int> &MessageReactions::list() const {
const std::vector<MessageReaction> &MessageReactions::list() const {
return _list;
}
@ -974,8 +1096,11 @@ void MessageReactions::markRead() {
}
}
ReactionId MessageReactions::chosen() const {
return _chosen;
std::vector<ReactionId> MessageReactions::chosen() const {
return _list
| ranges::views::filter(&MessageReaction::my)
| ranges::views::transform(&MessageReaction::id)
| ranges::to_vector;
}
} // namespace Data

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ struct ReactionAnimationArgs;
namespace HistoryView::Reactions {
using ::Data::ReactionId;
using ::Data::MessageReaction;
class Animation;
struct InlineListData {
@ -41,9 +42,8 @@ struct InlineListData {
friend inline constexpr bool is_flag_type(Flag) { return true; };
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;
ReactionId chosenReaction;
Flags flags = {};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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