mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-17 22:57:11 +02:00
Proof-of-concept reactions dropdown.
This commit is contained in:
parent
9c18f7b0e3
commit
e148b5ff08
19 changed files with 585 additions and 9 deletions
|
@ -66,9 +66,7 @@ void AddReactionIcon(
|
|||
document->session().downloaderTaskFinished(
|
||||
) | rpl::map([=] {
|
||||
return state->media->getStickerLarge();
|
||||
}) | rpl::filter([=](Image *image) {
|
||||
return (image != nullptr);
|
||||
}) | rpl::take(
|
||||
}) | rpl::filter_nullptr() | rpl::take(
|
||||
1
|
||||
) | rpl::start_with_next([=](not_null<Image*> image) {
|
||||
setImage(image);
|
||||
|
|
|
@ -10,6 +10,7 @@ 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,6 +673,11 @@ 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,6 +141,8 @@ 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();
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/view/history_view_service_message.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "history/view/history_view_context_menu.h"
|
||||
#include "history/view/history_view_reactions.h"
|
||||
#include "history/view/history_view_emoji_interactions.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "history/history_item_text.h"
|
||||
|
@ -176,6 +177,8 @@ HistoryInner::HistoryInner(
|
|||
HistoryView::MakePathShiftGradient(
|
||||
controller->chatStyle(),
|
||||
[=] { update(); }))
|
||||
, _reactionsMenus(
|
||||
std::make_unique<HistoryView::ReactionsMenuManager>(historyWidget))
|
||||
, _touchSelectTimer([=] { onTouchSelect(); })
|
||||
, _touchScrollTimer([=] { onTouchScrollTimer(); })
|
||||
, _scrollDateCheck([this] { scrollDateCheck(); })
|
||||
|
@ -222,6 +225,14 @@ HistoryInner::HistoryInner(
|
|||
_controller->emojiInteractions().playStarted(_peer, std::move(emoji));
|
||||
}, lifetime());
|
||||
|
||||
using ChosenReaction = HistoryView::ReactionsMenuManager::Chosen;
|
||||
_reactionsMenus->chosen(
|
||||
) | rpl::start_with_next([=](ChosenReaction reaction) {
|
||||
if (const auto item = session().data().message(reaction.context)) {
|
||||
item->addReaction(reaction.emoji);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
session().data().itemRemoved(
|
||||
) | rpl::start_with_next(
|
||||
[this](auto item) { itemRemoved(item); },
|
||||
|
@ -263,6 +274,18 @@ HistoryInner::HistoryInner(
|
|||
update();
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(
|
||||
rpl::empty_value()
|
||||
) | rpl::then(session().data().reactions().updates()),
|
||||
session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::Reactions)
|
||||
) | rpl::start_with_next([=] {
|
||||
_reactions = session().data().reactions().list(_peer);
|
||||
repaintItem(App::mousedItem());
|
||||
}, lifetime());
|
||||
|
||||
controller->adaptive().chatWideValue(
|
||||
) | rpl::start_with_next([=](bool wide) {
|
||||
_isChatWide = wide;
|
||||
|
@ -1510,6 +1533,7 @@ void HistoryInner::mouseActionFinish(
|
|||
.sessionWindow = base::make_weak(_controller.get()),
|
||||
})
|
||||
});
|
||||
_reactionsMenus->hideAll(anim::type::normal);
|
||||
return;
|
||||
}
|
||||
if ((_mouseAction == MouseAction::PrepareSelect)
|
||||
|
@ -2125,6 +2149,19 @@ 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()) {
|
||||
|
@ -2884,6 +2921,13 @@ 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 {};
|
||||
|
@ -2960,7 +3004,16 @@ void HistoryInner::mouseActionUpdate() {
|
|||
view = block->messages[_curItem].get();
|
||||
item = view->data();
|
||||
|
||||
App::mousedItem(view);
|
||||
const auto was = App::mousedItem();
|
||||
if (was != view) {
|
||||
if (!_reactions.empty()) {
|
||||
repaintItem(was);
|
||||
}
|
||||
App::mousedItem(view);
|
||||
if (!_reactions.empty()) {
|
||||
repaintItem(view);
|
||||
}
|
||||
}
|
||||
m = mapPointToItem(point, view);
|
||||
if (view->pointState(m) != PointState::Outside) {
|
||||
if (App::hoveredItem() != view) {
|
||||
|
@ -3095,6 +3148,7 @@ 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) {
|
||||
|
@ -3798,6 +3852,12 @@ 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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
namespace Data {
|
||||
struct Group;
|
||||
class CloudImageView;
|
||||
struct Reaction;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
|
@ -28,6 +29,7 @@ enum class CursorState : char;
|
|||
enum class PointState : char;
|
||||
class EmptyPainter;
|
||||
class Element;
|
||||
class ReactionsMenuManager;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Window {
|
||||
|
@ -57,6 +59,7 @@ public:
|
|||
not_null<Ui::ScrollArea*> scroll,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history);
|
||||
~HistoryInner();
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
[[nodiscard]] not_null<Ui::ChatTheme*> theme() const {
|
||||
|
@ -117,6 +120,7 @@ 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);
|
||||
|
||||
|
@ -155,8 +159,6 @@ public:
|
|||
// HistoryView::ElementDelegate interface.
|
||||
static not_null<HistoryView::ElementDelegate*> ElementDelegate();
|
||||
|
||||
~HistoryInner();
|
||||
|
||||
protected:
|
||||
bool focusNextPrevChild(bool next) override;
|
||||
|
||||
|
@ -338,10 +340,10 @@ private:
|
|||
void deleteAsGroup(FullMsgId itemId);
|
||||
void reportItem(FullMsgId itemId);
|
||||
void reportAsGroup(FullMsgId itemId);
|
||||
void reportItems(MessageIdsList ids);
|
||||
void blockSenderItem(FullMsgId itemId);
|
||||
void blockSenderAsGroup(FullMsgId itemId);
|
||||
void copySelectedText();
|
||||
void showReactionsMenu(FullMsgId itemId, QRect area);
|
||||
|
||||
void setupSharingDisallowed();
|
||||
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;
|
||||
|
@ -395,6 +397,9 @@ private:
|
|||
not_null<PeerData*>,
|
||||
std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
|
||||
|
||||
std::vector<Data::Reaction> _reactions;
|
||||
std::unique_ptr<HistoryView::ReactionsMenuManager> _reactionsMenus;
|
||||
|
||||
MouseAction _mouseAction = MouseAction::None;
|
||||
TextSelectType _mouseSelectType = TextSelectType::Letters;
|
||||
QPoint _dragStartPosition;
|
||||
|
|
|
@ -53,6 +53,7 @@ struct TextState {
|
|||
bool customTooltip = false;
|
||||
uint16 symbol = 0;
|
||||
QString customTooltipText;
|
||||
QRect reactionArea;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -188,6 +188,11 @@ 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) {
|
||||
|
|
|
@ -18,6 +18,10 @@ class HistoryMessage;
|
|||
class HistoryService;
|
||||
struct HistoryMessageReply;
|
||||
|
||||
namespace Data {
|
||||
struct Reaction;
|
||||
} // namespace Data
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
@ -92,6 +96,8 @@ 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() {
|
||||
}
|
||||
|
@ -150,6 +156,8 @@ 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 {
|
||||
|
|
|
@ -1461,6 +1461,11 @@ 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,6 +279,8 @@ 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);
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "mainwidget.h"
|
||||
#include "main/main_session.h"
|
||||
|
@ -604,6 +605,28 @@ 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()
|
||||
|
@ -744,6 +767,10 @@ 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);
|
||||
}
|
||||
|
@ -1070,6 +1097,12 @@ 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();
|
||||
|
@ -1246,6 +1279,14 @@ 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;
|
||||
|
@ -1923,9 +1964,11 @@ 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() + add.bottom()
|
||||
.height = height() + add.top() + addBottom
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2669,6 +2712,7 @@ int Message::resizeContentGetHeight(int newWidth) {
|
|||
if (_reactions && !reactionsInBubble) {
|
||||
newHeight += st::mediaInBubbleSkip + _reactions->resizeGetHeight(contentWidth);
|
||||
}
|
||||
|
||||
if (const auto keyboard = item->inlineReplyKeyboard()) {
|
||||
const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();
|
||||
newHeight += keyboardHeight;
|
||||
|
|
|
@ -19,6 +19,7 @@ struct HistoryMessageForwarded;
|
|||
namespace HistoryView {
|
||||
|
||||
class ViewButton;
|
||||
class ReactButton;
|
||||
class Reactions;
|
||||
class WebPage;
|
||||
|
||||
|
@ -233,6 +234,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;
|
||||
mutable std::unique_ptr<CommentsButton> _comments;
|
||||
|
||||
|
|
|
@ -8,11 +8,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/view/history_view_reactions.h"
|
||||
|
||||
#include "history/view/history_view_message.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "history/history_message.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "main/main_session.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
constexpr auto kItemsPerRow = 5;
|
||||
|
||||
} // namespace
|
||||
|
||||
Reactions::Reactions(Data &&data)
|
||||
: _data(std::move(data))
|
||||
|
@ -101,4 +115,313 @@ 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int ReactButton::bottomOutsideMargin(int fullHeight) const {
|
||||
return _geometry.y() + _geometry.height() - fullHeight;
|
||||
}
|
||||
|
||||
std::optional<PointState> ReactButton::pointState(QPoint point) const {
|
||||
if (!_geometry.contains(point)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return PointState::Inside;
|
||||
}
|
||||
|
||||
std::optional<TextState> ReactButton::textState(
|
||||
QPoint point,
|
||||
const StateRequest &request) const {
|
||||
if (!_geometry.contains(point)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto result = TextState(nullptr, _handler);
|
||||
result.reactionArea = _geometry;
|
||||
return result;
|
||||
}
|
||||
|
||||
void ReactButton::paint(Painter &p, const PaintContext &context) {
|
||||
const auto shown = _shownAnimation.value(_shown ? 1. : 0.);
|
||||
if (shown == 0.) {
|
||||
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.);
|
||||
}
|
||||
|
||||
void ReactButton::toggle(bool shown) {
|
||||
if (_shown == shown) {
|
||||
return;
|
||||
}
|
||||
_shown = shown;
|
||||
_shownAnimation.start(_update, _shown ? 0. : 1., _shown ? 1. : 0., 120);
|
||||
}
|
||||
|
||||
bool ReactButton::isHidden() const {
|
||||
return !_shown && !_shownAnimation.animating();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ReactionsMenu::ReactionsMenu(
|
||||
QWidget *parent,
|
||||
const std::vector<Data::Reaction> &list)
|
||||
: _dropdown(parent) {
|
||||
_dropdown.setAutoHiding(false);
|
||||
|
||||
const auto content = _dropdown.setOwnedWidget(
|
||||
object_ptr<Ui::RpWidget>(&_dropdown));
|
||||
|
||||
const auto count = int(list.size());
|
||||
const auto single = st::reactionPopupImage;
|
||||
const auto padding = st::reactionPopupPadding;
|
||||
const auto width = padding.left() + single + padding.right();
|
||||
const auto height = padding.top() + single + padding.bottom();
|
||||
const auto rows = (count + kItemsPerRow - 1) / kItemsPerRow;
|
||||
const auto columns = (int(list.size()) + rows - 1) / rows;
|
||||
const auto inner = QRect(0, 0, columns * width, rows * height);
|
||||
const auto outer = inner.marginsAdded(padding);
|
||||
content->resize(outer.size());
|
||||
|
||||
_elements.reserve(list.size());
|
||||
auto x = padding.left();
|
||||
auto y = padding.top();
|
||||
auto row = -1;
|
||||
auto perrow = 0;
|
||||
while (_elements.size() != list.size()) {
|
||||
if (!perrow) {
|
||||
++row;
|
||||
perrow = (list.size() - _elements.size()) / (rows - row);
|
||||
x = (outer.width() - perrow * width) / 2;
|
||||
}
|
||||
auto &reaction = list[_elements.size()];
|
||||
_elements.push_back({
|
||||
.emoji = reaction.emoji,
|
||||
.geometry = QRect(x, y + row * height, width, height),
|
||||
});
|
||||
x += width;
|
||||
--perrow;
|
||||
}
|
||||
|
||||
struct State {
|
||||
int selected = -1;
|
||||
int pressed = -1;
|
||||
};
|
||||
const auto state = content->lifetime().make_state<State>();
|
||||
content->setMouseTracking(true);
|
||||
content->events(
|
||||
) | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::MouseMove) {
|
||||
const auto position = static_cast<QMouseEvent*>(e.get())->pos();
|
||||
const auto i = ranges::find_if(_elements, [&](const Element &e) {
|
||||
return e.geometry.contains(position);
|
||||
});
|
||||
const auto selected = (i != end(_elements))
|
||||
? int(i - begin(_elements))
|
||||
: -1;
|
||||
if (state->selected != selected) {
|
||||
state->selected = selected;
|
||||
content->update();
|
||||
}
|
||||
} else if (type == QEvent::MouseButtonPress) {
|
||||
state->pressed = state->selected;
|
||||
content->update();
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
const auto pressed = std::exchange(state->pressed, -1);
|
||||
if (pressed >= 0) {
|
||||
content->update();
|
||||
if (pressed == state->selected) {
|
||||
_chosen.fire_copy(_elements[pressed].emoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, content->lifetime());
|
||||
|
||||
content->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = QPainter(content);
|
||||
const auto radius = st::roundRadiusSmall;
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(st::emojiPanBg);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(content->rect(), radius, radius);
|
||||
}
|
||||
auto index = 0;
|
||||
const auto activeIndex = (state->pressed >= 0)
|
||||
? state->pressed
|
||||
: state->selected;
|
||||
const auto size = Ui::Emoji::GetSizeNormal();
|
||||
for (const auto &element : _elements) {
|
||||
const auto active = (index++ == activeIndex);
|
||||
if (active) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(st::windowBgOver);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(element.geometry, radius, radius);
|
||||
}
|
||||
if (const auto emoji = Ui::Emoji::Find(element.emoji)) {
|
||||
Ui::Emoji::Draw(
|
||||
p,
|
||||
emoji,
|
||||
size,
|
||||
element.geometry.x() + (width - size) / 2,
|
||||
element.geometry.y() + (height - size) / 2);
|
||||
}
|
||||
}
|
||||
}, content->lifetime());
|
||||
|
||||
_dropdown.resizeToContent();
|
||||
}
|
||||
|
||||
void ReactionsMenu::showAround(QRect area) {
|
||||
const auto parent = _dropdown.parentWidget();
|
||||
const auto left = std::min(
|
||||
std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0),
|
||||
parent->width() - _dropdown.width());
|
||||
_fromTop = (area.y() >= _dropdown.height());
|
||||
_fromLeft = (area.center().x() - left
|
||||
<= left + _dropdown.width() - area.center().x());
|
||||
const auto top = _fromTop
|
||||
? (area.y() - _dropdown.height())
|
||||
: (area.y() + area.height());
|
||||
_dropdown.move(left, top);
|
||||
}
|
||||
|
||||
void ReactionsMenu::toggle(bool shown, anim::type animated) {
|
||||
if (animated == anim::type::normal) {
|
||||
if (shown) {
|
||||
using Origin = Ui::PanelAnimation::Origin;
|
||||
_dropdown.showAnimated(_fromTop
|
||||
? (_fromLeft ? Origin::BottomLeft : Origin::BottomRight)
|
||||
: (_fromLeft ? Origin::TopLeft : Origin::TopRight));
|
||||
} else {
|
||||
_dropdown.hideAnimated();
|
||||
}
|
||||
} else if (shown) {
|
||||
_dropdown.showFast();
|
||||
} else {
|
||||
_dropdown.hideFast();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> ReactionsMenu::chosen() const {
|
||||
return _chosen.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::lifetime &ReactionsMenu::lifetime() {
|
||||
return _dropdown.lifetime();
|
||||
}
|
||||
|
||||
ReactionsMenuManager::ReactionsMenuManager(QWidget *parent)
|
||||
: _parent(parent) {
|
||||
}
|
||||
|
||||
ReactionsMenuManager::~ReactionsMenuManager() = default;
|
||||
|
||||
void ReactionsMenuManager::showReactionsMenu(
|
||||
FullMsgId context,
|
||||
QRect globalReactionArea,
|
||||
const std::vector<Data::Reaction> &list) {
|
||||
if (globalReactionArea.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));
|
||||
}
|
||||
_context = context;
|
||||
_list = list;
|
||||
if (list.size() < 2 || !context || (!changed && !_menu)) {
|
||||
return;
|
||||
} else if (!_menu) {
|
||||
_menu = std::make_unique<ReactionsMenu>(_parent, list);
|
||||
_menu->chosen(
|
||||
) | rpl::start_with_next([=](QString emoji) {
|
||||
_menu->toggle(false, anim::type::normal);
|
||||
_hiding.push_back(std::move(_menu));
|
||||
_chosen.fire({ context, std::move(emoji) });
|
||||
}, _menu->lifetime());
|
||||
}
|
||||
const auto area = QRect(
|
||||
_parent->mapFromGlobal(globalReactionArea.topLeft()),
|
||||
globalReactionArea.size());
|
||||
_menu->showAround(area);
|
||||
_menu->toggle(true, anim::type::normal);
|
||||
}
|
||||
|
||||
void ReactionsMenuManager::hideAll(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));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
|
|
@ -8,13 +8,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#pragma once
|
||||
|
||||
#include "history/view/history_view_object.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/inner_dropdown.h"
|
||||
|
||||
class Image;
|
||||
|
||||
namespace Ui {
|
||||
class ChatStyle;
|
||||
struct ChatPaintContext;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
struct Reaction;
|
||||
class DocumentMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
using PaintContext = Ui::ChatPaintContext;
|
||||
enum class PointState : char;
|
||||
struct TextState;
|
||||
struct StateRequest;
|
||||
class Message;
|
||||
|
||||
class Reactions final : public Object {
|
||||
|
@ -52,4 +66,92 @@ private:
|
|||
[[nodiscard]] Reactions::Data ReactionsDataFromMessage(
|
||||
not_null<Message*> message);
|
||||
|
||||
class ReactButton final {
|
||||
public:
|
||||
ReactButton(Fn<void()> update, Fn<void()> react, QRect bubble);
|
||||
|
||||
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 paint(Painter &p, const PaintContext &context);
|
||||
|
||||
void toggle(bool shown);
|
||||
[[nodiscard]] bool isHidden() const;
|
||||
void show(not_null<const Data::Reaction*> reaction);
|
||||
|
||||
private:
|
||||
const Fn<void()> _update;
|
||||
const ClickHandlerPtr _handler;
|
||||
QRect _geometry;
|
||||
bool _shown = false;
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
|
||||
QImage _image;
|
||||
QPoint _imagePosition;
|
||||
std::shared_ptr<Data::DocumentMedia> _media;
|
||||
rpl::lifetime _downloadTaskLifetime;
|
||||
|
||||
};
|
||||
|
||||
class ReactionsMenu final {
|
||||
public:
|
||||
ReactionsMenu(
|
||||
QWidget *parent,
|
||||
const std::vector<Data::Reaction> &list);
|
||||
|
||||
void showAround(QRect area);
|
||||
void toggle(bool shown, anim::type animated);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> chosen() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct Element {
|
||||
QString emoji;
|
||||
QRect geometry;
|
||||
};
|
||||
Ui::InnerDropdown _dropdown;
|
||||
rpl::event_stream<QString> _chosen;
|
||||
std::vector<Element> _elements;
|
||||
bool _fromTop = true;
|
||||
bool _fromLeft = true;
|
||||
|
||||
};
|
||||
|
||||
class ReactionsMenuManager final {
|
||||
public:
|
||||
explicit ReactionsMenuManager(QWidget *parent);
|
||||
~ReactionsMenuManager();
|
||||
|
||||
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;
|
||||
|
||||
std::unique_ptr<ReactionsMenu> _menu;
|
||||
FullMsgId _context;
|
||||
std::vector<Data::Reaction> _list;
|
||||
std::vector<std::unique_ptr<ReactionsMenu>> _hiding;
|
||||
|
||||
};
|
||||
|
||||
} // namespace HistoryView
|
||||
|
|
|
@ -962,7 +962,7 @@ void ListWidget::repaintItem(QRect itemGeometry) {
|
|||
}
|
||||
|
||||
bool ListWidget::isMyItem(not_null<const HistoryItem*> item) const {
|
||||
auto peer = item->history()->peer;
|
||||
const auto peer = item->history()->peer;
|
||||
return (_peer == peer) || (_migrated == peer);
|
||||
}
|
||||
|
||||
|
|
|
@ -954,3 +954,10 @@ sendAsButton: SendAsButton {
|
|||
}
|
||||
duration: 150;
|
||||
}
|
||||
|
||||
reactionCornerSize: size(23px, 18px);
|
||||
reactionCornerOut: point(7px, 5px);
|
||||
reactionCornerImage: 14px;
|
||||
|
||||
reactionPopupImage: 25px;
|
||||
reactionPopupPadding: margins(5px, 5px, 5px, 5px);
|
||||
|
|
|
@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/history_view_replies_section.h"
|
||||
#include "history/view/history_view_reactions.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "media/view/media_view_open_common.h"
|
||||
#include "data/data_document_resolver.h"
|
||||
|
|
|
@ -55,8 +55,13 @@ 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