Implement a nice corner reaction button.

This commit is contained in:
John Preston 2021-12-15 19:25:48 +04:00
parent e148b5ff08
commit 371c9c1bfe
16 changed files with 658 additions and 332 deletions

View file

@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/basic_click_handlers.h" #include "ui/basic_click_handlers.h"
constexpr auto kPeerLinkPeerIdProperty = 0x01; constexpr auto kPeerLinkPeerIdProperty = 0x01;
constexpr auto kReactionIdProperty = 0x02;
namespace Main { namespace Main {
class Session; class Session;

View file

@ -673,11 +673,6 @@ void InnerWidget::elementStartInteraction(not_null<const Element*> view) {
void InnerWidget::elementShowReactions(not_null<const Element*> view) { void InnerWidget::elementShowReactions(not_null<const Element*> view) {
} }
const Data::Reaction *InnerWidget::elementCornerReaction(
not_null<const Element*> view) {
return nullptr;
}
void InnerWidget::saveState(not_null<SectionMemento*> memento) { void InnerWidget::saveState(not_null<SectionMemento*> memento) {
memento->setFilter(std::move(_filter)); memento->setFilter(std::move(_filter));
memento->setAdmins(std::move(_admins)); memento->setAdmins(std::move(_admins));

View file

@ -141,8 +141,6 @@ public:
not_null<const HistoryView::Element*> view) override; not_null<const HistoryView::Element*> view) override;
void elementShowReactions( void elementShowReactions(
not_null<const HistoryView::Element*> view) override; not_null<const HistoryView::Element*> view) override;
const Data::Reaction *elementCornerReaction(
not_null<const HistoryView::Element*> view) override;
~InnerWidget(); ~InnerWidget();

View file

@ -177,8 +177,10 @@ HistoryInner::HistoryInner(
HistoryView::MakePathShiftGradient( HistoryView::MakePathShiftGradient(
controller->chatStyle(), controller->chatStyle(),
[=] { update(); })) [=] { update(); }))
, _reactionsMenus( , _reactionsManager(
std::make_unique<HistoryView::ReactionsMenuManager>(historyWidget)) std::make_unique<HistoryView::Reactions::Manager>(
historyWidget,
[=](QRect updated) { update(updated); }))
, _touchSelectTimer([=] { onTouchSelect(); }) , _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); })
, _scrollDateCheck([this] { scrollDateCheck(); }) , _scrollDateCheck([this] { scrollDateCheck(); })
@ -225,8 +227,8 @@ HistoryInner::HistoryInner(
_controller->emojiInteractions().playStarted(_peer, std::move(emoji)); _controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime()); }, lifetime());
using ChosenReaction = HistoryView::ReactionsMenuManager::Chosen; using ChosenReaction = HistoryView::Reactions::Manager::Chosen;
_reactionsMenus->chosen( _reactionsManager->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) { ) | rpl::start_with_next([=](ChosenReaction reaction) {
if (const auto item = session().data().message(reaction.context)) { if (const auto item = session().data().message(reaction.context)) {
item->addReaction(reaction.emoji); item->addReaction(reaction.emoji);
@ -283,7 +285,7 @@ HistoryInner::HistoryInner(
Data::PeerUpdate::Flag::Reactions) Data::PeerUpdate::Flag::Reactions)
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
_reactions = session().data().reactions().list(_peer); _reactions = session().data().reactions().list(_peer);
repaintItem(App::mousedItem()); _reactionsManager->applyList(_reactions);
}, lifetime()); }, lifetime());
controller->adaptive().chatWideValue( controller->adaptive().chatWideValue(
@ -799,8 +801,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
view = block->messages[iItem].get(); view = block->messages[iItem].get();
item = view->data(); item = view->data();
} }
p.translate(0, -top);
context.translate(0, top); context.translate(0, top);
p.translate(0, -top);
} }
if (htop >= 0) { if (htop >= 0) {
auto iBlock = (_curHistory == _history ? _curBlock : 0); auto iBlock = (_curHistory == _history ? _curBlock : 0);
@ -863,6 +865,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
view = block->messages[iItem].get(); view = block->messages[iItem].get();
item = view->data(); item = view->data();
} }
context.translate(0, top);
p.translate(0, -top); p.translate(0, -top);
if (readTill && _widget->doWeReadServerHistory()) { if (readTill && _widget->doWeReadServerHistory()) {
@ -959,6 +962,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
return true; return true;
}); });
p.setOpacity(1.); p.setOpacity(1.);
_reactionsManager->paintButtons(p, context);
p.translate(0, _historyPaddingTop); p.translate(0, _historyPaddingTop);
_emojiInteractions->paint(p); _emojiInteractions->paint(p);
} }
@ -1533,7 +1539,7 @@ void HistoryInner::mouseActionFinish(
.sessionWindow = base::make_weak(_controller.get()), .sessionWindow = base::make_weak(_controller.get()),
}) })
}); });
_reactionsMenus->hideAll(anim::type::normal); _reactionsManager->hideSelectors(anim::type::normal);
return; return;
} }
if ((_mouseAction == MouseAction::PrepareSelect) if ((_mouseAction == MouseAction::PrepareSelect)
@ -1719,21 +1725,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
return; return;
} }
const auto itemId = item->fullId(); const auto itemId = item->fullId();
if (item->canReact()) {
auto reactionMenu = std::make_unique<Ui::PopupMenu>(
this,
st::reactionMenu);
auto &reactions = item->history()->owner().reactions();
const auto &list = reactions.list(item->history()->peer);
if (!list.empty()) {
for (const auto &entry : list) {
reactionMenu->addAction(entry.emoji, [=] {
item->addReaction(entry.emoji);
});
}
_menu->addAction("Reaction", std::move(reactionMenu), &st::menuIconReactions);
}
}
if (canSendMessages) { if (canSendMessages) {
_menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] {
_widget->replyToMessage(itemId); _widget->replyToMessage(itemId);
@ -2149,19 +2140,6 @@ void HistoryInner::copySelectedText() {
} }
} }
void HistoryInner::showReactionsMenu(FullMsgId itemId, QRect area) {
const auto top = itemTop(session().data().message(itemId));
if (top < 0) {
area = QRect(); // Just hide.
}
const auto skip = st::reactionCornerOut.y();
area = area.marginsRemoved({ 0, skip, 0, skip });
_reactionsMenus->showReactionsMenu(
itemId,
{ mapToGlobal(area.translated(0, top).topLeft()), area.size() },
_reactions);
}
void HistoryInner::savePhotoToFile(not_null<PhotoData*> photo) { void HistoryInner::savePhotoToFile(not_null<PhotoData*> photo) {
const auto media = photo->activeMediaView(); const auto media = photo->activeMediaView();
if (photo->isNull() || !media || !media->loaded()) { if (photo->isNull() || !media || !media->loaded()) {
@ -2696,6 +2674,7 @@ void HistoryInner::enterEventHook(QEnterEvent *e) {
} }
void HistoryInner::leaveEventHook(QEvent *e) { void HistoryInner::leaveEventHook(QEvent *e) {
_reactionsManager->showButton({});
if (auto item = App::hoveredItem()) { if (auto item = App::hoveredItem()) {
repaintItem(item); repaintItem(item);
App::hoveredItem(nullptr); App::hoveredItem(nullptr);
@ -2807,13 +2786,13 @@ bool HistoryInner::canCopySelected() const {
} }
bool HistoryInner::canDeleteSelected() const { bool HistoryInner::canDeleteSelected() const {
auto selectedState = getSelectionState(); const auto selectedState = getSelectionState();
return (selectedState.count > 0) && (selectedState.count == selectedState.canDeleteCount); return (selectedState.count > 0)
&& (selectedState.count == selectedState.canDeleteCount);
} }
bool HistoryInner::inSelectionMode() const { bool HistoryInner::inSelectionMode() const {
if (!_selected.empty() if (hasSelectedItems()) {
&& (_selected.begin()->second == FullSelection)) {
return true; return true;
} else if (_mouseAction == MouseAction::Selecting } else if (_mouseAction == MouseAction::Selecting
&& _dragSelFrom && _dragSelFrom
@ -2921,13 +2900,6 @@ void HistoryInner::elementShowReactions(not_null<const Element*> view) {
view->data())); view->data()));
} }
const Data::Reaction *HistoryInner::elementCornerReaction(
not_null<const Element*> view) {
return (view == App::mousedItem() && !_reactions.empty())
? &_reactions.front()
: nullptr;
}
auto HistoryInner::getSelectionState() const auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState { -> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {}; auto result = HistoryView::TopBarWidget::SelectedState {};
@ -2955,10 +2927,14 @@ void HistoryInner::clearSelected(bool onlyTextSelection) {
} }
} }
bool HistoryInner::hasSelectedItems() const {
return !_selected.empty() && _selected.cbegin()->second == FullSelection;
}
MessageIdsList HistoryInner::getSelectedItems() const { MessageIdsList HistoryInner::getSelectedItems() const {
using namespace ranges; using namespace ranges;
if (_selected.empty() || _selected.cbegin()->second != FullSelection) { if (!hasSelectedItems()) {
return {}; return {};
} }
@ -2985,6 +2961,21 @@ void HistoryInner::onTouchSelect() {
mouseActionStart(_touchPos, Qt::LeftButton); mouseActionStart(_touchPos, Qt::LeftButton);
} }
auto HistoryInner::reactionButtonParameters(
not_null<const Element*> view,
QPoint position) const
-> HistoryView::Reactions::ButtonParameters {
const auto top = itemTop(view);
if (top < 0
|| !view->data()->canReact()
|| _mouseAction == MouseAction::Dragging
|| inSelectionMode()) {
return {};
}
const auto local = view->reactionButtonParameters(position);
return local.translated({ 0, itemTop(view) });
}
void HistoryInner::mouseActionUpdate() { void HistoryInner::mouseActionUpdate() {
if (hasPendingResizedItems()) { if (hasPendingResizedItems()) {
return; return;
@ -3015,6 +3006,7 @@ void HistoryInner::mouseActionUpdate() {
} }
} }
m = mapPointToItem(point, view); m = mapPointToItem(point, view);
_reactionsManager->showButton(reactionButtonParameters(view, m));
if (view->pointState(m) != PointState::Outside) { if (view->pointState(m) != PointState::Outside) {
if (App::hoveredItem() != view) { if (App::hoveredItem() != view) {
repaintItem(App::hoveredItem()); repaintItem(App::hoveredItem());
@ -3025,6 +3017,8 @@ void HistoryInner::mouseActionUpdate() {
repaintItem(App::hoveredItem()); repaintItem(App::hoveredItem());
App::hoveredItem(nullptr); App::hoveredItem(nullptr);
} }
} else {
_reactionsManager->showButton({});
} }
if (_mouseActionItem && !_mouseActionItem->mainView()) { if (_mouseActionItem && !_mouseActionItem->mainView()) {
mouseActionCancel(); mouseActionCancel();
@ -3148,7 +3142,6 @@ void HistoryInner::mouseActionUpdate() {
|| dragState.customTooltip) { || dragState.customTooltip) {
Ui::Tooltip::Show(1000, this); Ui::Tooltip::Show(1000, this);
} }
showReactionsMenu(dragState.itemId, dragState.reactionArea);
Qt::CursorShape cur = style::cur_default; Qt::CursorShape cur = style::cur_default;
if (_mouseAction == MouseAction::None) { if (_mouseAction == MouseAction::None) {
@ -3852,12 +3845,6 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
Instance->elementShowReactions(view); Instance->elementShowReactions(view);
} }
} }
const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) override {
Expects(Instance != nullptr);
return Instance->elementCornerReaction(view);
}
}; };

View file

@ -29,9 +29,13 @@ enum class CursorState : char;
enum class PointState : char; enum class PointState : char;
class EmptyPainter; class EmptyPainter;
class Element; class Element;
class ReactionsMenuManager;
} // namespace HistoryView } // namespace HistoryView
namespace HistoryView::Reactions {
class Manager;
struct ButtonParameters;
} // namespace HistoryView::Reactions
namespace Window { namespace Window {
class SessionController; class SessionController;
} // namespace Window } // namespace Window
@ -69,7 +73,7 @@ public:
void messagesReceived(PeerData *peer, const QVector<MTPMessage> &messages); void messagesReceived(PeerData *peer, const QVector<MTPMessage> &messages);
void messagesReceivedDown(PeerData *peer, const QVector<MTPMessage> &messages); void messagesReceivedDown(PeerData *peer, const QVector<MTPMessage> &messages);
TextForMimeData getSelectedText() const; [[nodiscard]] TextForMimeData getSelectedText() const;
void touchScrollUpdated(const QPoint &screenPos); void touchScrollUpdated(const QPoint &screenPos);
@ -82,14 +86,16 @@ public:
void repaintItem(const HistoryItem *item); void repaintItem(const HistoryItem *item);
void repaintItem(const Element *view); void repaintItem(const Element *view);
bool canCopySelected() const; [[nodiscard]] bool canCopySelected() const;
bool canDeleteSelected() const; [[nodiscard]] bool canDeleteSelected() const;
HistoryView::TopBarWidget::SelectedState getSelectionState() const; [[nodiscard]] auto getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState;
void clearSelected(bool onlyTextSelection = false); void clearSelected(bool onlyTextSelection = false);
MessageIdsList getSelectedItems() const; [[nodiscard]] MessageIdsList getSelectedItems() const;
bool inSelectionMode() const; [[nodiscard]] bool hasSelectedItems() const;
bool elementIntersectsRange( [[nodiscard]] bool inSelectionMode() const;
[[nodiscard]] bool elementIntersectsRange(
not_null<const Element*> view, not_null<const Element*> view,
int from, int from,
int till) const; int till) const;
@ -120,7 +126,6 @@ public:
void elementReplyTo(const FullMsgId &to); void elementReplyTo(const FullMsgId &to);
void elementStartInteraction(not_null<const Element*> view); void elementStartInteraction(not_null<const Element*> view);
void elementShowReactions(not_null<const Element*> view); void elementShowReactions(not_null<const Element*> view);
const Data::Reaction *elementCornerReaction(not_null<const Element*> view);
void updateBotInfo(bool recount = true); void updateBotInfo(bool recount = true);
@ -343,7 +348,10 @@ private:
void blockSenderItem(FullMsgId itemId); void blockSenderItem(FullMsgId itemId);
void blockSenderAsGroup(FullMsgId itemId); void blockSenderAsGroup(FullMsgId itemId);
void copySelectedText(); void copySelectedText();
void showReactionsMenu(FullMsgId itemId, QRect area);
HistoryView::Reactions::ButtonParameters reactionButtonParameters(
not_null<const Element*> view,
QPoint position) const;
void setupSharingDisallowed(); void setupSharingDisallowed();
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;
@ -398,7 +406,7 @@ private:
std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache; std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
std::vector<Data::Reaction> _reactions; std::vector<Data::Reaction> _reactions;
std::unique_ptr<HistoryView::ReactionsMenuManager> _reactionsMenus; std::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;
MouseAction _mouseAction = MouseAction::None; MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters; TextSelectType _mouseSelectType = TextSelectType::Letters;

View file

@ -53,7 +53,6 @@ struct TextState {
bool customTooltip = false; bool customTooltip = false;
uint16 symbol = 0; uint16 symbol = 0;
QString customTooltipText; QString customTooltipText;
QRect reactionArea;
}; };

View file

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_media_grouped.h"
#include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_large_emoji.h" #include "history/view/media/history_view_large_emoji.h"
#include "history/view/history_view_reactions.h"
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/history.h" #include "history/history.h"
#include "base/unixtime.h" #include "base/unixtime.h"
@ -188,11 +189,6 @@ void SimpleElementDelegate::elementShowReactions(
not_null<const Element*> view) { not_null<const Element*> view) {
} }
const Data::Reaction *SimpleElementDelegate::elementCornerReaction(
not_null<const Element*> view) {
return nullptr;
}
TextSelection UnshiftItemSelection( TextSelection UnshiftItemSelection(
TextSelection selection, TextSelection selection,
uint16 byLength) { uint16 byLength) {
@ -979,6 +975,11 @@ TextSelection Element::adjustSelection(
return selection; return selection;
} }
Reactions::ButtonParameters Element::reactionButtonParameters(
QPoint position) const {
return {};
}
void Element::clickHandlerActiveChanged( void Element::clickHandlerActiveChanged(
const ClickHandlerPtr &handler, const ClickHandlerPtr &handler,
bool active) { bool active) {

View file

@ -43,6 +43,10 @@ class Media;
using PaintContext = Ui::ChatPaintContext; using PaintContext = Ui::ChatPaintContext;
namespace Reactions {
struct ButtonParameters;
} // namespace Reactions
enum class Context : char { enum class Context : char {
History, History,
Replies, Replies,
@ -96,8 +100,6 @@ public:
virtual void elementReplyTo(const FullMsgId &to) = 0; virtual void elementReplyTo(const FullMsgId &to) = 0;
virtual void elementStartInteraction(not_null<const Element*> view) = 0; virtual void elementStartInteraction(not_null<const Element*> view) = 0;
virtual void elementShowReactions(not_null<const Element*> view) = 0; virtual void elementShowReactions(not_null<const Element*> view) = 0;
virtual const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) = 0;
virtual ~ElementDelegate() { virtual ~ElementDelegate() {
} }
@ -156,8 +158,6 @@ public:
void elementReplyTo(const FullMsgId &to) override; void elementReplyTo(const FullMsgId &to) override;
void elementStartInteraction(not_null<const Element*> view) override; void elementStartInteraction(not_null<const Element*> view) override;
void elementShowReactions(not_null<const Element*> view) override; void elementShowReactions(not_null<const Element*> view) override;
const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) override;
protected: protected:
[[nodiscard]] not_null<Window::SessionController*> controller() const { [[nodiscard]] not_null<Window::SessionController*> controller() const {
@ -319,6 +319,9 @@ public:
TextSelection selection, TextSelection selection,
TextSelectType type) const; TextSelectType type) const;
[[nodiscard]] virtual auto reactionButtonParameters(
QPoint position) const -> Reactions::ButtonParameters;
// ClickHandlerHost interface. // ClickHandlerHost interface.
void clickHandlerActiveChanged( void clickHandlerActiveChanged(
const ClickHandlerPtr &handler, const ClickHandlerPtr &handler,

View file

@ -1461,11 +1461,6 @@ void ListWidget::elementStartInteraction(not_null<const Element*> view) {
void ListWidget::elementShowReactions(not_null<const Element*> view) { void ListWidget::elementShowReactions(not_null<const Element*> view) {
} }
const Data::Reaction *ListWidget::elementCornerReaction(
not_null<const Element*> view) {
return nullptr; // #TODO reactions
}
void ListWidget::saveState(not_null<ListMemento*> memento) { void ListWidget::saveState(not_null<ListMemento*> memento) {
memento->setAroundPosition(_aroundPosition); memento->setAroundPosition(_aroundPosition);
auto state = countScrollState(); auto state = countScrollState();

View file

@ -279,8 +279,6 @@ public:
void elementReplyTo(const FullMsgId &to) override; void elementReplyTo(const FullMsgId &to) override;
void elementStartInteraction(not_null<const Element*> view) override; void elementStartInteraction(not_null<const Element*> view) override;
void elementShowReactions(not_null<const Element*> view) override; void elementShowReactions(not_null<const Element*> view) override;
const Data::Reaction *elementCornerReaction(
not_null<const Element*> view) override;
void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w); void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w);

View file

@ -605,28 +605,6 @@ void Message::draw(Painter &p, const PaintContext &context) const {
p.translate(-reactionsPosition); p.translate(-reactionsPosition);
} }
if (const auto reaction = delegate()->elementCornerReaction(this)) {
if (!_react) {
_react = std::make_unique<ReactButton>([=] {
history()->owner().requestViewRepaint(this);
}, [=] {
if (const auto reaction
= delegate()->elementCornerReaction(this)) {
data()->addReaction(reaction->emoji);
}
}, g);
_react->toggle(true);
} else {
_react->updateGeometry(g);
}
_react->show(reaction);
} else if (_react) {
_react->toggle(false);
if (_react->isHidden()) {
_react = nullptr;
}
}
if (bubble) { if (bubble) {
if (displayFromName() if (displayFromName()
&& item->displayFrom() && item->displayFrom()
@ -767,10 +745,6 @@ void Message::draw(Painter &p, const PaintContext &context) const {
drawRightAction(p, context, fastShareLeft, fastShareTop, width()); drawRightAction(p, context, fastShareLeft, fastShareTop, width());
} }
if (_react) {
_react->paint(p, context);
}
if (media) { if (media) {
media->paintBubbleFireworks(p, g, context.now); media->paintBubbleFireworks(p, g, context.now);
} }
@ -1097,12 +1071,6 @@ PointState Message::pointState(QPoint point) const {
return PointState::Outside; return PointState::Outside;
} }
if (_react) {
if (const auto state = _react->pointState(point)) {
return *state;
}
}
const auto media = this->media(); const auto media = this->media();
const auto item = message(); const auto item = message();
const auto reactionsInBubble = _reactions && needInfoDisplay(); const auto reactionsInBubble = _reactions && needInfoDisplay();
@ -1279,14 +1247,6 @@ TextState Message::textState(
return result; return result;
} }
if (_react) {
if (const auto state = _react->textState(point, request)) {
result.link = state->link;
result.reactionArea = state->reactionArea;
return result;
}
}
const auto reactionsInBubble = _reactions && needInfoDisplay(); const auto reactionsInBubble = _reactions && needInfoDisplay();
auto keyboard = item->inlineReplyKeyboard(); auto keyboard = item->inlineReplyKeyboard();
auto keyboardHeight = 0; auto keyboardHeight = 0;
@ -1828,6 +1788,28 @@ TextSelection Message::adjustSelection(
return result; return result;
} }
Reactions::ButtonParameters Message::reactionButtonParameters(
QPoint position) const {
const auto top = marginTop();
if (!QRect(0, top, width(), height() - top).contains(position)) {
return {};
}
auto result = Reactions::ButtonParameters{ .context = data()->fullId() };
result.outbg = hasOutLayout();
const auto geometry = countGeometry();
result.center = geometry.topLeft()
+ QPoint(geometry.width(), geometry.height())
+ st::reactionCornerCenter;
const auto size = st::reactionCornerSize;
const auto button = QRect(
result.center - QPoint(size.width() / 2, size.height() / 2),
size);
result.active = button.marginsAdded(
st::reactionCornerActiveAreaPadding
).contains(position);
return result;
}
void Message::drawInfo( void Message::drawInfo(
Painter &p, Painter &p,
const PaintContext &context, const PaintContext &context,
@ -1932,11 +1914,14 @@ void Message::refreshReactions() {
const auto &list = item->reactions(); const auto &list = item->reactions();
if (list.empty() || embedReactionsInBottomInfo()) { if (list.empty() || embedReactionsInBottomInfo()) {
_reactions = nullptr; _reactions = nullptr;
} else if (!_reactions) { return;
_reactions = std::make_unique<Reactions>( }
ReactionsDataFromMessage(this)); using namespace Reactions;
auto data = InlineListDataFromMessage(this);
if (!_reactions) {
_reactions = std::make_unique<InlineList>(std::move(data));
} else { } else {
_reactions->update(ReactionsDataFromMessage(this), width()); _reactions->update(std::move(data), width());
} }
} }
@ -1964,11 +1949,9 @@ void Message::itemDataChanged() {
auto Message::verticalRepaintRange() const -> VerticalRepaintRange { auto Message::verticalRepaintRange() const -> VerticalRepaintRange {
const auto media = this->media(); const auto media = this->media();
const auto add = media ? media->bubbleRollRepaintMargins() : QMargins(); const auto add = media ? media->bubbleRollRepaintMargins() : QMargins();
const auto addBottom = add.bottom()
+ (_react ? std::max(_react->bottomOutsideMargin(height()), 0) : 0);
return { return {
.top = -add.top(), .top = -add.top(),
.height = height() + add.top() + addBottom .height = height() + add.top() + add.bottom()
}; };
} }

View file

@ -19,10 +19,12 @@ struct HistoryMessageForwarded;
namespace HistoryView { namespace HistoryView {
class ViewButton; class ViewButton;
class ReactButton;
class Reactions;
class WebPage; class WebPage;
namespace Reactions {
class InlineList;
} // namespace Reactions
// Special type of Component for the channel actions log. // Special type of Component for the channel actions log.
struct LogEntryOriginal struct LogEntryOriginal
: public RuntimeComponent<LogEntryOriginal, Element> { : public RuntimeComponent<LogEntryOriginal, Element> {
@ -85,6 +87,9 @@ public:
TextSelection selection, TextSelection selection,
TextSelectType type) const override; TextSelectType type) const override;
Reactions::ButtonParameters reactionButtonParameters(
QPoint position) const override;
bool hasHeavyPart() const override; bool hasHeavyPart() const override;
void unloadHeavyPart() override; void unloadHeavyPart() override;
@ -234,8 +239,7 @@ private:
mutable ClickHandlerPtr _rightActionLink; mutable ClickHandlerPtr _rightActionLink;
mutable ClickHandlerPtr _fastReplyLink; mutable ClickHandlerPtr _fastReplyLink;
mutable std::unique_ptr<ViewButton> _viewButton; mutable std::unique_ptr<ViewButton> _viewButton;
mutable std::unique_ptr<ReactButton> _react; std::unique_ptr<Reactions::InlineList> _reactions;
std::unique_ptr<Reactions> _reactions;
mutable std::unique_ptr<CommentsButton> _comments; mutable std::unique_ptr<CommentsButton> _comments;
Ui::Text::String _rightBadge; Ui::Text::String _rightBadge;

View file

@ -11,8 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/history_message.h" #include "history/history_message.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/chat/message_bubble.h"
#include "ui/text/text_options.h" #include "ui/text/text_options.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/image/image_prepare.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
@ -21,20 +23,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/palette.h" #include "styles/palette.h"
namespace HistoryView { namespace HistoryView::Reactions {
namespace { namespace {
constexpr auto kItemsPerRow = 5; constexpr auto kItemsPerRow = 5;
constexpr auto kToggleDuration = crl::time(80);
constexpr auto kActivateDuration = crl::time(150);
constexpr auto kInCacheIndex = 0;
constexpr auto kOutCacheIndex = 1;
constexpr auto kShadowCacheIndex = 0;
constexpr auto kEmojiCacheIndex = 1;
constexpr auto kMaskCacheIndex = 2;
constexpr auto kCacheColumsCount = 3;
[[nodiscard]] QSize CountOuterSize() {
const auto extended = QRect(
QPoint(),
st::reactionCornerSize
).marginsAdded(st::reactionCornerShadow);
const auto scale = Button::ScaleForState(ButtonState::Active);
return QSize(
int(base::SafeRound(extended.width() * scale)),
int(base::SafeRound(extended.height() * scale)));
}
} // namespace } // namespace
Reactions::Reactions(Data &&data) InlineList::InlineList(Data &&data)
: _data(std::move(data)) : _data(std::move(data))
, _reactions(st::msgMinWidth / 2) { , _reactions(st::msgMinWidth / 2) {
layout(); layout();
} }
void Reactions::update(Data &&data, int availableWidth) { void InlineList::update(Data &&data, int availableWidth) {
_data = std::move(data); _data = std::move(data);
layout(); layout();
if (width() > 0) { if (width() > 0) {
@ -42,20 +63,20 @@ void Reactions::update(Data &&data, int availableWidth) {
} }
} }
void Reactions::updateSkipBlock(int width, int height) { void InlineList::updateSkipBlock(int width, int height) {
_reactions.updateSkipBlock(width, height); _reactions.updateSkipBlock(width, height);
} }
void Reactions::removeSkipBlock() { void InlineList::removeSkipBlock() {
_reactions.removeSkipBlock(); _reactions.removeSkipBlock();
} }
void Reactions::layout() { void InlineList::layout() {
layoutReactionsText(); layoutReactionsText();
initDimensions(); initDimensions();
} }
void Reactions::layoutReactionsText() { void InlineList::layoutReactionsText() {
if (_data.reactions.empty()) { if (_data.reactions.empty()) {
_reactions.clear(); _reactions.clear();
return; return;
@ -87,18 +108,18 @@ void Reactions::layoutReactionsText() {
Ui::NameTextOptions()); Ui::NameTextOptions());
} }
QSize Reactions::countOptimalSize() { QSize InlineList::countOptimalSize() {
return QSize(_reactions.maxWidth(), _reactions.minHeight()); return QSize(_reactions.maxWidth(), _reactions.minHeight());
} }
QSize Reactions::countCurrentSize(int newWidth) { QSize InlineList::countCurrentSize(int newWidth) {
if (newWidth >= maxWidth()) { if (newWidth >= maxWidth()) {
return optimalSize(); return optimalSize();
} }
return { newWidth, _reactions.countHeight(newWidth) }; return { newWidth, _reactions.countHeight(newWidth) };
} }
void Reactions::paint( void InlineList::paint(
Painter &p, Painter &p,
const Ui::ChatStyle *st, const Ui::ChatStyle *st,
int outerWidth, int outerWidth,
@ -106,8 +127,8 @@ void Reactions::paint(
_reactions.draw(p, 0, 0, outerWidth); _reactions.draw(p, 0, 0, outerWidth);
} }
Reactions::Data ReactionsDataFromMessage(not_null<Message*> message) { InlineListData InlineListDataFromMessage(not_null<Message*> message) {
auto result = Reactions::Data(); auto result = InlineListData();
const auto item = message->message(); const auto item = message->message();
result.reactions = item->reactions(); result.reactions = item->reactions();
@ -115,111 +136,84 @@ Reactions::Data ReactionsDataFromMessage(not_null<Message*> message) {
return result; return result;
} }
ReactButton::ReactButton( Button::Button(
Fn<void()> update, Fn<void(QRect)> update,
Fn<void()> react, ButtonParameters parameters)
QRect bubble) : _update(std::move(update)) {
: _update(std::move(update)) _geometry = QRect(QPoint(), CountOuterSize());
, _handler(std::make_shared<LambdaClickHandler>(react)) { _outbg = parameters.outbg;
updateGeometry(bubble);
} }
void ReactButton::updateGeometry(QRect bubble) { Button::~Button() = default;
const auto topLeft = bubble.topLeft()
+ QPoint(bubble.width(), bubble.height()) bool Button::outbg() const {
+ QPoint(st::reactionCornerOut.x(), st::reactionCornerOut.y()) return _outbg;
- QPoint(
st::reactionCornerSize.width(),
st::reactionCornerSize.height());
_geometry = QRect(topLeft, st::reactionCornerSize);
_imagePosition = _geometry.topLeft() + QPoint(
(_geometry.width() - st::reactionCornerImage) / 2,
(_geometry.height() - st::reactionCornerImage) / 2);
} }
int ReactButton::bottomOutsideMargin(int fullHeight) const { bool Button::isHidden() const {
return _geometry.y() + _geometry.height() - fullHeight; return (_state == State::Hidden) && !_scaleAnimation.animating();
} }
std::optional<PointState> ReactButton::pointState(QPoint point) const { QRect Button::geometry() const {
if (!_geometry.contains(point)) { return _geometry;
return std::nullopt; }
void Button::applyParameters(ButtonParameters parameters) {
const auto size = _geometry.size();
const auto geometry = QRect(
parameters.center - QPoint(size.width(), size.height()) / 2,
size);
if (_outbg != parameters.outbg) {
_outbg = parameters.outbg;
_update(_geometry);
} }
return PointState::Inside; if (_geometry != geometry) {
} if (!_geometry.isNull()) {
_update(_geometry);
std::optional<TextState> ReactButton::textState( }
QPoint point, _geometry = geometry;
const StateRequest &request) const { _update(_geometry);
if (!_geometry.contains(point)) {
return std::nullopt;
} }
auto result = TextState(nullptr, _handler); applyState(parameters.active ? State::Active : State::Shown);
result.reactionArea = _geometry;
return result;
} }
void ReactButton::paint(Painter &p, const PaintContext &context) { void Button::applyState(State state) {
const auto shown = _shownAnimation.value(_shown ? 1. : 0.); if (_state == state) {
if (shown == 0.) {
return; return;
} }
p.setOpacity(shown); const auto duration = (state == State::Hidden
p.setBrush(context.messageStyle()->msgBg); || _state == State::Hidden)
p.setPen(st::shadowFg); ? kToggleDuration
const auto radius = _geometry.height() / 2; : kActivateDuration;
p.drawRoundedRect(_geometry, radius, radius); _scaleAnimation.start(
if (!_image.isNull()) { [=] { _update(_geometry); },
p.drawImage(_imagePosition, _image); ScaleForState(_state),
} ScaleForState(state),
p.setOpacity(1.); duration);
_state = state;
} }
void ReactButton::toggle(bool shown) { float64 Button::ScaleForState(State state) {
if (_shown == shown) { switch (state) {
return; case State::Hidden: return 0.7;
case State::Shown: return 1.;
case State::Active: return 1.4;
} }
_shown = shown; Unexpected("State in ReactionButton::ScaleForState.");
_shownAnimation.start(_update, _shown ? 0. : 1., _shown ? 1. : 0., 120);
} }
bool ReactButton::isHidden() const { float64 Button::OpacityForScale(float64 scale) {
return !_shown && !_shownAnimation.animating(); return (scale >= 1.)
? 1.
: ((scale - ScaleForState(State::Hidden))
/ (ScaleForState(State::Shown) - ScaleForState(State::Hidden)));
} }
void ReactButton::show(not_null<const Data::Reaction*> reaction) { float64 Button::currentScale() const {
if (_media && _media->owner() == reaction->staticIcon) { return _scaleAnimation.value(ScaleForState(_state));
return;
}
_handler->setProperty(kReactionIdProperty, reaction->emoji);
_media = reaction->staticIcon->createMediaView();
const auto setImage = [=](not_null<Image*> image) {
const auto size = st::reactionCornerImage;
_image = Images::prepare(
image->original(),
size * style::DevicePixelRatio(),
size * style::DevicePixelRatio(),
Images::Option::Smooth | Images::Option::TransparentBackground,
size,
size);
_image.setDevicePixelRatio(style::DevicePixelRatio());
};
if (const auto image = _media->getStickerLarge()) {
setImage(image);
} else {
reaction->staticIcon->session().downloaderTaskFinished(
) | rpl::map([=] {
return _media->getStickerLarge();
}) | rpl::filter_nullptr() | rpl::take(
1
) | rpl::start_with_next([=](not_null<Image*> image) {
setImage(image);
_update();
}, _downloadTaskLifetime);
}
} }
ReactionsMenu::ReactionsMenu( Selector::Selector(
QWidget *parent, QWidget *parent,
const std::vector<Data::Reaction> &list) const std::vector<Data::Reaction> &list)
: _dropdown(parent) { : _dropdown(parent) {
@ -331,7 +325,7 @@ ReactionsMenu::ReactionsMenu(
_dropdown.resizeToContent(); _dropdown.resizeToContent();
} }
void ReactionsMenu::showAround(QRect area) { void Selector::showAround(QRect area) {
const auto parent = _dropdown.parentWidget(); const auto parent = _dropdown.parentWidget();
const auto left = std::min( const auto left = std::min(
std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0), std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0),
@ -345,7 +339,7 @@ void ReactionsMenu::showAround(QRect area) {
_dropdown.move(left, top); _dropdown.move(left, top);
} }
void ReactionsMenu::toggle(bool shown, anim::type animated) { void Selector::toggle(bool shown, anim::type animated) {
if (animated == anim::type::normal) { if (animated == anim::type::normal) {
if (shown) { if (shown) {
using Origin = Ui::PanelAnimation::Origin; using Origin = Ui::PanelAnimation::Origin;
@ -362,65 +356,351 @@ void ReactionsMenu::toggle(bool shown, anim::type animated) {
} }
} }
[[nodiscard]] rpl::producer<QString> ReactionsMenu::chosen() const { [[nodiscard]] rpl::producer<QString> Selector::chosen() const {
return _chosen.events(); return _chosen.events();
} }
[[nodiscard]] rpl::lifetime &ReactionsMenu::lifetime() { [[nodiscard]] rpl::lifetime &Selector::lifetime() {
return _dropdown.lifetime(); return _dropdown.lifetime();
} }
ReactionsMenuManager::ReactionsMenuManager(QWidget *parent) Manager::Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate)
: _parent(parent) { : _outer(CountOuterSize())
, _inner(QRectF(
(_outer.width() - st::reactionCornerSize.width()) / 2.,
(_outer.height() - st::reactionCornerSize.height()) / 2.,
st::reactionCornerSize.width(),
st::reactionCornerSize.height()))
, _buttonUpdate(std::move(buttonUpdate))
, _selectorParent(selectorParent) {
const auto ratio = style::DevicePixelRatio();
_cacheInOut = QImage(
_outer.width() * 2 * ratio,
_outer.height() * kFramesCount * ratio,
QImage::Format_ARGB32_Premultiplied);
_cacheInOut.setDevicePixelRatio(ratio);
_cacheInOut.fill(Qt::transparent);
_cacheParts = QImage(
_outer.width() * kCacheColumsCount * ratio,
_outer.height() * kFramesCount * ratio,
QImage::Format_ARGB32_Premultiplied);
_cacheParts.setDevicePixelRatio(ratio);
_cacheParts.fill(Qt::transparent);
_shadowBuffer = QImage(
_outer * ratio,
QImage::Format_ARGB32_Premultiplied);
} }
ReactionsMenuManager::~ReactionsMenuManager() = default; Manager::~Manager() = default;
void ReactionsMenuManager::showReactionsMenu( void Manager::showButton(ButtonParameters parameters) {
FullMsgId context, if (_button && _buttonContext != parameters.context) {
QRect globalReactionArea, _button->applyState(ButtonState::Hidden);
const std::vector<Data::Reaction> &list) { _buttonHiding.push_back(std::move(_button));
if (globalReactionArea.isEmpty()) { }
_buttonContext = parameters.context;
if (!_buttonContext || _list.size() < 2) {
return;
}
if (!_button) {
_button = std::make_unique<Button>(_buttonUpdate, parameters);
} else {
_button->applyParameters(parameters);
}
}
void Manager::applyList(std::vector<Data::Reaction> list) {
constexpr auto proj = &Data::Reaction::emoji;
if (ranges::equal(_list, list, ranges::equal_to{}, proj, proj)) {
return;
}
_list = std::move(list);
if (_list.size() < 2) {
hideSelectors(anim::type::normal);
}
if (_list.empty()) {
_mainReactionMedia = nullptr;
return;
}
const auto main = _list.front().staticIcon;
if (_mainReactionMedia && _mainReactionMedia->owner() == main) {
return;
}
_mainReactionMedia = main->createMediaView();
if (const auto image = _mainReactionMedia->getStickerLarge()) {
setMainReactionImage(image->original());
} else {
main->session().downloaderTaskFinished(
) | rpl::map([=] {
return _mainReactionMedia->getStickerLarge();
}) | rpl::filter_nullptr() | rpl::take(
1
) | rpl::start_with_next([=](not_null<Image*> image) {
setMainReactionImage(image->original());
}, _mainReactionLifetime);
}
}
void Manager::setMainReactionImage(QImage image) {
_mainReactionImage = std::move(image);
ranges::fill(_validIn, false);
ranges::fill(_validOut, false);
ranges::fill(_validEmoji, false);
}
void Manager::removeStaleButtons() {
_buttonHiding.erase(
ranges::remove_if(_buttonHiding, &Button::isHidden),
end(_buttonHiding));
}
void Manager::paintButtons(Painter &p, const PaintContext &context) {
removeStaleButtons();
for (const auto &button : _buttonHiding) {
paintButton(p, context, button.get());
}
if (const auto current = _button.get()) {
paintButton(p, context, current);
}
}
void Manager::paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button) {
const auto geometry = button->geometry();
if (!context.clip.intersects(geometry)) {
return;
}
const auto scale = button->currentScale();
const auto scaleMin = Button::ScaleForState(ButtonState::Hidden);
const auto scaleMax = Button::ScaleForState(ButtonState::Active);
const auto progress = (scale - scaleMin) / (scaleMax - scaleMin);
const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
const auto useScale = scaleMin
+ (frame / float64(kFramesCount - 1)) * (scaleMax - scaleMin);
paintButton(p, context, button, frame, useScale);
}
void Manager::paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button,
int frameIndex,
float64 scale) {
const auto opacity = Button::OpacityForScale(scale);
if (opacity == 0.) {
return;
}
const auto geometry = button->geometry();
const auto position = geometry.topLeft();
const auto size = geometry.size();
const auto outbg = button->outbg();
const auto patterned = outbg
&& context.bubblesPattern
&& !context.viewport.isEmpty()
&& !context.bubblesPattern->pixmap.size().isEmpty();
const auto shadow = context.st->shadowFg()->c;
if (opacity != 1.) {
p.setOpacity(opacity);
}
if (patterned) {
p.drawImage(
position,
_cacheParts,
validateShadow(frameIndex, scale, shadow));
// #TODO reactions
} else {
const auto &stm = context.st->messageStyle(outbg, false);
const auto background = stm.msgBg->c;
const auto source = validateFrame(
outbg,
frameIndex,
scale,
stm.msgBg->c,
shadow);
p.drawImage(position, _cacheInOut, source);
}
if (opacity != 1.) {
p.setOpacity(1.);
}
}
void Manager::applyPatternedShadow(const QColor &shadow) {
if (_shadow == shadow) {
return;
}
_shadow = shadow;
ranges::fill(_validIn, false);
ranges::fill(_validOut, false);
ranges::fill(_validShadow, false);
}
QRect Manager::cacheRect(int frameIndex, int columnIndex) const {
const auto ratio = style::DevicePixelRatio();
const auto origin = QPoint(
_outer.width() * columnIndex,
_outer.height() * frameIndex);
return QRect(ratio * origin, ratio * _outer);
}
QRect Manager::validateShadow(
int frameIndex,
float64 scale,
const QColor &shadow) {
applyPatternedShadow(shadow);
const auto result = cacheRect(frameIndex, kShadowCacheIndex);
if (_validShadow[frameIndex]) {
return result;
}
_shadowBuffer.fill(Qt::transparent);
auto p = QPainter(&_shadowBuffer);
auto hq = PainterHighQualityEnabler(p);
const auto radius = _inner.height() / 2;
const auto center = _inner.center();
const auto add = style::ConvertScale(1.5);
const auto shift = style::ConvertScale(1.);
const auto extended = _inner.marginsAdded({ add, add, add, add });
p.setPen(Qt::NoPen);
p.setBrush(shadow);
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawRoundedRect(extended.translated(0, shift), radius, radius);
p.end();
_shadowBuffer = Images::prepareBlur(std::move(_shadowBuffer));
auto q = QPainter(&_cacheParts);
q.setCompositionMode(QPainter::CompositionMode_Source);
q.drawImage(result.topLeft() / style::DevicePixelRatio(), _shadowBuffer);
_validShadow[frameIndex] = true;
return result;
}
QRect Manager::validateEmoji(int frameIndex, float64 scale) {
const auto result = cacheRect(frameIndex, kEmojiCacheIndex);
if (_validEmoji[frameIndex]) {
return result;
}
auto p = QPainter(&_cacheParts);
const auto ratio = style::DevicePixelRatio();
const auto position = result.topLeft() / ratio;
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(QRect(position, result.size() / ratio), Qt::transparent);
if (!_mainReactionImage.isNull()) {
const auto size = st::reactionCornerImage * scale;
const auto inner = _inner.translated(position);
const auto target = QRectF(
inner.x() + (inner.width() - size) / 2,
inner.y() + (inner.height() - size) / 2,
size,
size);
auto hq = PainterHighQualityEnabler(p);
p.drawImage(target, _mainReactionImage);
}
_validEmoji[frameIndex] = true;
return result;
}
QRect Manager::validateFrame(
bool outbg,
int frameIndex,
float64 scale,
const QColor &background,
const QColor &shadow) {
applyPatternedShadow(shadow);
auto &valid = outbg ? _validOut : _validIn;
auto &color = outbg ? _backgroundOut : _backgroundIn;
if (color != background) {
color = background;
ranges::fill(valid, false);
}
const auto columnIndex = outbg ? kOutCacheIndex : kInCacheIndex;
const auto result = cacheRect(frameIndex, columnIndex);
if (valid[frameIndex]) {
return result;
}
const auto shadowSource = validateShadow(frameIndex, scale, shadow);
const auto emojiSource = validateEmoji(frameIndex, scale);
const auto position = result.topLeft() / style::DevicePixelRatio();
auto p = QPainter(&_cacheInOut);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.drawImage(position, _cacheParts, shadowSource);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
auto hq = PainterHighQualityEnabler(p);
const auto inner = _inner.translated(position);
const auto radius = inner.height() / 2;
const auto center = inner.center();
p.setPen(Qt::NoPen);
p.setBrush(background);
p.save();
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawRoundedRect(inner, radius, radius);
p.restore();
p.drawImage(position, _cacheParts, emojiSource);
p.end();
valid[frameIndex] = true;
return result;
}
void Manager::showSelector(Fn<QPoint(QPoint)> mapToGlobal) {
if (!_button) {
showSelector({}, {});
} else {
const auto geometry = _button->geometry();
showSelector(
_buttonContext,
{ mapToGlobal(geometry.topLeft()), geometry.size() });
}
}
void Manager::showSelector(FullMsgId context, QRect globalButtonArea) {
if (globalButtonArea.isEmpty()) {
context = FullMsgId(); context = FullMsgId();
} }
const auto listsEqual = ranges::equal( const auto changed = (_selectorContext != context);
_list, if (_selector && changed) {
list, _selector->toggle(false, anim::type::normal);
ranges::equal_to(), _selectorHiding.push_back(std::move(_selector));
&Data::Reaction::emoji,
&Data::Reaction::emoji);
const auto changed = (_context != context || !listsEqual);
if (_menu && changed) {
_menu->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu));
} }
_context = context; _selectorContext = context;
_list = list; if (_list.size() < 2 || !context || (!changed && !_selector)) {
if (list.size() < 2 || !context || (!changed && !_menu)) {
return; return;
} else if (!_menu) { } else if (!_selector) {
_menu = std::make_unique<ReactionsMenu>(_parent, list); _selector = std::make_unique<Selector>(_selectorParent, _list);
_menu->chosen( _selector->chosen(
) | rpl::start_with_next([=](QString emoji) { ) | rpl::start_with_next([=](QString emoji) {
_menu->toggle(false, anim::type::normal); _selector->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu)); _selectorHiding.push_back(std::move(_selector));
_chosen.fire({ context, std::move(emoji) }); _chosen.fire({ context, std::move(emoji) });
}, _menu->lifetime()); }, _selector->lifetime());
} }
const auto area = QRect( const auto area = QRect(
_parent->mapFromGlobal(globalReactionArea.topLeft()), _selectorParent->mapFromGlobal(globalButtonArea.topLeft()),
globalReactionArea.size()); globalButtonArea.size());
_menu->showAround(area); _selector->showAround(area);
_menu->toggle(true, anim::type::normal); _selector->toggle(true, anim::type::normal);
} }
void ReactionsMenuManager::hideAll(anim::type animated) { void Manager::hideSelectors(anim::type animated) {
if (animated == anim::type::instant) { if (animated == anim::type::instant) {
_hiding.clear(); _selectorHiding.clear();
_menu = nullptr; _selector = nullptr;
} else if (_menu) { } else if (_selector) {
_menu->toggle(false, anim::type::normal); _selector->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu)); _selectorHiding.push_back(std::move(_selector));
} }
} }

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/inner_dropdown.h" #include "ui/widgets/inner_dropdown.h"
class Image; class Image;
class Painter;
namespace Ui { namespace Ui {
class ChatStyle; class ChatStyle;
@ -24,21 +25,24 @@ class DocumentMedia;
} // namespace Data } // namespace Data
namespace HistoryView { namespace HistoryView {
using PaintContext = Ui::ChatPaintContext; using PaintContext = Ui::ChatPaintContext;
enum class PointState : char; enum class PointState : char;
struct TextState; struct TextState;
struct StateRequest; struct StateRequest;
class Message; class Message;
} // namespace HistoryView
class Reactions final : public Object { namespace HistoryView::Reactions {
struct InlineListData {
base::flat_map<QString, int> reactions;
QString chosenReaction;
};
class InlineList final : public Object {
public: public:
struct Data { using Data = InlineListData;
base::flat_map<QString, int> reactions; explicit InlineList(Data &&data);
QString chosenReaction;
};
explicit Reactions(Data &&data);
void update(Data &&data, int availableWidth); void update(Data &&data, int availableWidth);
QSize countCurrentSize(int newWidth) override; QSize countCurrentSize(int newWidth) override;
@ -63,43 +67,64 @@ private:
}; };
[[nodiscard]] Reactions::Data ReactionsDataFromMessage( [[nodiscard]] InlineListData InlineListDataFromMessage(
not_null<Message*> message); not_null<Message*> message);
class ReactButton final { enum class ButtonStyle {
Bubble,
};
struct ButtonParameters {
[[nodiscard]] ButtonParameters translated(QPoint delta) const {
auto result = *this;
result.center += delta;
return result;
}
FullMsgId context;
QPoint center;
ButtonStyle style = ButtonStyle::Bubble;
bool active = false;
bool outbg = false;
};
enum class ButtonState {
Hidden,
Shown,
Active,
};
class Button final {
public: public:
ReactButton(Fn<void()> update, Fn<void()> react, QRect bubble); Button(Fn<void(QRect)> update, ButtonParameters parameters);
~Button();
void updateGeometry(QRect bubble); void applyParameters(ButtonParameters parameters);
[[nodiscard]] int bottomOutsideMargin(int fullHeight) const;
[[nodiscard]] std::optional<PointState> pointState(QPoint point) const;
[[nodiscard]] std::optional<TextState> textState(
QPoint point,
const StateRequest &request) const;
void paint(Painter &p, const PaintContext &context); using State = ButtonState;
void applyState(State state);
void toggle(bool shown); [[nodiscard]] bool outbg() const;
[[nodiscard]] bool isHidden() const; [[nodiscard]] bool isHidden() const;
void show(not_null<const Data::Reaction*> reaction); [[nodiscard]] QRect geometry() const;
[[nodiscard]] float64 currentScale() const;
[[nodiscard]] static float64 ScaleForState(State state);
[[nodiscard]] static float64 OpacityForScale(float64 scale);
private: private:
const Fn<void()> _update; const Fn<void(QRect)> _update;
const ClickHandlerPtr _handler; State _state = State::Hidden;
QRect _geometry; Ui::Animations::Simple _scaleAnimation;
bool _shown = false;
Ui::Animations::Simple _shownAnimation;
QImage _image; QRect _geometry;
QPoint _imagePosition; ButtonStyle _style = ButtonStyle::Bubble;
std::shared_ptr<Data::DocumentMedia> _media; bool _outbg = false;
rpl::lifetime _downloadTaskLifetime;
}; };
class ReactionsMenu final { class Selector final {
public: public:
ReactionsMenu( Selector(
QWidget *parent, QWidget *parent,
const std::vector<Data::Reaction> &list); const std::vector<Data::Reaction> &list);
@ -123,34 +148,88 @@ private:
}; };
class ReactionsMenuManager final { class Manager final {
public: public:
explicit ReactionsMenuManager(QWidget *parent); Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate);
~ReactionsMenuManager(); ~Manager();
void applyList(std::vector<Data::Reaction> list);
void showButton(ButtonParameters parameters);
void paintButtons(Painter &p, const PaintContext &context);
void showSelector(Fn<QPoint(QPoint)> mapToGlobal);
void showSelector(FullMsgId context, QRect globalButtonArea);
void hideSelectors(anim::type animated);
struct Chosen { struct Chosen {
FullMsgId context; FullMsgId context;
QString emoji; QString emoji;
}; };
void showReactionsMenu(
FullMsgId context,
QRect globalReactionArea,
const std::vector<Data::Reaction> &list);
void hideAll(anim::type animated);
[[nodiscard]] rpl::producer<Chosen> chosen() const { [[nodiscard]] rpl::producer<Chosen> chosen() const {
return _chosen.events(); return _chosen.events();
} }
private: private:
QWidget *_parent = nullptr; static constexpr auto kFramesCount = 30;
rpl::event_stream<Chosen> _chosen;
std::unique_ptr<ReactionsMenu> _menu; void removeStaleButtons();
FullMsgId _context; void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button);
void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button,
int frame,
float64 scale);
void setMainReactionImage(QImage image);
void applyPatternedShadow(const QColor &shadow);
[[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const;
QRect validateShadow(
int frameIndex,
float64 scale,
const QColor &shadow);
QRect validateEmoji(int frameIndex, float64 scale);
QRect validateFrame(
bool outbg,
int frameIndex,
float64 scale,
const QColor &background,
const QColor &shadow);
rpl::event_stream<Chosen> _chosen;
std::vector<Data::Reaction> _list; std::vector<Data::Reaction> _list;
std::vector<std::unique_ptr<ReactionsMenu>> _hiding; QSize _outer;
QRectF _inner;
QImage _cacheInOut;
QImage _cacheParts;
QImage _shadowBuffer;
std::array<bool, kFramesCount> _validIn;
std::array<bool, kFramesCount> _validOut;
std::array<bool, kFramesCount> _validShadow;
std::array<bool, kFramesCount> _validEmoji;
std::array<bool, kFramesCount> _validMask;
QColor _backgroundIn;
QColor _backgroundOut;
QColor _shadow;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
const Fn<void(QRect)> _buttonUpdate;
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext;
QWidget *_selectorParent = nullptr;
std::unique_ptr<Selector> _selector;
std::vector<std::unique_ptr<Selector>> _selectorHiding;
FullMsgId _selectorContext;
}; };

View file

@ -955,9 +955,11 @@ sendAsButton: SendAsButton {
duration: 150; duration: 150;
} }
reactionCornerSize: size(23px, 18px); reactionCornerSize: size(27px, 19px);
reactionCornerOut: point(7px, 5px); reactionCornerCenter: point(-6px, -5px);
reactionCornerImage: 14px; reactionCornerImage: 15px;
reactionCornerShadow: margins(4px, 4px, 4px, 8px);
reactionCornerActiveAreaPadding: margins(10px, 10px, 10px, 10px);
reactionPopupImage: 25px; reactionPopupImage: 25px;
reactionPopupPadding: margins(5px, 5px, 5px, 5px); reactionPopupPadding: margins(5px, 5px, 5px, 5px);

View file

@ -55,13 +55,8 @@ struct ChatThemeBackgroundData;
namespace Data { namespace Data {
struct CloudTheme; struct CloudTheme;
enum class CloudThemeType; enum class CloudThemeType;
struct Reaction;
} // namespace Data } // namespace Data
namespace HistoryView {
class ReactionsMenu;
} // namespace HistoryView
namespace Window { namespace Window {
class MainWindow; class MainWindow;