Implement recent reaction userpics in groups.

This commit is contained in:
John Preston 2022-01-13 13:13:08 +03:00
parent 2dec1b72f7
commit f3e84de5fb
7 changed files with 204 additions and 12 deletions

View file

@ -438,19 +438,32 @@ void MessageReactions::add(const QString &reaction) {
if (_chosen == reaction) { if (_chosen == reaction) {
return; return;
} }
const auto history = _item->history();
const auto self = history->session().user();
if (!_chosen.isEmpty()) { if (!_chosen.isEmpty()) {
const auto i = _list.find(_chosen); const auto i = _list.find(_chosen);
Assert(i != end(_list)); Assert(i != end(_list));
--i->second; --i->second;
if (!i->second) { const auto removed = !i->second;
if (removed) {
_list.erase(i); _list.erase(i);
} }
const auto j = _recent.find(_chosen);
if (j != end(_recent)) {
j->second.erase(ranges::remove(j->second, self), end(j->second));
if (j->second.empty() || removed) {
_recent.erase(j);
}
}
} }
_chosen = reaction; _chosen = reaction;
if (!reaction.isEmpty()) { if (!reaction.isEmpty()) {
if (_item->canViewReactions()) {
_recent[reaction].push_back(self);
}
++_list[reaction]; ++_list[reaction];
} }
auto &owner = _item->history()->owner(); auto &owner = history->owner();
owner.reactions().send(_item, _chosen); owner.reactions().send(_item, _chosen);
owner.notifyItemDataChange(_item); owner.notifyItemDataChange(_item);
} }
@ -461,8 +474,10 @@ void MessageReactions::remove() {
void MessageReactions::set( void MessageReactions::set(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
const QVector<MTPMessageUserReaction> &recent,
bool ignoreChosen) { bool ignoreChosen) {
if (_item->history()->owner().reactions().sending(_item)) { auto &owner = _item->history()->owner();
if (owner.reactions().sending(_item)) {
// We'll apply non-stale data from the request response. // We'll apply non-stale data from the request response.
return; return;
} }
@ -499,8 +514,24 @@ void MessageReactions::set(
_chosen = QString(); _chosen = QString();
} }
} }
auto parsed = base::flat_map<
QString,
std::vector<not_null<UserData*>>>();
for (const auto &reaction : recent) {
reaction.match([&](const MTPDmessageUserReaction &data) {
const auto emoji = qs(data.vreaction());
if (_list.contains(emoji)) {
parsed[emoji].push_back(owner.user(data.vuser_id()));
}
});
}
if (_recent != parsed) {
_recent = std::move(parsed);
changed = true;
}
if (changed) { if (changed) {
_item->history()->owner().notifyItemDataChange(_item); owner.notifyItemDataChange(_item);
} }
} }
@ -508,6 +539,11 @@ const base::flat_map<QString, int> &MessageReactions::list() const {
return _list; return _list;
} }
auto MessageReactions::recent() const
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
return _recent;
}
bool MessageReactions::empty() const { bool MessageReactions::empty() const {
return _list.empty(); return _list.empty();
} }

View file

@ -125,8 +125,13 @@ public:
void add(const QString &reaction); void add(const QString &reaction);
void remove(); void remove();
void set(const QVector<MTPReactionCount> &list, bool ignoreChosen); void set(
const QVector<MTPReactionCount> &list,
const QVector<MTPMessageUserReaction> &recent,
bool ignoreChosen);
[[nodiscard]] const base::flat_map<QString, int> &list() const; [[nodiscard]] const base::flat_map<QString, int> &list() const;
[[nodiscard]] auto recent() const
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> &;
[[nodiscard]] QString chosen() const; [[nodiscard]] QString chosen() const;
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
@ -135,6 +140,7 @@ private:
QString _chosen; QString _chosen;
base::flat_map<QString, int> _list; base::flat_map<QString, int> _list;
base::flat_map<QString, std::vector<not_null<UserData*>>> _recent;
}; };

View file

@ -834,7 +834,10 @@ void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
} else if (!_reactions) { } else if (!_reactions) {
_reactions = std::make_unique<Data::MessageReactions>(this); _reactions = std::make_unique<Data::MessageReactions>(this);
} }
_reactions->set(data.vresults().v, data.is_min()); _reactions->set(
data.vresults().v,
data.vrecent_reactons().value_or_empty(),
data.is_min());
}); });
} }
@ -847,6 +850,14 @@ const base::flat_map<QString, int> &HistoryItem::reactions() const {
return _reactions ? _reactions->list() : kEmpty; return _reactions ? _reactions->list() : kEmpty;
} }
auto HistoryItem::recentReactions() const
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
static const auto kEmpty = base::flat_map<
QString,
std::vector<not_null<UserData*>>>();
return _reactions ? _reactions->recent() : kEmpty;
}
bool HistoryItem::canViewReactions() const { bool HistoryItem::canViewReactions() const {
return (_flags & MessageFlag::CanViewReactions) return (_flags & MessageFlag::CanViewReactions)
&& _reactions && _reactions

View file

@ -359,6 +359,8 @@ public:
void updateReactions(const MTPMessageReactions *reactions); void updateReactions(const MTPMessageReactions *reactions);
void updateReactionsUnknown(); void updateReactionsUnknown();
[[nodiscard]] const base::flat_map<QString, int> &reactions() const; [[nodiscard]] const base::flat_map<QString, int> &reactions() const;
[[nodiscard]] auto recentReactions() const
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> &;
[[nodiscard]] bool canViewReactions() const; [[nodiscard]] bool canViewReactions() const;
[[nodiscard]] QString chosenReaction() const; [[nodiscard]] QString chosenReaction() const;
[[nodiscard]] crl::time lastReactionsRefreshTime() const; [[nodiscard]] crl::time lastReactionsRefreshTime() const;

View file

@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_message.h" #include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_react_animation.h" #include "history/view/history_view_react_animation.h"
#include "history/view/history_view_group_call_bar.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/data_user.h"
#include "lang/lang_tag.h" #include "lang/lang_tag.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
@ -22,6 +24,7 @@ namespace {
constexpr auto kInNonChosenOpacity = 0.12; constexpr auto kInNonChosenOpacity = 0.12;
constexpr auto kOutNonChosenOpacity = 0.18; constexpr auto kOutNonChosenOpacity = 0.18;
constexpr auto kMaxRecentUserpics = 3;
[[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) { [[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) {
serviceBg.setAlpha(std::max(serviceBg.alpha(), 192)); serviceBg.setAlpha(std::max(serviceBg.alpha(), 192));
@ -92,7 +95,12 @@ void InlineList::layoutButtons() {
? std::move(*i) ? std::move(*i)
: prepareButtonWithEmoji(emoji)); : prepareButtonWithEmoji(emoji));
const auto add = (emoji == _data.chosenReaction) ? 1 : 0; const auto add = (emoji == _data.chosenReaction) ? 1 : 0;
setButtonCount(buttons.back(), count + add); const auto j = _data.recent.find(emoji);
if (j != end(_data.recent) && !j->second.empty()) {
setButtonUserpics(buttons.back(), j->second);
} else {
setButtonCount(buttons.back(), count + add);
}
} }
_buttons = std::move(buttons); _buttons = std::move(buttons);
} }
@ -104,14 +112,53 @@ InlineList::Button InlineList::prepareButtonWithEmoji(const QString &emoji) {
} }
void InlineList::setButtonCount(Button &button, int count) { void InlineList::setButtonCount(Button &button, int count) {
if (button.count == count) { if (button.count == count && !button.userpics) {
return; return;
} }
button.userpics = nullptr;
button.count = count; button.count = count;
button.countText = Lang::FormatCountToShort(count).string; button.countText = Lang::FormatCountToShort(count).string;
button.countTextWidth = st::semiboldFont->width(button.countText); button.countTextWidth = st::semiboldFont->width(button.countText);
} }
void InlineList::setButtonUserpics(
Button &button,
const std::vector<not_null<UserData*>> &users) {
if (!button.userpics) {
button.userpics = std::make_unique<Userpics>();
}
const auto count = int(users.size());
auto &list = button.userpics->list;
const auto regenerate = [&] {
if (list.size() != count) {
return true;
}
for (auto i = 0; i != count; ++i) {
if (users[i] != list[i].peer) {
return true;
}
}
return false;
}();
if (!regenerate) {
return;
}
auto generated = std::vector<UserpicInRow>();
generated.reserve(count);
for (auto i = 0; i != count; ++i) {
if (i == list.size()) {
list.push_back(UserpicInRow{
users[i]
});
} else if (list[i].peer != users[i]) {
list[i].peer = users[i];
}
}
while (list.size() > count) {
list.pop_back();
}
}
QSize InlineList::countOptimalSize() { QSize InlineList::countOptimalSize() {
if (_buttons.empty()) { if (_buttons.empty()) {
return _skipBlock; return _skipBlock;
@ -123,13 +170,26 @@ QSize InlineList::countOptimalSize() {
const auto between = st::reactionInlineBetween; const auto between = st::reactionInlineBetween;
const auto padding = st::reactionInlinePadding; const auto padding = st::reactionInlinePadding;
const auto size = st::reactionInlineSize; const auto size = st::reactionInlineSize;
const auto widthBase = padding.left() const auto widthBaseCount = padding.left()
+ size + size
+ st::reactionInlineSkip + st::reactionInlineSkip
+ padding.right(); + padding.right();
const auto widthBaseUserpics = padding.left()
+ size
+ st::reactionInlineUserpicsPadding.left()
+ st::reactionInlineUserpicsPadding.right();
const auto userpicsWidth = [](const Button &button) {
const auto count = int(button.userpics->list.size());
const auto single = st::reactionInlineUserpics.size;
const auto shift = st::reactionInlineUserpics.shift;
const auto width = single + (count - 1) * (single - shift);
return width;
};
const auto height = padding.top() + size + padding.bottom(); const auto height = padding.top() + size + padding.bottom();
for (auto &button : _buttons) { for (auto &button : _buttons) {
const auto width = widthBase + button.countTextWidth; const auto width = button.userpics
? (widthBaseUserpics + userpicsWidth(button))
: (widthBaseCount + button.countTextWidth);
button.geometry.setSize({ width, height }); button.geometry.setSize({ width, height });
x += width + between; x += width + between;
} }
@ -243,7 +303,16 @@ void InlineList::paint(
return _animation->paintGetArea(p, QPoint(), image); return _animation->paintGetArea(p, QPoint(), image);
}; };
} }
if (!skipBubble) { if (skipBubble) {
continue;
}
resolveUserpicsImage(button);
if (button.userpics) {
p.drawImage(
inner.x() + size + st::reactionInlineUserpicsPadding.left(),
geometry.y() + st::reactionInlineUserpicsPadding.top(),
button.userpics->image);
} else {
p.setPen(!inbubble p.setPen(!inbubble
? (chosen ? (chosen
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c)) ? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
@ -299,6 +368,35 @@ void InlineList::animateSend(
st::reactionInlineImage); st::reactionInlineImage);
} }
void InlineList::resolveUserpicsImage(const Button &button) const {
const auto userpics = button.userpics.get();
const auto regenerate = [&] {
if (!userpics) {
return false;
} else if (userpics->image.isNull()) {
return true;
}
for (auto &entry : userpics->list) {
const auto peer = entry.peer;
auto &view = entry.view;
const auto wasView = view.get();
if (peer->userpicUniqueKey(view) != entry.uniqueKey
|| view.get() != wasView) {
return true;
}
}
return false;
}();
if (!regenerate) {
return;
}
GenerateUserpicsInRow(
userpics->image,
userpics->list,
st::reactionInlineUserpics,
kMaxRecentUserpics);
}
std::unique_ptr<SendAnimation> InlineList::takeSendAnimation() { std::unique_ptr<SendAnimation> InlineList::takeSendAnimation() {
return std::move(_animation); return std::move(_animation);
} }
@ -311,9 +409,28 @@ void InlineList::continueSendAnimation(
InlineListData InlineListDataFromMessage(not_null<Message*> message) { InlineListData InlineListDataFromMessage(not_null<Message*> message) {
using Flag = InlineListData::Flag; using Flag = InlineListData::Flag;
const auto item = message->message(); const auto item = message->message();
auto result = InlineListData(); auto result = InlineListData();
result.reactions = item->reactions(); result.reactions = item->reactions();
const auto &recent = item->recentReactions();
const auto showUserpics = [&] {
if (recent.size() != result.reactions.size()) {
return false;
}
auto b = begin(recent);
auto sum = 0;
for (const auto &[emoji, count] : result.reactions) {
sum += count;
if (emoji != b->first
|| count != b->second.size()
|| sum > kMaxRecentUserpics) {
return false;
}
}
return true;
}();
if (showUserpics) {
result.recent = recent;
}
result.chosenReaction = item->chosenReaction(); result.chosenReaction = item->chosenReaction();
if (!result.chosenReaction.isEmpty()) { if (!result.chosenReaction.isEmpty()) {
--result.reactions[result.chosenReaction]; --result.reactions[result.chosenReaction];

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data { namespace Data {
class Reactions; class Reactions;
class CloudImageView;
} // namespace Data } // namespace Data
namespace Ui { namespace Ui {
@ -22,6 +23,7 @@ using PaintContext = Ui::ChatPaintContext;
class Message; class Message;
struct TextState; struct TextState;
struct SendReactionAnimationArgs; struct SendReactionAnimationArgs;
struct UserpicInRow;
} // namespace HistoryView } // namespace HistoryView
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
@ -37,6 +39,7 @@ struct InlineListData {
using Flags = base::flags<Flag>; using Flags = base::flags<Flag>;
base::flat_map<QString, int> reactions; base::flat_map<QString, int> reactions;
base::flat_map<QString, std::vector<not_null<UserData*>>> recent;
QString chosenReaction; QString chosenReaction;
Flags flags = {}; Flags flags = {};
}; };
@ -74,10 +77,16 @@ public:
void continueSendAnimation(std::unique_ptr<SendAnimation> animation); void continueSendAnimation(std::unique_ptr<SendAnimation> animation);
private: private:
struct Userpics {
QImage image;
std::vector<UserpicInRow> list;
bool someNotLoaded = false;
};
struct Button { struct Button {
QRect geometry; QRect geometry;
mutable QImage image; mutable QImage image;
mutable ClickHandlerPtr link; mutable ClickHandlerPtr link;
std::unique_ptr<Userpics> userpics;
QString emoji; QString emoji;
QString countText; QString countText;
int count = 0; int count = 0;
@ -88,7 +97,11 @@ private:
void layoutButtons(); void layoutButtons();
void setButtonCount(Button &button, int count); void setButtonCount(Button &button, int count);
void setButtonUserpics(
Button &button,
const std::vector<not_null<UserData*>> &users);
[[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji); [[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji);
void resolveUserpicsImage(const Button &button) const;
QSize countOptimalSize() override; QSize countOptimalSize() override;

View file

@ -969,6 +969,13 @@ reactionInlineImage: 28px;
reactionInlineSkip: 3px; reactionInlineSkip: 3px;
reactionInlineBetween: 4px; reactionInlineBetween: 4px;
reactionInlineInBubbleLeft: -3px; reactionInlineInBubbleLeft: -3px;
reactionInlineUserpicsPadding: margins(1px, 1px, 1px, 1px);
reactionInlineUserpics: GroupCallUserpics {
size: 18px;
shift: 7px;
stroke: 1px;
align: align(left);
}
reactionInfoSize: 15px; reactionInfoSize: 15px;
reactionInfoImage: 30px; reactionInfoImage: 30px;