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"
constexpr auto kPeerLinkPeerIdProperty = 0x01;
constexpr auto kReactionIdProperty = 0x02;
namespace Main {
class Session;

View file

@ -673,11 +673,6 @@ void InnerWidget::elementStartInteraction(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) {
memento->setFilter(std::move(_filter));
memento->setAdmins(std::move(_admins));

View file

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

View file

@ -177,8 +177,10 @@ HistoryInner::HistoryInner(
HistoryView::MakePathShiftGradient(
controller->chatStyle(),
[=] { update(); }))
, _reactionsMenus(
std::make_unique<HistoryView::ReactionsMenuManager>(historyWidget))
, _reactionsManager(
std::make_unique<HistoryView::Reactions::Manager>(
historyWidget,
[=](QRect updated) { update(updated); }))
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
, _scrollDateCheck([this] { scrollDateCheck(); })
@ -225,8 +227,8 @@ HistoryInner::HistoryInner(
_controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime());
using ChosenReaction = HistoryView::ReactionsMenuManager::Chosen;
_reactionsMenus->chosen(
using ChosenReaction = HistoryView::Reactions::Manager::Chosen;
_reactionsManager->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) {
if (const auto item = session().data().message(reaction.context)) {
item->addReaction(reaction.emoji);
@ -283,7 +285,7 @@ HistoryInner::HistoryInner(
Data::PeerUpdate::Flag::Reactions)
) | rpl::start_with_next([=] {
_reactions = session().data().reactions().list(_peer);
repaintItem(App::mousedItem());
_reactionsManager->applyList(_reactions);
}, lifetime());
controller->adaptive().chatWideValue(
@ -799,8 +801,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
view = block->messages[iItem].get();
item = view->data();
}
p.translate(0, -top);
context.translate(0, top);
p.translate(0, -top);
}
if (htop >= 0) {
auto iBlock = (_curHistory == _history ? _curBlock : 0);
@ -863,6 +865,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
view = block->messages[iItem].get();
item = view->data();
}
context.translate(0, top);
p.translate(0, -top);
if (readTill && _widget->doWeReadServerHistory()) {
@ -959,6 +962,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
return true;
});
p.setOpacity(1.);
_reactionsManager->paintButtons(p, context);
p.translate(0, _historyPaddingTop);
_emojiInteractions->paint(p);
}
@ -1533,7 +1539,7 @@ void HistoryInner::mouseActionFinish(
.sessionWindow = base::make_weak(_controller.get()),
})
});
_reactionsMenus->hideAll(anim::type::normal);
_reactionsManager->hideSelectors(anim::type::normal);
return;
}
if ((_mouseAction == MouseAction::PrepareSelect)
@ -1719,21 +1725,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
return;
}
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) {
_menu->addAction(tr::lng_context_reply_msg(tr::now), [=] {
_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) {
const auto media = photo->activeMediaView();
if (photo->isNull() || !media || !media->loaded()) {
@ -2696,6 +2674,7 @@ void HistoryInner::enterEventHook(QEnterEvent *e) {
}
void HistoryInner::leaveEventHook(QEvent *e) {
_reactionsManager->showButton({});
if (auto item = App::hoveredItem()) {
repaintItem(item);
App::hoveredItem(nullptr);
@ -2807,13 +2786,13 @@ bool HistoryInner::canCopySelected() const {
}
bool HistoryInner::canDeleteSelected() const {
auto selectedState = getSelectionState();
return (selectedState.count > 0) && (selectedState.count == selectedState.canDeleteCount);
const auto selectedState = getSelectionState();
return (selectedState.count > 0)
&& (selectedState.count == selectedState.canDeleteCount);
}
bool HistoryInner::inSelectionMode() const {
if (!_selected.empty()
&& (_selected.begin()->second == FullSelection)) {
if (hasSelectedItems()) {
return true;
} else if (_mouseAction == MouseAction::Selecting
&& _dragSelFrom
@ -2921,13 +2900,6 @@ void HistoryInner::elementShowReactions(not_null<const Element*> view) {
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
-> 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 {
using namespace ranges;
if (_selected.empty() || _selected.cbegin()->second != FullSelection) {
if (!hasSelectedItems()) {
return {};
}
@ -2985,6 +2961,21 @@ void HistoryInner::onTouchSelect() {
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() {
if (hasPendingResizedItems()) {
return;
@ -3015,6 +3006,7 @@ void HistoryInner::mouseActionUpdate() {
}
}
m = mapPointToItem(point, view);
_reactionsManager->showButton(reactionButtonParameters(view, m));
if (view->pointState(m) != PointState::Outside) {
if (App::hoveredItem() != view) {
repaintItem(App::hoveredItem());
@ -3025,6 +3017,8 @@ void HistoryInner::mouseActionUpdate() {
repaintItem(App::hoveredItem());
App::hoveredItem(nullptr);
}
} else {
_reactionsManager->showButton({});
}
if (_mouseActionItem && !_mouseActionItem->mainView()) {
mouseActionCancel();
@ -3148,7 +3142,6 @@ void HistoryInner::mouseActionUpdate() {
|| dragState.customTooltip) {
Ui::Tooltip::Show(1000, this);
}
showReactionsMenu(dragState.itemId, dragState.reactionArea);
Qt::CursorShape cur = style::cur_default;
if (_mouseAction == MouseAction::None) {
@ -3852,12 +3845,6 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
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;
class EmptyPainter;
class Element;
class ReactionsMenuManager;
} // namespace HistoryView
namespace HistoryView::Reactions {
class Manager;
struct ButtonParameters;
} // namespace HistoryView::Reactions
namespace Window {
class SessionController;
} // namespace Window
@ -69,7 +73,7 @@ public:
void messagesReceived(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);
@ -82,14 +86,16 @@ public:
void repaintItem(const HistoryItem *item);
void repaintItem(const Element *view);
bool canCopySelected() const;
bool canDeleteSelected() const;
[[nodiscard]] bool canCopySelected() const;
[[nodiscard]] bool canDeleteSelected() const;
HistoryView::TopBarWidget::SelectedState getSelectionState() const;
[[nodiscard]] auto getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState;
void clearSelected(bool onlyTextSelection = false);
MessageIdsList getSelectedItems() const;
bool inSelectionMode() const;
bool elementIntersectsRange(
[[nodiscard]] MessageIdsList getSelectedItems() const;
[[nodiscard]] bool hasSelectedItems() const;
[[nodiscard]] bool inSelectionMode() const;
[[nodiscard]] bool elementIntersectsRange(
not_null<const Element*> view,
int from,
int till) const;
@ -120,7 +126,6 @@ public:
void elementReplyTo(const FullMsgId &to);
void elementStartInteraction(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);
@ -343,7 +348,10 @@ private:
void blockSenderItem(FullMsgId itemId);
void blockSenderAsGroup(FullMsgId itemId);
void copySelectedText();
void showReactionsMenu(FullMsgId itemId, QRect area);
HistoryView::Reactions::ButtonParameters reactionButtonParameters(
not_null<const Element*> view,
QPoint position) const;
void setupSharingDisallowed();
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;
@ -398,7 +406,7 @@ private:
std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
std::vector<Data::Reaction> _reactions;
std::unique_ptr<HistoryView::ReactionsMenuManager> _reactionsMenus;
std::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;

View file

@ -53,7 +53,6 @@ struct TextState {
bool customTooltip = false;
uint16 symbol = 0;
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_sticker.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/history.h"
#include "base/unixtime.h"
@ -188,11 +189,6 @@ void SimpleElementDelegate::elementShowReactions(
not_null<const Element*> view) {
}
const Data::Reaction *SimpleElementDelegate::elementCornerReaction(
not_null<const Element*> view) {
return nullptr;
}
TextSelection UnshiftItemSelection(
TextSelection selection,
uint16 byLength) {
@ -979,6 +975,11 @@ TextSelection Element::adjustSelection(
return selection;
}
Reactions::ButtonParameters Element::reactionButtonParameters(
QPoint position) const {
return {};
}
void Element::clickHandlerActiveChanged(
const ClickHandlerPtr &handler,
bool active) {

View file

@ -43,6 +43,10 @@ class Media;
using PaintContext = Ui::ChatPaintContext;
namespace Reactions {
struct ButtonParameters;
} // namespace Reactions
enum class Context : char {
History,
Replies,
@ -96,8 +100,6 @@ public:
virtual void elementReplyTo(const FullMsgId &to) = 0;
virtual void elementStartInteraction(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() {
}
@ -156,8 +158,6 @@ public:
void elementReplyTo(const FullMsgId &to) override;
void elementStartInteraction(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:
[[nodiscard]] not_null<Window::SessionController*> controller() const {
@ -319,6 +319,9 @@ public:
TextSelection selection,
TextSelectType type) const;
[[nodiscard]] virtual auto reactionButtonParameters(
QPoint position) const -> Reactions::ButtonParameters;
// ClickHandlerHost interface.
void clickHandlerActiveChanged(
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) {
}
const Data::Reaction *ListWidget::elementCornerReaction(
not_null<const Element*> view) {
return nullptr; // #TODO reactions
}
void ListWidget::saveState(not_null<ListMemento*> memento) {
memento->setAroundPosition(_aroundPosition);
auto state = countScrollState();

View file

@ -279,8 +279,6 @@ public:
void elementReplyTo(const FullMsgId &to) override;
void elementStartInteraction(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);

View file

@ -605,28 +605,6 @@ void Message::draw(Painter &p, const PaintContext &context) const {
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 (displayFromName()
&& item->displayFrom()
@ -767,10 +745,6 @@ void Message::draw(Painter &p, const PaintContext &context) const {
drawRightAction(p, context, fastShareLeft, fastShareTop, width());
}
if (_react) {
_react->paint(p, context);
}
if (media) {
media->paintBubbleFireworks(p, g, context.now);
}
@ -1097,12 +1071,6 @@ PointState Message::pointState(QPoint point) const {
return PointState::Outside;
}
if (_react) {
if (const auto state = _react->pointState(point)) {
return *state;
}
}
const auto media = this->media();
const auto item = message();
const auto reactionsInBubble = _reactions && needInfoDisplay();
@ -1279,14 +1247,6 @@ TextState Message::textState(
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();
auto keyboard = item->inlineReplyKeyboard();
auto keyboardHeight = 0;
@ -1828,6 +1788,28 @@ TextSelection Message::adjustSelection(
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(
Painter &p,
const PaintContext &context,
@ -1932,11 +1914,14 @@ void Message::refreshReactions() {
const auto &list = item->reactions();
if (list.empty() || embedReactionsInBottomInfo()) {
_reactions = nullptr;
} else if (!_reactions) {
_reactions = std::make_unique<Reactions>(
ReactionsDataFromMessage(this));
return;
}
using namespace Reactions;
auto data = InlineListDataFromMessage(this);
if (!_reactions) {
_reactions = std::make_unique<InlineList>(std::move(data));
} else {
_reactions->update(ReactionsDataFromMessage(this), width());
_reactions->update(std::move(data), width());
}
}
@ -1964,11 +1949,9 @@ void Message::itemDataChanged() {
auto Message::verticalRepaintRange() const -> VerticalRepaintRange {
const auto media = this->media();
const auto add = media ? media->bubbleRollRepaintMargins() : QMargins();
const auto addBottom = add.bottom()
+ (_react ? std::max(_react->bottomOutsideMargin(height()), 0) : 0);
return {
.top = -add.top(),
.height = height() + add.top() + addBottom
.height = height() + add.top() + add.bottom()
};
}

View file

@ -19,10 +19,12 @@ struct HistoryMessageForwarded;
namespace HistoryView {
class ViewButton;
class ReactButton;
class Reactions;
class WebPage;
namespace Reactions {
class InlineList;
} // namespace Reactions
// Special type of Component for the channel actions log.
struct LogEntryOriginal
: public RuntimeComponent<LogEntryOriginal, Element> {
@ -85,6 +87,9 @@ public:
TextSelection selection,
TextSelectType type) const override;
Reactions::ButtonParameters reactionButtonParameters(
QPoint position) const override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
@ -234,8 +239,7 @@ private:
mutable ClickHandlerPtr _rightActionLink;
mutable ClickHandlerPtr _fastReplyLink;
mutable std::unique_ptr<ViewButton> _viewButton;
mutable std::unique_ptr<ReactButton> _react;
std::unique_ptr<Reactions> _reactions;
std::unique_ptr<Reactions::InlineList> _reactions;
mutable std::unique_ptr<CommentsButton> _comments;
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/history_message.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/message_bubble.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/image/image_prepare.h"
#include "data/data_message_reactions.h"
#include "data/data_document.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/palette.h"
namespace HistoryView {
namespace HistoryView::Reactions {
namespace {
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
Reactions::Reactions(Data &&data)
InlineList::InlineList(Data &&data)
: _data(std::move(data))
, _reactions(st::msgMinWidth / 2) {
layout();
}
void Reactions::update(Data &&data, int availableWidth) {
void InlineList::update(Data &&data, int availableWidth) {
_data = std::move(data);
layout();
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);
}
void Reactions::removeSkipBlock() {
void InlineList::removeSkipBlock() {
_reactions.removeSkipBlock();
}
void Reactions::layout() {
void InlineList::layout() {
layoutReactionsText();
initDimensions();
}
void Reactions::layoutReactionsText() {
void InlineList::layoutReactionsText() {
if (_data.reactions.empty()) {
_reactions.clear();
return;
@ -87,18 +108,18 @@ void Reactions::layoutReactionsText() {
Ui::NameTextOptions());
}
QSize Reactions::countOptimalSize() {
QSize InlineList::countOptimalSize() {
return QSize(_reactions.maxWidth(), _reactions.minHeight());
}
QSize Reactions::countCurrentSize(int newWidth) {
QSize InlineList::countCurrentSize(int newWidth) {
if (newWidth >= maxWidth()) {
return optimalSize();
}
return { newWidth, _reactions.countHeight(newWidth) };
}
void Reactions::paint(
void InlineList::paint(
Painter &p,
const Ui::ChatStyle *st,
int outerWidth,
@ -106,8 +127,8 @@ void Reactions::paint(
_reactions.draw(p, 0, 0, outerWidth);
}
Reactions::Data ReactionsDataFromMessage(not_null<Message*> message) {
auto result = Reactions::Data();
InlineListData InlineListDataFromMessage(not_null<Message*> message) {
auto result = InlineListData();
const auto item = message->message();
result.reactions = item->reactions();
@ -115,111 +136,84 @@ Reactions::Data ReactionsDataFromMessage(not_null<Message*> message) {
return result;
}
ReactButton::ReactButton(
Fn<void()> update,
Fn<void()> react,
QRect bubble)
: _update(std::move(update))
, _handler(std::make_shared<LambdaClickHandler>(react)) {
updateGeometry(bubble);
Button::Button(
Fn<void(QRect)> update,
ButtonParameters parameters)
: _update(std::move(update)) {
_geometry = QRect(QPoint(), CountOuterSize());
_outbg = parameters.outbg;
}
void ReactButton::updateGeometry(QRect bubble) {
const auto topLeft = bubble.topLeft()
+ QPoint(bubble.width(), bubble.height())
+ QPoint(st::reactionCornerOut.x(), st::reactionCornerOut.y())
- 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);
Button::~Button() = default;
bool Button::outbg() const {
return _outbg;
}
int ReactButton::bottomOutsideMargin(int fullHeight) const {
return _geometry.y() + _geometry.height() - fullHeight;
bool Button::isHidden() const {
return (_state == State::Hidden) && !_scaleAnimation.animating();
}
std::optional<PointState> ReactButton::pointState(QPoint point) const {
if (!_geometry.contains(point)) {
return std::nullopt;
QRect Button::geometry() const {
return _geometry;
}
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;
}
std::optional<TextState> ReactButton::textState(
QPoint point,
const StateRequest &request) const {
if (!_geometry.contains(point)) {
return std::nullopt;
if (_geometry != geometry) {
if (!_geometry.isNull()) {
_update(_geometry);
}
_geometry = geometry;
_update(_geometry);
}
auto result = TextState(nullptr, _handler);
result.reactionArea = _geometry;
return result;
applyState(parameters.active ? State::Active : State::Shown);
}
void ReactButton::paint(Painter &p, const PaintContext &context) {
const auto shown = _shownAnimation.value(_shown ? 1. : 0.);
if (shown == 0.) {
void Button::applyState(State state) {
if (_state == state) {
return;
}
p.setOpacity(shown);
p.setBrush(context.messageStyle()->msgBg);
p.setPen(st::shadowFg);
const auto radius = _geometry.height() / 2;
p.drawRoundedRect(_geometry, radius, radius);
if (!_image.isNull()) {
p.drawImage(_imagePosition, _image);
}
p.setOpacity(1.);
const auto duration = (state == State::Hidden
|| _state == State::Hidden)
? kToggleDuration
: kActivateDuration;
_scaleAnimation.start(
[=] { _update(_geometry); },
ScaleForState(_state),
ScaleForState(state),
duration);
_state = state;
}
void ReactButton::toggle(bool shown) {
if (_shown == shown) {
return;
float64 Button::ScaleForState(State state) {
switch (state) {
case State::Hidden: return 0.7;
case State::Shown: return 1.;
case State::Active: return 1.4;
}
_shown = shown;
_shownAnimation.start(_update, _shown ? 0. : 1., _shown ? 1. : 0., 120);
Unexpected("State in ReactionButton::ScaleForState.");
}
bool ReactButton::isHidden() const {
return !_shown && !_shownAnimation.animating();
float64 Button::OpacityForScale(float64 scale) {
return (scale >= 1.)
? 1.
: ((scale - ScaleForState(State::Hidden))
/ (ScaleForState(State::Shown) - ScaleForState(State::Hidden)));
}
void ReactButton::show(not_null<const Data::Reaction*> reaction) {
if (_media && _media->owner() == reaction->staticIcon) {
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);
}
float64 Button::currentScale() const {
return _scaleAnimation.value(ScaleForState(_state));
}
ReactionsMenu::ReactionsMenu(
Selector::Selector(
QWidget *parent,
const std::vector<Data::Reaction> &list)
: _dropdown(parent) {
@ -331,7 +325,7 @@ ReactionsMenu::ReactionsMenu(
_dropdown.resizeToContent();
}
void ReactionsMenu::showAround(QRect area) {
void Selector::showAround(QRect area) {
const auto parent = _dropdown.parentWidget();
const auto left = std::min(
std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0),
@ -345,7 +339,7 @@ void ReactionsMenu::showAround(QRect area) {
_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 (shown) {
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();
}
[[nodiscard]] rpl::lifetime &ReactionsMenu::lifetime() {
[[nodiscard]] rpl::lifetime &Selector::lifetime() {
return _dropdown.lifetime();
}
ReactionsMenuManager::ReactionsMenuManager(QWidget *parent)
: _parent(parent) {
Manager::Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate)
: _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(
FullMsgId context,
QRect globalReactionArea,
const std::vector<Data::Reaction> &list) {
if (globalReactionArea.isEmpty()) {
void Manager::showButton(ButtonParameters parameters) {
if (_button && _buttonContext != parameters.context) {
_button->applyState(ButtonState::Hidden);
_buttonHiding.push_back(std::move(_button));
}
_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();
}
const auto listsEqual = ranges::equal(
_list,
list,
ranges::equal_to(),
&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));
const auto changed = (_selectorContext != context);
if (_selector && changed) {
_selector->toggle(false, anim::type::normal);
_selectorHiding.push_back(std::move(_selector));
}
_context = context;
_list = list;
if (list.size() < 2 || !context || (!changed && !_menu)) {
_selectorContext = context;
if (_list.size() < 2 || !context || (!changed && !_selector)) {
return;
} else if (!_menu) {
_menu = std::make_unique<ReactionsMenu>(_parent, list);
_menu->chosen(
} else if (!_selector) {
_selector = std::make_unique<Selector>(_selectorParent, _list);
_selector->chosen(
) | rpl::start_with_next([=](QString emoji) {
_menu->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu));
_selector->toggle(false, anim::type::normal);
_selectorHiding.push_back(std::move(_selector));
_chosen.fire({ context, std::move(emoji) });
}, _menu->lifetime());
}, _selector->lifetime());
}
const auto area = QRect(
_parent->mapFromGlobal(globalReactionArea.topLeft()),
globalReactionArea.size());
_menu->showAround(area);
_menu->toggle(true, anim::type::normal);
_selectorParent->mapFromGlobal(globalButtonArea.topLeft()),
globalButtonArea.size());
_selector->showAround(area);
_selector->toggle(true, anim::type::normal);
}
void ReactionsMenuManager::hideAll(anim::type animated) {
void Manager::hideSelectors(anim::type animated) {
if (animated == anim::type::instant) {
_hiding.clear();
_menu = nullptr;
} else if (_menu) {
_menu->toggle(false, anim::type::normal);
_hiding.push_back(std::move(_menu));
_selectorHiding.clear();
_selector = nullptr;
} else if (_selector) {
_selector->toggle(false, anim::type::normal);
_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"
class Image;
class Painter;
namespace Ui {
class ChatStyle;
@ -24,21 +25,24 @@ class DocumentMedia;
} // namespace Data
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
enum class PointState : char;
struct TextState;
struct StateRequest;
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:
struct Data {
base::flat_map<QString, int> reactions;
QString chosenReaction;
};
explicit Reactions(Data &&data);
using Data = InlineListData;
explicit InlineList(Data &&data);
void update(Data &&data, int availableWidth);
QSize countCurrentSize(int newWidth) override;
@ -63,43 +67,64 @@ private:
};
[[nodiscard]] Reactions::Data ReactionsDataFromMessage(
[[nodiscard]] InlineListData InlineListDataFromMessage(
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:
ReactButton(Fn<void()> update, Fn<void()> react, QRect bubble);
Button(Fn<void(QRect)> update, ButtonParameters parameters);
~Button();
void updateGeometry(QRect bubble);
[[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 applyParameters(ButtonParameters parameters);
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;
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:
const Fn<void()> _update;
const ClickHandlerPtr _handler;
QRect _geometry;
bool _shown = false;
Ui::Animations::Simple _shownAnimation;
const Fn<void(QRect)> _update;
State _state = State::Hidden;
Ui::Animations::Simple _scaleAnimation;
QImage _image;
QPoint _imagePosition;
std::shared_ptr<Data::DocumentMedia> _media;
rpl::lifetime _downloadTaskLifetime;
QRect _geometry;
ButtonStyle _style = ButtonStyle::Bubble;
bool _outbg = false;
};
class ReactionsMenu final {
class Selector final {
public:
ReactionsMenu(
Selector(
QWidget *parent,
const std::vector<Data::Reaction> &list);
@ -123,34 +148,88 @@ private:
};
class ReactionsMenuManager final {
class Manager final {
public:
explicit ReactionsMenuManager(QWidget *parent);
~ReactionsMenuManager();
Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate);
~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 {
FullMsgId context;
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 {
return _chosen.events();
}
private:
QWidget *_parent = nullptr;
rpl::event_stream<Chosen> _chosen;
static constexpr auto kFramesCount = 30;
std::unique_ptr<ReactionsMenu> _menu;
FullMsgId _context;
void removeStaleButtons();
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<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;
}
reactionCornerSize: size(23px, 18px);
reactionCornerOut: point(7px, 5px);
reactionCornerImage: 14px;
reactionCornerSize: size(27px, 19px);
reactionCornerCenter: point(-6px, -5px);
reactionCornerImage: 15px;
reactionCornerShadow: margins(4px, 4px, 4px, 8px);
reactionCornerActiveAreaPadding: margins(10px, 10px, 10px, 10px);
reactionPopupImage: 25px;
reactionPopupPadding: margins(5px, 5px, 5px, 5px);

View file

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