diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 46972e2e3..e8cd57be8 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -671,6 +671,8 @@ PRIVATE history/view/reactions/history_view_reactions_list.h history/view/reactions/history_view_reactions_selector.cpp history/view/reactions/history_view_reactions_selector.h + history/view/reactions/history_view_reactions_strip.cpp + history/view/reactions/history_view_reactions_strip.h history/view/reactions/history_view_reactions_tabs.cpp history/view/reactions/history_view_reactions_tabs.h history/view/history_view_bottom_info.cpp diff --git a/Telegram/Resources/icons/chat/reactions_bubble.png b/Telegram/Resources/icons/chat/reactions_bubble.png new file mode 100644 index 000000000..b7c428757 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_bubble.png differ diff --git a/Telegram/Resources/icons/chat/reactions_bubble@2x.png b/Telegram/Resources/icons/chat/reactions_bubble@2x.png new file mode 100644 index 000000000..d98f19194 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_bubble@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_bubble@3x.png b/Telegram/Resources/icons/chat/reactions_bubble@3x.png new file mode 100644 index 000000000..53bbc4cf9 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_bubble@3x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_bubble_shadow.png b/Telegram/Resources/icons/chat/reactions_bubble_shadow.png new file mode 100644 index 000000000..27cecc54b Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_bubble_shadow.png differ diff --git a/Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png b/Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png new file mode 100644 index 000000000..d6875ca32 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_bubble_shadow@3x.png b/Telegram/Resources/icons/chat/reactions_bubble_shadow@3x.png new file mode 100644 index 000000000..2e8f80f73 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_bubble_shadow@3x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg.png b/Telegram/Resources/icons/chat/reactions_expand_bg.png new file mode 100644 index 000000000..9c96eedd5 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_expand_bg.png differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png new file mode 100644 index 000000000..827ac5f14 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png new file mode 100644 index 000000000..3aaa0e272 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_panel.png b/Telegram/Resources/icons/chat/reactions_expand_panel.png new file mode 100644 index 000000000..35ceca70c Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_expand_panel.png differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_panel@2x.png b/Telegram/Resources/icons/chat/reactions_expand_panel@2x.png new file mode 100644 index 000000000..b76326d45 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_expand_panel@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_expand_panel@3x.png b/Telegram/Resources/icons/chat/reactions_expand_panel@3x.png new file mode 100644 index 000000000..3c5a48b5f Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_expand_panel@3x.png differ diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 2cf6df13d..1f295fbdd 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -17,7 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_message.h" #include "history/view/history_view_element.h" -#include "history/view/reactions/history_view_reactions_button.h" +#include "history/view/reactions/history_view_reactions_strip.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "boxes/premium_preview_box.h" diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 70caa3f8c..c12f2150f 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -316,3 +316,8 @@ reactStripExtend: margins(21px, 49px, 39px, 0px); reactStripHeight: 40px; reactStripSize: 32px; reactStripSkip: 7px; +reactStripBubble: icon{ + { "chat/reactions_bubble_shadow", windowShadowFg }, + { "chat/reactions_bubble", windowBg }, +}; +reactStripBubbleRight: 20px; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index d9d5b1429..4bd3f04ad 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_peer_values.h" #include "data/stickers/data_custom_emoji.h" #include "lottie/lottie_icon.h" #include "storage/localimageloader.h" @@ -43,6 +44,67 @@ constexpr auto kSizeForDownscale = 64; } // namespace +PossibleItemReactions LookupPossibleReactions(not_null item) { + if (!item->canReact()) { + return {}; + } + auto result = PossibleItemReactions(); + const auto peer = item->history()->peer; + const auto session = &peer->session(); + const auto reactions = &session->data().reactions(); + const auto &full = reactions->list(Reactions::Type::Active); + const auto &all = item->reactions(); + const auto my = item->chosenReaction(); + auto myIsUnique = false; + for (const auto &[id, count] : all) { + if (count == 1 && id == my) { + myIsUnique = true; + } + } + const auto notMineCount = int(all.size()) - (myIsUnique ? 1 : 0); + const auto limit = UniqueReactionsLimit(peer); + if (limit > 0 && notMineCount >= limit) { + result.recent.reserve(all.size()); + for (const auto &reaction : full) { + const auto id = reaction.id; + if (all.contains(id)) { + result.recent.push_back(&reaction); + } + } + } else { + const auto filter = PeerReactionsFilter(peer); + result.recent.reserve(filter.allowed + ? filter.allowed->size() + : full.size()); + for (const auto &reaction : full) { + const auto id = reaction.id; + const auto emoji = filter.allowed ? id.emoji() : QString(); + if (filter.allowed + && (emoji.isEmpty() || !filter.allowed->contains(emoji))) { + continue; + } else if (reaction.premium + && !session->premium() + && !all.contains(id)) { + if (session->premiumPossible()) { + result.morePremiumAvailable = true; + } + continue; + } else { + result.recent.push_back(&reaction); + } + } + result.customAllowed = session->premium() && peer->isUser(); + } + const auto i = ranges::find( + result.recent, + reactions->favorite(), + &Reaction::id); + if (i != end(result.recent) && i != begin(result.recent)) { + std::rotate(begin(result.recent), i, i + 1); + } + return result; +} + Reactions::Reactions(not_null owner) : _owner(owner) , _repaintTimer([=] { repaintCollected(); }) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index b47f72ac3..8dbb3b782 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -37,6 +37,15 @@ struct Reaction { bool premium = false; }; +struct PossibleItemReactions { + std::vector> recent; + bool morePremiumAvailable = false; + bool customAllowed = false; +}; + +[[nodiscard]] PossibleItemReactions LookupPossibleReactions( + not_null item); + class Reactions final { public: explicit Reactions(not_null owner); @@ -115,7 +124,7 @@ private: ReactionId _favorite; base::flat_map< not_null, - std::shared_ptr> _iconsCache; + std::shared_ptr> _iconsCache; rpl::event_stream<> _updated; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index eff154d10..89f0616f1 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -542,8 +542,8 @@ int UniqueReactionsLimit(not_null peer) { } rpl::producer UniqueReactionsLimitValue( - not_null session) { - const auto config = &session->account().appConfig(); + not_null peer) { + const auto config = &peer->session().account().appConfig(); return config->value( ) | rpl::map([=] { return UniqueReactionsLimit(config); diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index 61866e7fa..c4c13475f 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -140,6 +140,6 @@ inline auto PeerFullFlagValue( [[nodiscard]] int UniqueReactionsLimit(not_null peer); [[nodiscard]] rpl::producer UniqueReactionsLimitValue( - not_null session); + not_null peer); } // namespace Data diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0ae16886d..374846d15 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -342,10 +342,8 @@ HistoryInner::HistoryInner( , _reactionsManager( std::make_unique( this, - Data::UniqueReactionsLimitValue(&controller->session()), [=](QRect updated) { update(updated); }, controller->cachedReactionIconFactory().createMethod())) -, _reactionsSelector(std::make_unique()) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) @@ -394,26 +392,16 @@ HistoryInner::HistoryInner( _controller->emojiInteractions().playStarted(_peer, std::move(emoji)); }, lifetime()); - rpl::merge( - _reactionsManager->chosen(), - _reactionsSelector->chosen() + _reactionsManager->chosen( ) | rpl::start_with_next([=](ChosenReaction reaction) { _reactionsManager->updateButton({}); reactionChosen(reaction); }, lifetime()); - _reactionsManager->setExternalSelectorShown(_reactionsSelector->shown()); - _reactionsManager->expandSelectorRequests( - ) | rpl::start_with_next([=](ReactionExpandRequest request) { - if (request.expanded) { - _reactionsSelector->show( - _controller, - this, - request.context, - request.button); - } else { - _reactionsSelector->hide(); - } + _reactionsManager->premiumPromoChosen( + ) | rpl::start_with_next([=](FullMsgId context) { + _reactionsManager->updateButton({}); + premiumPromoChosen(context); }, lifetime()); session().data().itemRemoved( @@ -448,7 +436,6 @@ HistoryInner::HistoryInner( return item->mainView() != nullptr; }) | rpl::start_with_next([=](not_null item) { item->mainView()->itemDataChanged(); - _reactionsManager->updateUniqueLimit(item); }, lifetime()); session().changes().historyUpdates( @@ -460,8 +447,7 @@ HistoryInner::HistoryInner( HistoryView::Reactions::SetupManagerList( _reactionsManager.get(), - &session(), - Data::PeerReactionsFilterValue(_peer)); + _reactionsItem.value()); controller->adaptive().chatWideValue( ) | rpl::start_with_next([=](bool wide) { @@ -477,8 +463,6 @@ HistoryInner::HistoryInner( } void HistoryInner::reactionChosen(const ChosenReaction &reaction) { - const auto guard = gsl::finally([&] { _reactionsSelector->hide(); }); - const auto item = session().data().message(reaction.context); if (!item || Window::ShowReactPremiumError( @@ -501,6 +485,12 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { } } +void HistoryInner::premiumPromoChosen(FullMsgId context) { + if (const auto item = session().data().message(context)) { + ShowPremiumPromoBox(_controller, item); + } +} + Main::Session &HistoryInner::session() const { return _controller->session(); } @@ -1740,6 +1730,9 @@ void HistoryInner::itemRemoved(not_null item) { return; } + if (_reactionsItem.current() == item) { + _reactionsItem = nullptr; + } _animatedStickersPlayed.remove(item); _reactionsManager->remove(item->fullId()); @@ -1947,16 +1940,13 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) { } void HistoryInner::toggleFavoriteReaction(not_null view) const { - const auto favorite = session().data().reactions().favorite(); - const auto &filter = _reactionsManager->filter(); - if (favorite.emoji().isEmpty() && !filter.customAllowed) { - return; - } else if (filter.allowed - && !filter.allowed->contains(favorite.emoji())) { - return; - } const auto item = view->data(); - if (Window::ShowReactPremiumError(_controller, item, favorite)) { + const auto favorite = session().data().reactions().favorite(); + if (!ranges::contains( + Data::LookupPossibleReactions(item).recent, + favorite, + &Data::Reaction::id) + || Window::ShowReactPremiumError(_controller, item, favorite)) { return; } else if (item->chosenReaction() != favorite) { if (const auto top = itemTop(view); top >= 0) { @@ -2454,7 +2444,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { ? Element::Hovered()->data().get() : nullptr; const auto attached = reactItem - ? AttachSelectorToMenu(_menu.get(), desiredPosition, reactItem) + ? AttachSelectorToMenu( + _menu.get(), + desiredPosition, + reactItem, + [=](ChosenReaction reaction) { reactionChosen(reaction); }, + [=](FullMsgId context) { premiumPromoChosen(context); }, + _controller->cachedReactionIconFactory().createMethod()) : AttachSelectorResult::Skipped; if (attached == AttachSelectorResult::Failed) { _menu = nullptr; @@ -3417,7 +3413,7 @@ void HistoryInner::mouseActionUpdate() { m, reactionState)); if (changed) { - _reactionsManager->updateUniqueLimit(item); + _reactionsItem = item; } if (view->pointState(m) != PointState::Outside) { if (Element::Hovered() != view) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 3d90999df..11b4a0114 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -36,9 +36,7 @@ class Element; namespace HistoryView::Reactions { class Manager; -class Selector; struct ChosenReaction; -struct ExpandRequest; struct ButtonParameters; } // namespace HistoryView::Reactions @@ -229,7 +227,6 @@ private: class BotAbout; using ChosenReaction = HistoryView::Reactions::ChosenReaction; - using ReactionExpandRequest = HistoryView::Reactions::ExpandRequest; using VideoUserpic = Dialogs::Ui::VideoUserpic; using SelectedItems = std::map>; enum class MouseAction { @@ -402,6 +399,7 @@ private: -> HistoryView::Reactions::ButtonParameters; void toggleFavoriteReaction(not_null view) const; void reactionChosen(const ChosenReaction &reaction); + void premiumPromoChosen(FullMsgId context); void setupSharingDisallowed(); [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; @@ -464,7 +462,7 @@ private: std::unique_ptr> _videoUserpics; std::unique_ptr _reactionsManager; - std::unique_ptr _reactionsSelector; + rpl::variable _reactionsItem; MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 0cb72a5b6..5c5d9fe9a 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -277,7 +277,6 @@ ListWidget::ListWidget( , _reactionsManager( std::make_unique( this, - Data::UniqueReactionsLimitValue(&controller->session()), [=](QRect updated) { update(updated); }, controller->cachedReactionIconFactory().createMethod())) , _scrollDateCheck([this] { scrollDateCheck(); }) @@ -379,10 +378,17 @@ ListWidget::ListWidget( } }, lifetime()); + _reactionsManager->premiumPromoChosen( + ) | rpl::start_with_next([=] { + _reactionsManager->updateButton({}); + if (const auto item = _reactionsItem.current()) { + ShowPremiumPromoBox(_controller, item); + } + }, lifetime()); + Reactions::SetupManagerList( _reactionsManager.get(), - &session(), - _delegate->listAllowedReactionsValue()); + _reactionsItem.value()); controller->adaptive().chatWideValue( ) | rpl::start_with_next([=](bool wide) { @@ -2115,16 +2121,13 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) { } void ListWidget::toggleFavoriteReaction(not_null view) const { - const auto favorite = session().data().reactions().favorite(); - const auto &filter = _reactionsManager->filter(); - if (favorite.emoji().isEmpty() && !filter.customAllowed) { - return; - } else if (filter.allowed - && !filter.allowed->contains(favorite.emoji())) { - return; - } const auto item = view->data(); - if (Window::ShowReactPremiumError(_controller, item, favorite)) { + const auto favorite = session().data().reactions().favorite(); + if (!ranges::contains( + Data::LookupPossibleReactions(item).recent, + favorite, + &Data::Reaction::id) + || Window::ShowReactPremiumError(_controller, item, favorite)) { return; } else if (item->chosenReaction() != favorite) { if (const auto top = itemTop(view); top >= 0) { @@ -2727,7 +2730,7 @@ void ListWidget::mouseActionUpdate() { reactionState) : Reactions::ButtonParameters()); if (viewChanged && view) { - _reactionsManager->updateUniqueLimit(item); + _reactionsItem = item; } TextState dragState; @@ -3161,6 +3164,9 @@ void ListWidget::viewReplaced(not_null was, Element *now) { } void ListWidget::itemRemoved(not_null item) { + if (_reactionsItem.current() == item) { + _reactionsItem = nullptr; + } if (_selectedTextItem == item) { clearTextSelection(); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index f9b433c21..1f50c4b15 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -586,6 +586,7 @@ private: base::unique_qptr _emptyInfo = nullptr; std::unique_ptr _reactionsManager; + rpl::variable _reactionsItem; int _minHeight = 0; int _visibleTop = 0; diff --git a/Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp new file mode 100644 index 000000000..e4578c7b5 --- /dev/null +++ b/Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp @@ -0,0 +1,1422 @@ +/* +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_react_button.h" + +#include "history/view/history_view_cursor_state.h" +#include "history/history_item.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/message_bubble.h" +#include "ui/widgets/popup_menu.h" +#include "data/data_message_reactions.h" +#include "data/data_session.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_peer_values.h" +#include "lang/lang_keys.h" +#include "core/click_handler_types.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "base/event_filter.h" +#include "styles/style_chat.h" +#include "styles/style_menu_icons.h" + +namespace HistoryView::Reactions { +namespace { + +constexpr auto kDivider = 4; +constexpr auto kToggleDuration = crl::time(120); +constexpr auto kActivateDuration = crl::time(150); +constexpr auto kExpandDuration = crl::time(300); +constexpr auto kCollapseDuration = crl::time(250); +constexpr auto kEmojiCacheIndex = 0; +constexpr auto kButtonShowDelay = crl::time(300); +constexpr auto kButtonExpandDelay = crl::time(25); +constexpr auto kButtonHideDelay = crl::time(300); +constexpr auto kButtonExpandedHideDelay = crl::time(0); +constexpr auto kSizeForDownscale = 96; +constexpr auto kHoverScaleDuration = crl::time(200); +constexpr auto kHoverScale = 1.24; +constexpr auto kMaxReactionsScrollAtOnce = 2; + +[[nodiscard]] QPoint LocalPosition(not_null e) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return e->position().toPoint(); +#else // Qt >= 6.0 + return e->pos(); +#endif // Qt >= 6.0 +} + +[[nodiscard]] QSize CountMaxSizeWithMargins(style::margins margins) { + return QRect( + QPoint(), + st::reactionCornerSize + ).marginsAdded(margins).size(); +} + +[[nodiscard]] QSize CountOuterSize() { + return CountMaxSizeWithMargins(st::reactionCornerShadow); +} + +[[nodiscard]] int CornerImageSize(float64 scale) { + return int(base::SafeRound(st::reactionCornerImage * scale)); +} + +[[nodiscard]] int MainReactionSize() { + return style::ConvertScale(kSizeForDownscale); +} + +[[nodiscard]] std::shared_ptr CreateIcon( + not_null media, + int size, + int frame) { + Expects(media->loaded()); + + return std::make_shared(Lottie::IconDescriptor{ + .path = media->owner()->filepath(true), + .json = media->bytes(), + .sizeOverride = QSize(size, size), + .frame = frame, + }); +} + +} // namespace + +Button::Button( + Fn update, + ButtonParameters parameters, + Fn toggleExpanded, + Fn hide) +: _update(std::move(update)) +, _toggleExpanded(std::move(toggleExpanded)) +, _finalScale(ScaleForState(_state)) +, _collapsed(QPoint(), CountOuterSize()) +, _finalHeight(_collapsed.height()) +, _expandTimer([=] { _toggleExpanded(true); }) +, _hideTimer(hide) { + applyParameters(parameters, nullptr); +} + +Button::~Button() = default; + +void Button::expandWithoutCustom() { + applyState(State::Inside, _update); +} + +bool Button::isHidden() const { + return (_state == State::Hidden) && !_opacityAnimation.animating(); +} + +QRect Button::geometry() const { + return _geometry; +} + +int Button::expandedHeight() const { + return _expandedHeight; +} + +int Button::scroll() const { + return _scroll; +} + +int Button::scrollMax() const { + return _expandedInnerHeight - _expandedHeight; +} + +float64 Button::expandAnimationOpacity(float64 expandRatio) const { + return (_collapseType == CollapseType::Fade) + ? expandRatio + : 1.; +} + +int Button::expandAnimationScroll(float64 expandRatio) const { + return (_collapseType == CollapseType::Scroll && expandRatio < 1.) + ? std::clamp(int(base::SafeRound(expandRatio * _scroll)), 0, _scroll) + : _scroll; +} + +bool Button::expandUp() const { + return (_expandDirection == ExpandDirection::Up); +} + +bool Button::consumeWheelEvent(not_null e) { + const auto scrollMax = (_expandedInnerHeight - _expandedHeight); + if (_state != State::Inside + || scrollMax <= 0 + || !_geometry.contains(LocalPosition(e))) { + return false; + } + const auto delta = e->angleDelta(); + const auto horizontal = std::abs(delta.x()) > std::abs(delta.y()); + if (horizontal) { + return false; + } + const auto between = st::reactionCornerSkip; + const auto oneHeight = (st::reactionCornerSize.height() + between); + const auto max = oneHeight * kMaxReactionsScrollAtOnce; + const auto shift = std::clamp( + delta.y() * (expandUp() ? 1 : -1), + -max, + max); + _scroll = std::clamp(_scroll + shift, 0, scrollMax); + _update(_geometry); + e->accept(); + return true; +} + +void Button::applyParameters(ButtonParameters parameters) { + applyParameters(std::move(parameters), _update); +} + +void Button::applyParameters( + ButtonParameters parameters, + Fn update) { + const auto shift = parameters.center - _collapsed.center(); + _collapsed = _collapsed.translated(shift); + updateGeometry(update); + const auto inner = _geometry.marginsRemoved(st::reactionCornerShadow); + const auto active = inner.marginsAdded( + st::reactionCornerActiveAreaPadding + ).contains(parameters.pointer); + const auto inside = inner.contains(parameters.pointer) + || (active && (_state == State::Inside)); + if (_state != State::Inside && !_heightAnimation.animating()) { + updateExpandDirection(parameters); + } + const auto delayInside = inside && (_state != State::Inside); + if (!delayInside) { + _expandTimer.cancel(); + _lastGlobalPosition = std::nullopt; + } else { + const auto globalPositionChanged = _lastGlobalPosition + && (*_lastGlobalPosition != parameters.globalPointer); + if (globalPositionChanged || _state == State::Hidden) { + _expandTimer.callOnce(kButtonExpandDelay); + } + _lastGlobalPosition = parameters.globalPointer; + } + const auto wasInside = (_state == State::Inside); + const auto state = (inside && !delayInside) + ? State::Inside + : active + ? State::Active + : State::Shown; + applyState(state, update); + if (parameters.outside && _state == State::Shown) { + _hideTimer.callOnce(wasInside + ? kButtonExpandedHideDelay + : kButtonHideDelay); + } else { + _hideTimer.cancel(); + } +} + +void Button::updateExpandDirection(const ButtonParameters ¶meters) { + const auto maxAddedHeight = (parameters.reactionsCount - 1) + * (st::reactionCornerSize.height() + st::reactionCornerSkip) + + (parameters.reactionsCount > 1 ? 2 * st::reactionExpandedSkip : 0); + _expandedInnerHeight = _collapsed.height() + maxAddedHeight; + const auto addedHeight = std::min( + maxAddedHeight, + st::reactionCornerAddedHeightMax); + _expandedHeight = _collapsed.height() + addedHeight; + _scroll = std::clamp(_scroll, 0, scrollMax()); + if (parameters.reactionsCount < 2) { + return; + } + const auto up = (_collapsed.y() - addedHeight >= parameters.visibleTop) + || (_collapsed.y() + _collapsed.height() + addedHeight + > parameters.visibleBottom); + _expandDirection = up ? ExpandDirection::Up : ExpandDirection::Down; +} + +void Button::updateGeometry(Fn update) { + const auto added = int(base::SafeRound( + _heightAnimation.value(_finalHeight) + )) - _collapsed.height(); + if (!added && _state != State::Inside) { + _scroll = 0; + } + const auto geometry = _collapsed.marginsAdded({ + 0, + (_expandDirection == ExpandDirection::Up) ? added : 0, + 0, + (_expandDirection == ExpandDirection::Down) ? added : 0, + }); + if (_geometry != geometry) { + if (update) { + update(_geometry); + } + _geometry = geometry; + if (update) { + update(_geometry); + } + } +} + +void Button::applyState(State state) { + applyState(state, _update); +} + +void Button::applyState(State state, Fn update) { + if (state == State::Hidden) { + _expandTimer.cancel(); + _hideTimer.cancel(); + } + const auto finalHeight = (state == State::Hidden) + ? _heightAnimation.value(_finalHeight) + : (state == State::Inside) + ? _expandedHeight + : _collapsed.height(); + if (_finalHeight != finalHeight) { + if (state == State::Hidden) { + _heightAnimation.stop(); + } else { + if (!_heightAnimation.animating()) { + _collapseType = (_scroll < st::reactionCollapseFadeThreshold) + ? CollapseType::Scroll + : CollapseType::Fade; + } + _heightAnimation.start( + [=] { updateGeometry(_update); }, + _finalHeight, + finalHeight, + (state == State::Inside + ? kExpandDuration + : kCollapseDuration), + anim::easeOutCirc); + } + _finalHeight = finalHeight; + } + updateGeometry(update); + if (_state == state) { + return; + } + const auto duration = (state == State::Hidden || _state == State::Hidden) + ? kToggleDuration + : kActivateDuration; + const auto finalScale = ScaleForState(state); + _opacityAnimation.start( + [=] { _update(_geometry); }, + OpacityForScale(ScaleForState(_state)), + OpacityForScale(ScaleForState(state)), + duration, + anim::sineInOut); + if (state != State::Hidden && _finalScale != finalScale) { + _scaleAnimation.start( + [=] { _update(_geometry); }, + _finalScale, + finalScale, + duration, + anim::sineInOut); + _finalScale = finalScale; + } + _state = state; + _toggleExpanded(false); +} + +float64 Button::ScaleForState(State state) { + switch (state) { + case State::Hidden: return 1. / 3; + case State::Shown: return 2. / 3; + case State::Active: + case State::Inside: return 1.; + } + Unexpected("State in ReactionButton::ScaleForState."); +} + +float64 Button::OpacityForScale(float64 scale) { + return std::min( + ((scale - ScaleForState(State::Hidden)) + / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))), + 1.); +} + +float64 Button::currentScale() const { + return _scaleAnimation.value(_finalScale); +} + +float64 Button::currentOpacity() const { + return _opacityAnimation.value(OpacityForScale(ScaleForState(_state))); +} + +Manager::Manager( + QWidget *wheelEventsTarget, + rpl::producer uniqueLimitValue, + Fn buttonUpdate, + IconFactory iconFactory) +: _iconFactory(std::move(iconFactory)) +, _outer(CountOuterSize()) +, _inner(QRect({}, st::reactionCornerSize)) +, _cachedRound( + st::reactionCornerSize, + st::reactionCornerShadow, + _inner.width()) +, _uniqueLimit(std::move(uniqueLimitValue)) +, _buttonShowTimer([=] { showButtonDelayed(); }) +, _buttonUpdate(std::move(buttonUpdate)) { + _inner.translate(QRect({}, _outer).center() - _inner.center()); + + _emojiParts = _cachedRound.PrepareFramesCache(_outer); + _expandedBuffer = _cachedRound.PrepareImage(QSize( + _outer.width(), + _outer.height() + st::reactionCornerAddedHeightMax)); + if (wheelEventsTarget) { + stealWheelEvents(wheelEventsTarget); + } + + _uniqueLimit.changes( + ) | rpl::start_with_next([=] { + applyListFilters(); + }, _lifetime); + + _createChooseCallback = [=](ReactionId id) { + return [=] { + if (auto chosen = lookupChosen(id)) { + _chosen.fire(std::move(chosen)); + } + }; + }; +} + +ChosenReaction Manager::lookupChosen(const ReactionId &id) const { + auto result = ChosenReaction{ + .context = _buttonContext, + .id = id, + }; + const auto button = _button.get(); + const auto i = ranges::find(_icons, id, &ReactionIcons::id); + if (i == end(_icons) || !button) { + return result; + } + const auto &icon = *i; + if (const auto &appear = icon->appear; appear && appear->animating()) { + result.icon = CreateIcon( + icon->appearAnimation->activeMediaView().get(), + appear->width(), + appear->frameIndex()); + } else if (const auto &select = icon->select) { + result.icon = CreateIcon( + icon->selectAnimation->activeMediaView().get(), + select->width(), + select->frameIndex()); + } + const auto index = (i - begin(_icons)); + const auto between = st::reactionCornerSkip; + const auto oneHeight = (st::reactionCornerSize.height() + between); + const auto expanded = (_icons.size() > 1); + const auto skip = (expanded ? st::reactionExpandedSkip : 0); + const auto scroll = button->scroll(); + const auto local = skip + index * oneHeight - scroll; + const auto geometry = button->geometry(); + const auto top = button->expandUp() + ? (geometry.height() - local - _outer.height()) + : local; + const auto rect = QRect(geometry.topLeft() + QPoint(0, top), _outer); + const auto imageSize = int(base::SafeRound( + st::reactionCornerImage * kHoverScale)); + result.geometry = QRect( + rect.x() + (rect.width() - imageSize) / 2, + rect.y() + (rect.height() - imageSize) / 2, + imageSize, + imageSize); + return result; +} + +bool Manager::applyUniqueLimit() const { + const auto limit = _uniqueLimit.current(); + return _buttonContext + && (limit > 0) + && (_buttonAlreadyNotMineCount >= limit); +} + +void Manager::applyListFilters() { + const auto limited = applyUniqueLimit(); + auto icons = std::vector>(); + icons.reserve(_list.size()); + auto showPremiumLock = (ReactionIcons*)nullptr; + auto favoriteIndex = -1; + for (auto &icon : _list) { + const auto &id = icon.id; + const auto add = limited + ? _buttonAlreadyList.contains(id) + : id.emoji().isEmpty() + ? _filter.customAllowed + : (!_filter.allowed || _filter.allowed->contains(id.emoji())); + if (add) { + if (icon.premium + && !_allowSendingPremium + && !_buttonAlreadyList.contains(id)) { + if (_premiumPossible) { + showPremiumLock = &icon; + } else { + clearStateForHidden(icon); + } + } else { + icon.premiumLock = false; + if (id == _favorite) { + favoriteIndex = int(icons.size()); + } + icons.push_back(&icon); + } + } else { + clearStateForHidden(icon); + } + } + if (showPremiumLock) { + showPremiumLock->premiumLock = true; + icons.push_back(showPremiumLock); + } + if (favoriteIndex > 0) { + const auto first = begin(icons); + std::rotate(first, first + favoriteIndex, first + favoriteIndex + 1); + } + if (!limited && _filter.customAllowed && icons.size() > 1) { + icons.erase(begin(icons) + 1, end(icons)); + } + if (_icons == icons) { + return; + } + const auto selected = _selectedIcon; + setSelectedIcon(-1); + _icons = std::move(icons); + setSelectedIcon((selected < _icons.size()) ? selected : -1); + resolveMainReactionIcon(); +} + +void Manager::stealWheelEvents(not_null target) { + base::install_event_filter(target, [=](not_null e) { + if (e->type() != QEvent::Wheel + || !consumeWheelEvent(static_cast(e.get()))) { + return base::EventFilterResult::Continue; + } + Ui::SendSynteticMouseEvent(target, QEvent::MouseMove, Qt::NoButton); + return base::EventFilterResult::Cancel; + }); +} + +Manager::~Manager() = default; + +void Manager::updateButton(ButtonParameters parameters) { + if (parameters.cursorLeft) { + if (_menu) { + return; + } else if (_externalSelectorShown) { + setSelectedIcon(-1); + return; + } + } + const auto contextChanged = (_buttonContext != parameters.context); + if (contextChanged) { + setSelectedIcon(-1); + if (_button) { + _button->applyState(ButtonState::Hidden); + _buttonHiding.push_back(std::move(_button)); + } + _buttonShowTimer.cancel(); + _scheduledParameters = std::nullopt; + } + _buttonContext = parameters.context; + parameters.reactionsCount = _icons.size(); + if (!_buttonContext || !parameters.reactionsCount) { + return; + } else if (_button) { + _button->applyParameters(parameters); + if (_button->geometry().height() == _outer.height()) { + clearAppearAnimations(); + } + return; + } else if (parameters.outside) { + _buttonShowTimer.cancel(); + _scheduledParameters = std::nullopt; + return; + } + const auto globalPositionChanged = _scheduledParameters + && (_scheduledParameters->globalPointer != parameters.globalPointer); + const auto positionChanged = _scheduledParameters + && (_scheduledParameters->pointer != parameters.pointer); + _scheduledParameters = parameters; + if ((_buttonShowTimer.isActive() && positionChanged) + || globalPositionChanged) { + _buttonShowTimer.callOnce(kButtonShowDelay); + } +} + +void Manager::toggleExpanded(bool expanded) { + if (!_button || !_buttonContext) { + } else if (!expanded || (_filter.customAllowed && !applyUniqueLimit())) { + _expandSelectorRequests.fire({ + .context = _buttonContext, + .button = _button->geometry().marginsRemoved( + st::reactionCornerShadow), + .expanded = expanded, + }); + } else { + _button->expandWithoutCustom(); + } +} + +void Manager::setExternalSelectorShown(rpl::producer shown) { + std::move(shown) | rpl::start_with_next([=](bool shown) { + _externalSelectorShown = shown; + }, _lifetime); +} + +void Manager::showButtonDelayed() { + clearAppearAnimations(); + _button = std::make_unique