Display custom emoji reactions under messages.

This commit is contained in:
John Preston 2022-08-26 13:35:19 +04:00
parent 14f937cb02
commit ba83836922
17 changed files with 178 additions and 47 deletions

View file

@ -592,8 +592,13 @@ bool WhoReadExists(not_null<HistoryItem*> item) {
return true;
}
bool WhoReactedExists(not_null<HistoryItem*> item) {
return item->canViewReactions() || WhoReadExists(item);
bool WhoReactedExists(
not_null<HistoryItem*> item,
WhoReactedList list) {
if (item->canViewReactions() || WhoReadExists(item)) {
return true;
}
return (list == WhoReactedList::One) && item->history()->peer->isUser();
}
rpl::producer<Ui::WhoReadContent> WhoReacted(

View file

@ -24,8 +24,15 @@ struct ReactionId;
namespace Api {
enum class WhoReactedList {
All,
One,
};
[[nodiscard]] bool WhoReadExists(not_null<HistoryItem*> item);
[[nodiscard]] bool WhoReactedExists(not_null<HistoryItem*> item);
[[nodiscard]] bool WhoReactedExists(
not_null<HistoryItem*> item,
WhoReactedList list);
struct WhoReadList {
std::vector<PeerId> list;

View file

@ -77,11 +77,12 @@ constexpr auto kTopReactionsLimit = 10;
} // namespace
PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item) {
if (!item->canReact()) {
return {};
}
auto result = PossibleItemReactions();
auto result = PossibleItemReactionsRef();
const auto peer = item->history()->peer;
const auto session = &peer->session();
const auto reactions = &session->data().reactions();
@ -157,6 +158,15 @@ PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
return result;
}
PossibleItemReactions::PossibleItemReactions(
const PossibleItemReactionsRef &other)
: recent(other.recent | ranges::views::transform([](const auto &value) {
return *value;
}) | ranges::to_vector)
, morePremiumAvailable(other.morePremiumAvailable)
, customAllowed(other.customAllowed) {
}
Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _topRefreshTimer([=] { refreshTop(); })
@ -194,6 +204,10 @@ Reactions::Reactions(not_null<Session*> owner)
Reactions::~Reactions() = default;
Main::Session &Reactions::session() const {
return _owner->session();
}
void Reactions::refreshTop() {
requestTop();
}
@ -868,7 +882,7 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
}
return removed;
}), end(_list));
if (_item->canViewReactions()) {
if (_item->canViewReactions() || history->peer->isUser()) {
auto &list = _recent[id];
list.insert(begin(list), RecentReaction{ self });
}

View file

@ -38,13 +38,22 @@ struct Reaction {
bool premium = false;
};
struct PossibleItemReactions {
struct PossibleItemReactionsRef {
std::vector<not_null<const Reaction*>> recent;
bool morePremiumAvailable = false;
bool customAllowed = false;
};
[[nodiscard]] PossibleItemReactions LookupPossibleReactions(
struct PossibleItemReactions {
PossibleItemReactions() = default;
explicit PossibleItemReactions(const PossibleItemReactionsRef &other);
std::vector<Reaction> recent;
bool morePremiumAvailable = false;
bool customAllowed = false;
};
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item);
class Reactions final : private CustomEmojiManager::Listener {
@ -52,6 +61,11 @@ public:
explicit Reactions(not_null<Session*> owner);
~Reactions();
[[nodiscard]] Session &owner() const {
return *_owner;
}
[[nodiscard]] Main::Session &session() const;
void refreshTop();
void refreshRecent();
void refreshRecentDelayed();

View file

@ -2011,13 +2011,15 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
const auto hasWhoReactedItem = _dragStateItem
&& Api::WhoReactedExists(_dragStateItem);
&& Api::WhoReactedExists(_dragStateItem, Api::WhoReactedList::All);
const auto clickedReaction = link
? link->property(
kReactionsCountEmojiProperty).value<Data::ReactionId>()
: Data::ReactionId();
_whoReactedMenuLifetime.destroy();
if (hasWhoReactedItem && !clickedReaction.empty()) {
if (!clickedReaction.empty()
&& _dragStateItem
&& Api::WhoReactedExists(_dragStateItem, Api::WhoReactedList::One)) {
HistoryView::ShowWhoReactedMenu(
&_menu,
e->globalPos(),

View file

@ -948,7 +948,8 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
: nullptr;
const auto hasSelection = !request.selectedItems.empty()
|| !request.selectedText.empty();
const auto hasWhoReactedItem = item && Api::WhoReactedExists(item);
const auto hasWhoReactedItem = item
&& Api::WhoReactedExists(item, Api::WhoReactedList::All);
auto result = base::make_unique_q<Ui::PopupMenu>(
list,

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_custom_emoji.h"
#include "history/view/reactions/history_view_reactions_animation.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/reactions/history_view_reactions.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
#include "base/unixtime.h"
@ -430,6 +431,20 @@ void Element::prepareCustomEmojiPaint(
}
}
void Element::prepareCustomEmojiPaint(
Painter &p,
const Reactions::InlineList &reactions) const {
if (!reactions.hasCustomEmoji()) {
return;
}
clearCustomEmojiRepaint();
p.setInactive(delegate()->elementIsGifPaused());
if (!_heavyCustomEmoji) {
_heavyCustomEmoji = true;
history()->owner().registerHeavyViewPart(const_cast<Element*>(this));
}
}
//void Element::externalLottieProgressing(bool external) const {
// if (const auto media = _media.get()) {
// media->externalLottieProgressing(external);

View file

@ -49,6 +49,7 @@ using PaintContext = Ui::ChatPaintContext;
namespace Reactions {
struct ButtonParameters;
class Animation;
class InlineList;
} // namespace Reactions
enum class Context : char {
@ -421,6 +422,9 @@ public:
void prepareCustomEmojiPaint(
Painter &p,
const Ui::Text::String &text) const;
void prepareCustomEmojiPaint(
Painter &p,
const Reactions::InlineList &reactions) const;
void clearCustomEmojiRepaint() const;
[[nodiscard]] ClickHandlerPtr fromPhotoLink() const {

View file

@ -2210,13 +2210,15 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
? _overElement->data().get()
: nullptr;
const auto hasWhoReactedItem = overItem
&& Api::WhoReactedExists(overItem);
&& Api::WhoReactedExists(overItem, Api::WhoReactedList::All);
const auto clickedReaction = link
? link->property(
kReactionsCountEmojiProperty).value<Data::ReactionId>()
: Data::ReactionId();
_whoReactedMenuLifetime.destroy();
if (hasWhoReactedItem && !clickedReaction.empty()) {
if (!clickedReaction.empty()
&& overItem
&& Api::WhoReactedExists(overItem, Api::WhoReactedList::One)) {
HistoryView::ShowWhoReactedMenu(
&_menu,
e->globalPos(),

View file

@ -739,6 +739,7 @@ void Message::draw(Painter &p, const PaintContext &context) const {
g.setHeight(g.height() - reactionsHeight);
const auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);
p.translate(reactionsPosition);
prepareCustomEmojiPaint(p, *_reactions);
_reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition));
if (context.reactionInfo) {
context.reactionInfo->position = reactionsPosition;
@ -794,6 +795,7 @@ void Message::draw(Painter &p, const PaintContext &context) const {
trect.setHeight(trect.height() - reactionsHeight);
const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop);
p.translate(reactionsPosition);
prepareCustomEmojiPaint(p, *_reactions);
_reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition));
if (context.reactionInfo) {
context.reactionInfo->position = reactionsPosition;
@ -1385,6 +1387,9 @@ bool Message::hasHeavyPart() const {
void Message::unloadHeavyPart() {
Element::unloadHeavyPart();
if (_reactions) {
_reactions->unloadCustomEmoji();
}
_comments = nullptr;
}
@ -2232,6 +2237,7 @@ void Message::refreshReactions() {
_reactions = std::make_unique<InlineList>(
&item->history()->owner().reactions(),
handlerFactory,
[=] { customEmojiRepaint(); },
std::move(reactionsData));
} else {
_reactions->update(std::move(reactionsData), width());

View file

@ -14,9 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_group_call_bar.h"
#include "core/click_handler_types.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "lang/lang_tag.h"
#include "ui/text/text_block.h"
#include "ui/chat/chat_style.h"
#include "styles/style_chat.h"
@ -40,6 +43,7 @@ struct InlineList::Button {
mutable std::unique_ptr<Animation> animation;
mutable QImage image;
mutable ClickHandlerPtr link;
mutable std::unique_ptr<Ui::Text::CustomEmoji> custom;
std::unique_ptr<Userpics> userpics;
ReactionId id;
QString countText;
@ -51,9 +55,11 @@ struct InlineList::Button {
InlineList::InlineList(
not_null<::Data::Reactions*> owner,
Fn<ClickHandlerPtr(ReactionId)> handlerFactory,
Fn<void()> customEmojiRepaint,
Data &&data)
: _owner(owner)
, _handlerFactory(std::move(handlerFactory))
, _customEmojiRepaint(std::move(customEmojiRepaint))
, _data(std::move(data)) {
layout();
}
@ -76,6 +82,21 @@ void InlineList::removeSkipBlock() {
_skipBlock = {};
}
bool InlineList::hasCustomEmoji() const {
return _hasCustomEmoji;
}
void InlineList::unloadCustomEmoji() {
if (!hasCustomEmoji()) {
return;
}
for (const auto &button : _buttons) {
if (const auto custom = button.custom.get()) {
custom->unload();
}
}
}
void InlineList::layout() {
layoutButtons();
initDimensions();
@ -106,6 +127,7 @@ void InlineList::layoutButtons() {
< ranges::find(list, b->id, &::Data::Reaction::id);
});
_hasCustomEmoji = false;
auto buttons = std::vector<Button>();
buttons.reserve(sorted.size());
for (const auto &reaction : sorted) {
@ -121,13 +143,22 @@ void InlineList::layoutButtons() {
setButtonCount(buttons.back(), reaction->count);
}
buttons.back().chosen = reaction->my;
if (id.custom()) {
_hasCustomEmoji = true;
}
}
_buttons = std::move(buttons);
}
InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) {
auto result = Button{ .id = id };
_owner->preloadImageFor(id);
if (const auto customId = id.custom()) {
result.custom = _owner->owner().customEmojiManager().create(
customId,
_customEmojiRepaint);
} else {
_owner->preloadImageFor(id);
}
return result;
}
@ -353,16 +384,44 @@ void InlineList::paint(
p.setOpacity(bubbleProgress);
}
}
if (button.image.isNull()) {
if (!button.custom && button.image.isNull()) {
button.image = _owner->resolveImageFor(
button.id,
::Data::Reactions::ImageSize::InlineList);
}
const auto textFg = !inbubble
? (chosen
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
: st->msgServiceFg())
: !chosen
? stm->msgServiceFg
: context.outbg
? (context.selected()
? st->historyFileOutIconFgSelected()
: st->historyFileOutIconFg())
: (context.selected()
? st->historyFileInIconFgSelected()
: st->historyFileInIconFg());
const auto image = QRect(
inner.topLeft() + QPoint(skip, skip),
QSize(st::reactionInlineImage, st::reactionInlineImage));
if (!button.image.isNull() && !skipImage) {
p.drawImage(image.topLeft(), button.image);
if (!skipImage) {
if (button.custom) {
if (!_customSkip) {
using namespace Ui::Text;
const auto size = st::emojiSize;
_customSkip = (size - AdjustCustomEmojiSize(size)) / 2;
}
button.custom->paint(p, {
.preview = textFg.color(),
.now = context.now,
.position = (inner.topLeft()
+ QPoint(_customSkip, _customSkip)),
.paused = p.inactive(),
});
} else if (!button.image.isNull()) {
p.drawImage(image.topLeft(), button.image);
}
}
if (animating) {
animations.push_back({
@ -381,19 +440,7 @@ void InlineList::paint(
geometry.y() + st::reactionInlineUserpicsPadding.top(),
button.userpics->image);
} else {
p.setPen(!inbubble
? (chosen
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
: st->msgServiceFg())
: !chosen
? stm->msgServiceFg
: context.outbg
? (context.selected()
? st->historyFileOutIconFgSelected()
: st->historyFileOutIconFg())
: (context.selected()
? st->historyFileInIconFgSelected()
: st->historyFileInIconFg()));
p.setPen(textFg);
const auto textTop = geometry.y()
+ ((geometry.height() - st::semiboldFont->height) / 2);
p.drawText(

View file

@ -53,6 +53,7 @@ public:
InlineList(
not_null<::Data::Reactions*> owner,
Fn<ClickHandlerPtr(ReactionId)> handlerFactory,
Fn<void()> customEmojiRepaint,
Data &&data);
~InlineList();
@ -65,6 +66,9 @@ public:
void updateSkipBlock(int width, int height);
void removeSkipBlock();
[[nodiscard]] bool hasCustomEmoji() const;
void unloadCustomEmoji();
void paint(
Painter &p,
const PaintContext &context,
@ -105,9 +109,12 @@ private:
const not_null<::Data::Reactions*> _owner;
const Fn<ClickHandlerPtr(ReactionId)> _handlerFactory;
const Fn<void()> _customEmojiRepaint;
Data _data;
std::vector<Button> _buttons;
QSize _skipBlock;
mutable int _customSkip = 0;
bool _hasCustomEmoji = false;
};

View file

@ -441,7 +441,7 @@ void Manager::showButtonDelayed() {
[=]{ updateButton({}); });
}
void Manager::applyList(Data::PossibleItemReactions &&reactions) {
void Manager::applyList(const Data::PossibleItemReactionsRef &reactions) {
using Button = Strip::AddedButton;
_strip.applyList(
reactions.recent,

View file

@ -23,7 +23,7 @@ class PopupMenu;
namespace Data {
struct ReactionId;
struct Reaction;
struct PossibleItemReactions;
struct PossibleItemReactionsRef;
class DocumentMedia;
} // namespace Data
@ -143,7 +143,7 @@ public:
using ReactionId = ::Data::ReactionId;
void applyList(Data::PossibleItemReactions &&reactions);
void applyList(const Data::PossibleItemReactionsRef &reactions);
void updateButton(ButtonParameters parameters);
void paint(Painter &p, const PaintContext &context);

View file

@ -144,12 +144,12 @@ bool StripEmoji::ready() {
Selector::Selector(
not_null<QWidget*> parent,
not_null<Window::SessionController*> parentController,
Data::PossibleItemReactions &&reactions,
const Data::PossibleItemReactionsRef &reactions,
IconFactory iconFactory,
Fn<void(bool fast)> close)
: RpWidget(parent)
, _parentController(parentController.get())
, _reactions(std::move(reactions))
, _reactions(reactions)
, _jumpedToPremium([=] { close(false); })
, _cachedRound(
QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight),
@ -198,14 +198,21 @@ int Selector::countWidth(int desiredWidth, int maxWidth) {
: _reactions.morePremiumAvailable
? Strip::AddedButton::Premium
: Strip::AddedButton::None;
const auto &real = _reactions.recent;
auto list = std::vector<not_null<const Data::Reaction*>>();
list.reserve(_columns);
if (const auto cut = max - _columns) {
_strip.applyList(ranges::make_subrange(
begin(_reactions.recent),
end(_reactions.recent) - (cut + (addedToMax ? 0 : 1))
) | ranges::to_vector, added);
const auto from = begin(real);
const auto till = end(real) - (cut + (addedToMax ? 0 : 1));
for (auto i = from; i != till; ++i) {
list.push_back(&*i);
}
} else {
_strip.applyList(_reactions.recent, added);
for (const auto &reaction : real) {
list.push_back(&reaction);
}
}
_strip.applyList(list, added);
_strip.clearAppearAnimations(false);
return std::max(2 * _skipx + _columns * _size, desiredWidth);
}
@ -583,11 +590,11 @@ void Selector::createList(not_null<Window::SessionController*> controller) {
auto index = 0;
const auto inStrip = _strip.count();
for (const auto &reaction : _reactions.recent) {
if (const auto id = reaction->id.custom()) {
if (const auto id = reaction.id.custom()) {
recent.push_back(id);
} else {
recent.push_back(reaction->selectAnimation->id);
defaultReactionIds.emplace(recent.back(), reaction->id.emoji());
recent.push_back(reaction.selectAnimation->id);
defaultReactionIds.emplace(recent.back(), reaction.id.emoji());
}
if (index + 1 < inStrip) {
_defaultReactionInStripMap.emplace(recent.back(), index++);

View file

@ -40,7 +40,7 @@ public:
Selector(
not_null<QWidget*> parent,
not_null<Window::SessionController*> parentController,
Data::PossibleItemReactions &&reactions,
const Data::PossibleItemReactionsRef &reactions,
IconFactory iconFactory,
Fn<void(bool fast)> close);

View file

@ -1027,14 +1027,14 @@ sendAsButton: SendAsButton {
}
reactionInlinePadding: margins(5px, 2px, 7px, 2px);
reactionInlineSize: 16px;
reactionInlineImage: 28px;
reactionInlineSize: 18px;
reactionInlineImage: 32px;
reactionInlineSkip: 3px;
reactionInlineBetween: 4px;
reactionInlineInBubbleLeft: -3px;
reactionInlineUserpicsPadding: margins(1px, 1px, 1px, 1px);
reactionInlineUserpics: GroupCallUserpics {
size: 18px;
size: 20px;
shift: 7px;
stroke: 1px;
align: align(left);