mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-18 07:07:08 +02:00
Support multiple reactions from one user.
This commit is contained in:
parent
31db1804c8
commit
8a6b3027f5
29 changed files with 354 additions and 186 deletions
Telegram
Resources/icons/chat
reactions_expand_bg.pngreactions_expand_bg@2x.pngreactions_expand_bg@3x.pngreactions_round_big.pngreactions_round_big@2x.pngreactions_round_big@3x.pngreactions_round_small.pngreactions_round_small@2x.pngreactions_round_small@3x.png
SourceFiles
lib_tl
Binary file not shown.
Before ![]() (image error) Size: 418 B |
Binary file not shown.
Before ![]() (image error) Size: 803 B |
Binary file not shown.
Before ![]() (image error) Size: 1.1 KiB |
BIN
Telegram/Resources/icons/chat/reactions_round_big.png
Normal file
BIN
Telegram/Resources/icons/chat/reactions_round_big.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 398 B |
BIN
Telegram/Resources/icons/chat/reactions_round_big@2x.png
Normal file
BIN
Telegram/Resources/icons/chat/reactions_round_big@2x.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 691 B |
BIN
Telegram/Resources/icons/chat/reactions_round_big@3x.png
Normal file
BIN
Telegram/Resources/icons/chat/reactions_round_big@3x.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1 KiB |
BIN
Telegram/Resources/icons/chat/reactions_round_small.png
Normal file
BIN
Telegram/Resources/icons/chat/reactions_round_small.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 359 B |
BIN
Telegram/Resources/icons/chat/reactions_round_small@2x.png
Normal file
BIN
Telegram/Resources/icons/chat/reactions_round_small@2x.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 670 B |
BIN
Telegram/Resources/icons/chat/reactions_round_small@3x.png
Normal file
BIN
Telegram/Resources/icons/chat/reactions_round_small@3x.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 947 B |
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue