/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: 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/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" #include "core/click_handler_types.h" #include "main/main_session.h" #include "styles/style_chat.h" #include "styles/palette.h" 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 CountMaxSizeWithMargins(style::margins margins) { const auto extended = QRect( QPoint(), st::reactionCornerSize ).marginsAdded(margins); const auto scale = Button::ScaleForState(ButtonState::Active); return QSize( int(base::SafeRound(extended.width() * scale)), int(base::SafeRound(extended.height() * scale))); } [[nodiscard]] QSize CountOuterSize() { return CountMaxSizeWithMargins(st::reactionCornerShadow); } } // namespace InlineList::InlineList(Data &&data) : _data(std::move(data)) , _reactions(st::msgMinWidth / 2) { layout(); } void InlineList::update(Data &&data, int availableWidth) { _data = std::move(data); layout(); if (width() > 0) { resizeGetHeight(std::min(maxWidth(), availableWidth)); } } void InlineList::updateSkipBlock(int width, int height) { _reactions.updateSkipBlock(width, height); } void InlineList::removeSkipBlock() { _reactions.removeSkipBlock(); } void InlineList::layout() { layoutReactionsText(); initDimensions(); } void InlineList::layoutReactionsText() { if (_data.reactions.empty()) { _reactions.clear(); return; } auto sorted = ranges::view::all( _data.reactions ) | ranges::view::transform([](const auto &pair) { return std::make_pair(pair.first, pair.second); }) | ranges::to_vector; ranges::sort(sorted, std::greater<>(), &std::pair::second); auto text = TextWithEntities(); for (const auto &[string, count] : sorted) { if (!text.text.isEmpty()) { text.append(" - "); } const auto chosen = (_data.chosenReaction == string); text.append(string); if (_data.chosenReaction == string) { text.append(Ui::Text::Bold(QString::number(count))); } else { text.append(QString::number(count)); } } _reactions.setMarkedText( st::msgDateTextStyle, text, Ui::NameTextOptions()); } QSize InlineList::countOptimalSize() { return QSize(_reactions.maxWidth(), _reactions.minHeight()); } QSize InlineList::countCurrentSize(int newWidth) { if (newWidth >= maxWidth()) { return optimalSize(); } return { newWidth, _reactions.countHeight(newWidth) }; } void InlineList::paint( Painter &p, const Ui::ChatStyle *st, int outerWidth, const QRect &clip) const { _reactions.draw(p, 0, 0, outerWidth); } InlineListData InlineListDataFromMessage(not_null message) { auto result = InlineListData(); const auto item = message->message(); result.reactions = item->reactions(); result.chosenReaction = item->chosenReaction(); return result; } Button::Button( Fn update, ButtonParameters parameters) : _update(std::move(update)) { const auto initial = QRect(QPoint(), CountOuterSize()); _geometry = initial.translated(parameters.center - initial.center()); _outbg = parameters.outbg; applyState(parameters.active ? State::Active : State::Shown); } Button::~Button() = default; bool Button::outbg() const { return _outbg; } bool Button::isHidden() const { return (_state == State::Hidden) && !_scaleAnimation.animating(); } QRect Button::geometry() const { return _geometry; } void Button::applyParameters(ButtonParameters parameters) { const auto geometry = _geometry.translated( parameters.center - _geometry.center()); if (_outbg != parameters.outbg) { _outbg = parameters.outbg; _update(_geometry); } if (_geometry != geometry) { if (!_geometry.isNull()) { _update(_geometry); } _geometry = geometry; _update(_geometry); } applyState(parameters.active ? State::Active : State::Shown); } void Button::applyState(State state) { if (_state == state) { return; } const auto duration = (state == State::Hidden || _state == State::Hidden) ? kToggleDuration : kActivateDuration; _scaleAnimation.start( [=] { _update(_geometry); }, ScaleForState(_state), ScaleForState(state), duration); _state = state; } float64 Button::ScaleForState(State state) { switch (state) { case State::Hidden: return 0.7; case State::Shown: return 1.; case State::Active: return 1.4; } Unexpected("State in ReactionButton::ScaleForState."); } float64 Button::OpacityForScale(float64 scale) { return (scale >= 1.) ? 1. : ((scale - ScaleForState(State::Hidden)) / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))); } float64 Button::currentScale() const { return _scaleAnimation.value(ScaleForState(_state)); } Selector::Selector( QWidget *parent, const std::vector &list) : _dropdown(parent) { _dropdown.setAutoHiding(false); const auto content = _dropdown.setOwnedWidget( object_ptr(&_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(); content->setMouseTracking(true); content->events( ) | rpl::start_with_next([=](not_null e) { const auto type = e->type(); if (type == QEvent::MouseMove) { const auto position = static_cast(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 Selector::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 Selector::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 Selector::chosen() const { return _chosen.events(); } [[nodiscard]] rpl::lifetime &Selector::lifetime() { return _dropdown.lifetime(); } Manager::Manager(QWidget *selectorParent, Fn 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)) , _buttonLink(std::make_shared(crl::guard(this, [=] { if (_buttonContext && !_list.empty()) { _chosen.fire({ .context = _buttonContext, .emoji = _list.front().emoji, }); } }))) , _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); } Manager::~Manager() = default; 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