diff --git a/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp b/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp index d52312d319..d685677d81 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "calls/group/calls_group_message_field.h" +#include "base/event_filter.h" #include "boxes/premium_preview_box.h" #include "calls/group/calls_group_messages.h" #include "chat_helpers/compose/compose_show.h" @@ -18,6 +19,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" #include "data/data_document.h" +#include "data/data_session.h" +#include "history/view/reactions/history_view_reactions_selector.h" +#include "history/view/reactions/history_view_reactions_strip.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/controls/emoji_button.h" @@ -39,8 +43,272 @@ namespace { constexpr auto kWarnLimit = 24; constexpr auto kErrorLimit = 99; +using Chosen = HistoryView::Reactions::ChosenReaction; + } // namespace +class ReactionPanel final { +public: + ReactionPanel( + not_null outer, + std::shared_ptr show, + rpl::producer fieldGeometry); + ~ReactionPanel(); + + [[nodiscard]] rpl::producer chosen() const; + + void show(); + void hide(); + void raise(); + void hideIfCollapsed(); + void collapse(); + +private: + struct Hiding; + + void create(); + void updateShowState(); + void fadeOutSelector(); + void startAnimation(); + + const not_null _outer; + const std::shared_ptr _show; + std::unique_ptr _parent; + std::unique_ptr _selector; + std::vector> _hiding; + rpl::event_stream _chosen; + Ui::Animations::Simple _showing; + rpl::variable _shownValue; + rpl::variable _fieldGeometry; + rpl::variable _expanded; + rpl::variable _shown = false; + +}; + +struct ReactionPanel::Hiding { + explicit Hiding(not_null parent) : widget(parent) { + } + + Ui::RpWidget widget; + Ui::Animations::Simple animation; + QImage frame; +}; + +ReactionPanel::ReactionPanel( + not_null outer, + std::shared_ptr show, + rpl::producer fieldGeometry) +: _outer(outer) +, _show(std::move(show)) +, _fieldGeometry(std::move(fieldGeometry)) { +} + +ReactionPanel::~ReactionPanel() = default; + +auto ReactionPanel::chosen() const -> rpl::producer { + return _chosen.events(); +} + +void ReactionPanel::show() { + if (_shown.current()) { + return; + } + create(); + if (!_selector) { + return; + } + const auto duration = st::defaultPanelAnimation.heightDuration + * st::defaultPopupMenu.showDuration; + _shown = true; + _showing.start([=] { updateShowState(); }, 0., 1., duration); + updateShowState(); + _parent->show(); +} + +void ReactionPanel::hide() { + if (!_selector) { + return; + } + _selector->beforeDestroy(); + if (!anim::Disabled()) { + fadeOutSelector(); + } + _shown = false; + _expanded = false; + _showing.stop(); + _selector = nullptr; + _parent = nullptr; +} + +void ReactionPanel::raise() { + if (_parent) { + _parent->raise(); + } +} + +void ReactionPanel::hideIfCollapsed() { + if (!_expanded.current()) { + hide(); + } +} + +void ReactionPanel::collapse() { + if (_expanded.current()) { + hide(); + show(); + } +} + +void ReactionPanel::create() { + auto reactions = Data::LookupPossibleReactions(&_show->session()); + if (reactions.recent.empty()) { + return; + } + _parent = std::make_unique(_outer); + _parent->show(); + + _parent->events() | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + const auto event = static_cast(e.get()); + if (event->button() == Qt::LeftButton) { + if (!_selector + || !_selector->geometry().contains(event->pos())) { + collapse(); + } + } + } + }, _parent->lifetime()); + + _selector = std::make_unique( + _parent.get(), + st::storiesReactionsPan, + _show, + std::move(reactions), + TextWithEntities(), + [=](bool fast) { hide(); }, + nullptr, // iconFactory + nullptr, // paused + true); + + _selector->chosen( + ) | rpl::start_with_next([=](Chosen reaction) { + if (reaction.id.custom() && !_show->session().premium()) { + ShowPremiumPreviewBox( + _show, + PremiumFeature::AnimatedEmoji); + } else { + _chosen.fire(std::move(reaction)); + hide(); + } + }, _selector->lifetime()); + + const auto desiredWidth = st::storiesReactionsWidth; + const auto maxWidth = desiredWidth * 2; + const auto width = _selector->countWidth(desiredWidth, maxWidth); + const auto margins = _selector->marginsForShadow(); + const auto categoriesTop = _selector->extendTopForCategoriesAndAbout( + width); + const auto full = margins.left() + width + margins.right(); + + _shownValue = 0.; + rpl::combine( + _fieldGeometry.value(), + _shownValue.value(), + _expanded.value() + ) | rpl::start_with_next([=](QRect field, float64 shown, bool expanded) { + const auto width = margins.left() + + _selector->countAppearedWidth(shown) + + margins.right(); + const auto available = field.y(); + const auto min = st::storiesReactionsBottomSkip + + st::reactStripHeight; + const auto max = min + + margins.top() + + categoriesTop + + st::storiesReactionsAddedTop; + const auto height = expanded ? std::min(available, max) : min; + const auto top = field.y() - height; + const auto shift = (width / 2); + const auto right = (field.x() + field.width() / 2 + shift); + _parent->setGeometry(QRect((right - width), top, full, height)); + const auto innerTop = height + - st::storiesReactionsBottomSkip + - st::reactStripHeight; + const auto maxAdded = innerTop - margins.top() - categoriesTop; + const auto added = std::min(maxAdded, st::storiesReactionsAddedTop); + _selector->setSpecialExpandTopSkip(added); + _selector->initGeometry(innerTop); + }, _selector->lifetime()); + + _selector->willExpand( + ) | rpl::start_with_next([=] { + _expanded = true; + + const auto raw = _parent.get(); + base::install_event_filter(raw, qApp, [=](not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + const auto event = static_cast(e.get()); + if (event->button() == Qt::LeftButton) { + if (!_selector + || !_selector->geometry().contains( + _parent->mapFromGlobal(event->globalPos()))) { + collapse(); + } + } + } + return base::EventFilterResult::Continue; + }); + }, _selector->lifetime()); + + _selector->escapes() | rpl::start_with_next([=] { + collapse(); + }, _selector->lifetime()); +} + +void ReactionPanel::fadeOutSelector() { + const auto geometry = Ui::MapFrom( + _outer, + _parent.get(), + _selector->geometry()); + _hiding.push_back(std::make_unique(_outer)); + const auto raw = _hiding.back().get(); + raw->frame = Ui::GrabWidgetToImage(_selector.get()); + raw->widget.setGeometry(geometry); + raw->widget.show(); + raw->widget.paintRequest( + ) | rpl::start_with_next([=] { + if (const auto opacity = raw->animation.value(0.)) { + auto p = QPainter(&raw->widget); + p.setOpacity(opacity); + p.drawImage(0, 0, raw->frame); + } + }, raw->widget.lifetime()); + Ui::PostponeCall(&raw->widget, [=] { + raw->animation.start([=] { + if (raw->animation.animating()) { + raw->widget.update(); + } else { + const auto i = ranges::find( + _hiding, + raw, + &std::unique_ptr::get); + if (i != end(_hiding)) { + _hiding.erase(i); + } + } + }, 1., 0., st::slideWrapDuration); + }); +} + +void ReactionPanel::updateShowState() { + const auto progress = _showing.value(_shown.current() ? 1. : 0.); + const auto opacity = 1.; + const auto appearing = _showing.animating(); + const auto toggling = false; + _shownValue = progress; + _selector->updateShowState(progress, opacity, appearing, toggling); +} + MessageField::MessageField( not_null parent, std::shared_ptr show, @@ -70,11 +338,51 @@ void MessageField::createControls(PeerData *peer) { _field->setDocumentMargin(4.); _field->setAdditionalMargin(style::ConvertScale(4) - 4); + _reactionPanel = std::make_unique( + _parent, + _show, + _wrap->geometryValue()); + _fieldFocused = _field->focusedChanges(); + _fieldEmpty = _field->changes() | rpl::map([field = _field] { + return field->getLastText().trimmed().isEmpty(); + }); + rpl::combine( + _fieldFocused.value(), + _fieldEmpty.value() + ) | rpl::start_with_next([=](bool focused, bool empty) { + if (!focused) { + _reactionPanel->hideIfCollapsed(); + } else if (empty) { + _reactionPanel->show(); + } else { + _reactionPanel->hide(); + } + }, _field->lifetime()); + + _reactionPanel->chosen( + ) | rpl::start_with_next([=](Chosen reaction) { + if (const auto customId = reaction.id.custom()) { + const auto document = _show->session().data().document(customId); + if (const auto sticker = document->sticker()) { + if (const auto alt = sticker->alt; !alt.isEmpty()) { + const auto length = int(alt.size()); + const auto data = Data::SerializeCustomEmojiId(customId); + const auto tag = Ui::InputField::CustomEmojiLink(data); + _submitted.fire({ alt, { { 0, length, tag } } }); + } + } + } else { + _submitted.fire({ reaction.id.emoji() }); + } + _reactionPanel->hide(); + }, _field->lifetime()); + const auto show = _show; const auto allow = [=](not_null emoji) { - return peer - ? Data::AllowEmojiWithoutPremium(peer, emoji) - : show->session().premium(); + if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) { + return true; + } + return false; }; InitMessageFieldHandlers({ .session = &show->session(), @@ -252,6 +560,8 @@ void MessageField::toggle(bool shown) { } else if (shown) { Assert(_width.current() > 0); Ui::SendPendingMoveResizeEvents(_wrap.get()); + } else if (Ui::InFocusChain(_field)) { + _parent->setFocus(); } _shown = shown; if (!anim::Disabled()) { @@ -291,6 +601,9 @@ void MessageField::raise() { if (_cache) { _cache->raise(); } + if (_reactionPanel) { + _reactionPanel->raise(); + } if (_emojiPanel) { _emojiPanel->raise(); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_message_field.h b/Telegram/SourceFiles/calls/group/calls_group_message_field.h index 48e2d8f754..b1f7eb4534 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_message_field.h +++ b/Telegram/SourceFiles/calls/group/calls_group_message_field.h @@ -25,6 +25,8 @@ class RpWidget; namespace Calls::Group { +class ReactionPanel; + class MessageField final { public: MessageField( @@ -64,6 +66,9 @@ private: Ui::SendButton *_send = nullptr; Ui::EmojiButton *_emojiToggle = nullptr; std::unique_ptr _emojiPanel; + std::unique_ptr _reactionPanel; + rpl::variable _fieldFocused; + rpl::variable _fieldEmpty = true; rpl::variable _width; rpl::variable _height; diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h index fe9226e0e3..a4d9d88a61 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h +++ b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h @@ -82,7 +82,6 @@ private: style::complex_color _messageBg; Ui::RoundRect _messageBgRect; - rpl::lifetime _effectsLifetime; Ui::Animations::Simple _topFadeAnimation; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index a909a2c4cd..96e3d303ea 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -254,7 +254,7 @@ PossibleItemReactionsRef LookupPossibleReactions( } } if (allowed.paidEnabled - && !added.contains(Data::ReactionId::Paid())) { + && !added.contains(ReactionId::Paid())) { result.recent.push_back(reactions->lookupPaid()); } } else { @@ -300,7 +300,7 @@ PossibleItemReactionsRef LookupPossibleReactions( } } if (!item->reactionsAreTags()) { - const auto toFront = [&](Data::ReactionId id) { + const auto toFront = [&](ReactionId id) { const auto i = ranges::find(result.recent, id, &Reaction::id); if (i != end(result.recent) && i != begin(result.recent)) { std::rotate(begin(result.recent), i, i + 1); @@ -308,12 +308,40 @@ PossibleItemReactionsRef LookupPossibleReactions( }; toFront(reactions->favoriteId()); if (paidInFront) { - toFront(Data::ReactionId::Paid()); + toFront(ReactionId::Paid()); } } return result; } +[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( + not_null session) { + auto result = PossibleItemReactionsRef(); + const auto reactions = &session->data().reactions(); + const auto &full = reactions->list(Reactions::Type::Active); + const auto &top = reactions->list(Reactions::Type::Top); + const auto &recent = reactions->list(Reactions::Type::Recent); + const auto premiumPossible = session->premiumPossible(); + auto added = base::flat_set(); + result.recent.reserve(full.size()); + for (const auto &reaction : ranges::views::concat(top, recent, full)) { + if (premiumPossible || !reaction.id.custom()) { + if (added.emplace(reaction.id).second) { + result.recent.push_back(&reaction); + } + } + } + result.customAllowed = premiumPossible; + const auto i = ranges::find( + result.recent, + reactions->favoriteId(), + &Reaction::id); + if (i != end(result.recent) && i != begin(result.recent)) { + std::rotate(begin(result.recent), i, i + 1); + } + return result; +} + PossibleItemReactions::PossibleItemReactions( const PossibleItemReactionsRef &other) : recent(other.recent | ranges::views::transform([](const auto &value) { @@ -574,8 +602,8 @@ DocumentData *Reactions::chooseGenericAnimation( const auto i = sticker ? ranges::find( _available, - ::Data::ReactionId{ { sticker->alt } }, - &::Data::Reaction::id) + ReactionId{ { sticker->alt } }, + &Reaction::id) : end(_available); if (i != end(_available) && i->aroundAnimation) { const auto view = i->aroundAnimation->createMediaView(); diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index e931ed20ed..da0d46d6ac 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -61,6 +61,8 @@ struct PossibleItemReactions { [[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( not_null item, bool paidInFront = false); +[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( + not_null session); struct MyTagInfo { ReactionId id; diff --git a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp index b35239841a..2644bc4a2f 100644 --- a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp @@ -36,7 +36,14 @@ StickerToast::StickerToast( not_null controller, not_null parent, Fn destroy) -: _controller(controller) +: StickerToast(controller->uiShow(), parent, std::move(destroy)) { +} + +StickerToast::StickerToast( + std::shared_ptr show, + not_null parent, + Fn destroy) +: _show(std::move(show)) , _parent(parent) , _destroy(std::move(destroy)) { } @@ -104,7 +111,7 @@ void StickerToast::requestSet() { Expects(_for != nullptr); if (const auto sticker = _for->sticker()) { - const auto api = &_controller->session().api(); + const auto api = &_show->session().api(); _setRequestId = api->request(MTPmessages_GetStickerSet( Data::InputStickerSet(sticker->set), MTP_int(0) // hash @@ -112,7 +119,7 @@ void StickerToast::requestSet() { _setRequestId = 0; result.match([&](const MTPDmessages_stickerSet &data) { data.vset().match([&](const MTPDstickerSet &data) { - const auto owner = &_controller->session().data(); + const auto owner = &_show->session().data(); showWithTitle(owner->stickers().getSetTitle(data)); }); }, [&](const MTPDmessages_stickerSetNotModified &) { @@ -125,7 +132,7 @@ void StickerToast::requestSet() { } void StickerToast::cancelRequest() { - _controller->session().api().request(base::take(_setRequestId)).cancel(); + _show->session().api().request(base::take(_setRequestId)).cancel(); } void StickerToast::showWithTitle(const QString &title) { @@ -219,29 +226,28 @@ void StickerToast::showWithTitle(const QString &title) { } button->setClickedCallback([=] { if (toSaved) { - _controller->showPeerHistory( - _controller->session().userPeerId(), - Window::SectionShow::Way::Forward); + if (const auto window = _show->resolveWindow()) { + window->showPeerHistory( + window->session().userPeerId(), + Window::SectionShow::Way::Forward); + } hideToast(); return; } else if (_section == Section::TopicIcon) { - Settings::ShowPremium(_controller, u"forum_topic_icon"_q); + if (const auto window = _show->resolveWindow()) { + Settings::ShowPremium(window, u"forum_topic_icon"_q); + } return; } - const auto id = _for->sticker()->set.id; + const auto id = _for->sticker()->set; const auto &sets = _for->owner().stickers().sets(); - const auto i = sets.find(id); + const auto i = sets.find(id.id); if (isEmoji && (i != end(sets)) && (i->second->flags & Data::StickersSetFlag::Installed)) { - ShowPremiumPreviewBox( - _controller, - PremiumFeature::AnimatedEmoji); + ShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji); } else { - _controller->show(Box( - _controller->uiShow(), - _for->sticker()->set, - setType)); + _show->show(Box(_show, id, setType)); } hideToast(); }); diff --git a/Telegram/SourceFiles/history/view/history_view_sticker_toast.h b/Telegram/SourceFiles/history/view/history_view_sticker_toast.h index 75cd372e86..eb8da4466b 100644 --- a/Telegram/SourceFiles/history/view/history_view_sticker_toast.h +++ b/Telegram/SourceFiles/history/view/history_view_sticker_toast.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Ui { class Show; class RpWidget; @@ -30,6 +34,10 @@ public: not_null controller, not_null parent, Fn destroy); + StickerToast( + std::shared_ptr show, + not_null parent, + Fn destroy); ~StickerToast(); enum class Section { @@ -50,7 +58,7 @@ private: void setupLottiePreview(not_null widget, int size); void clearHiddenHiding(); - const not_null _controller; + const std::shared_ptr _show; const not_null _parent; Section _section = {}; style::Toast _st; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index e65443b429..2d7af73be3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -735,34 +735,6 @@ void WeatherView::cacheBackground() { return { QString() + QChar(10084) }; } -[[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions( - not_null session) { - auto result = Data::PossibleItemReactionsRef(); - const auto reactions = &session->data().reactions(); - const auto &full = reactions->list(Data::Reactions::Type::Active); - const auto &top = reactions->list(Data::Reactions::Type::Top); - const auto &recent = reactions->list(Data::Reactions::Type::Recent); - const auto premiumPossible = session->premiumPossible(); - auto added = base::flat_set(); - result.recent.reserve(full.size()); - for (const auto &reaction : ranges::views::concat(top, recent, full)) { - if (premiumPossible || !reaction.id.custom()) { - if (added.emplace(reaction.id).second) { - result.recent.push_back(&reaction); - } - } - } - result.customAllowed = premiumPossible; - const auto i = ranges::find( - result.recent, - reactions->favoriteId(), - &Data::Reaction::id); - if (i != end(result.recent) && i != begin(result.recent)) { - std::rotate(begin(result.recent), i, i + 1); - } - return result; -} - HistoryView::Context ReactionView::elementContext() { return HistoryView::Context::ContactPreview; } @@ -907,7 +879,7 @@ void Reactions::Panel::attachToReactionButton( } void Reactions::Panel::create() { - auto reactions = LookupPossibleReactions( + auto reactions = Data::LookupPossibleReactions( &_controller->uiShow()->session()); if (reactions.recent.empty()) { return; @@ -1153,7 +1125,7 @@ auto Reactions::attachToMenu( desiredPosition, st::storiesReactionsPan, show, - LookupPossibleReactions(&show->session()), + Data::LookupPossibleReactions(&show->session()), TextWithEntities()); if (!result) { return result.error();