mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 14:17:12 +02:00
Implement a nice corner reaction button.
This commit is contained in:
parent
e148b5ff08
commit
371c9c1bfe
16 changed files with 658 additions and 332 deletions
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,7 +53,6 @@ struct TextState {
|
|||
bool customTooltip = false;
|
||||
uint16 symbol = 0;
|
||||
QString customTooltipText;
|
||||
QRect reactionArea;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue