Support multiple reactions from one user.
Before Width: | Height: | Size: 418 B |
Before Width: | Height: | Size: 803 B |
Before Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 398 B |
After Width: | Height: | Size: 691 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 359 B |
After Width: | Height: | Size: 670 B |
After Width: | Height: | Size: 947 B |
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|