/* 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 "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" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "core/ui_integration.h" #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_app_config.h" #include "main/main_session.h" #include "ui/controls/emoji_button.h" #include "ui/controls/send_button.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/scroll_area.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "ui/userpic_view.h" #include "styles/style_calls.h" #include "styles/style_chat_helpers.h" #include "styles/style_chat.h" #include "styles/style_media_view.h" namespace Calls::Group { namespace { 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::on_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::on_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::on_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::on_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::on_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::on_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, PeerData *peer) : _parent(parent) , _show(std::move(show)) , _wrap(std::make_unique(_parent)) , _limit(_show->session().appConfig().groupCallMessageLengthLimit()) { createControls(peer); } MessageField::~MessageField() = default; void MessageField::createControls(PeerData *peer) { setupBackground(); const auto &st = st::storiesComposeControls; _field = Ui::CreateChild( _wrap.get(), st.field, Ui::InputField::Mode::MultiLine, tr::lng_message_ph()); _field->setMaxLength(_limit + kErrorLimit); _field->setMinHeight( st::historySendSize.height() - 2 * st::historySendPadding); _field->setMaxHeight(st::historyComposeFieldMaxHeight); _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::on_next([=](bool focused, bool empty) { if (!focused) { _reactionPanel->hideIfCollapsed(); } else if (empty) { _reactionPanel->show(); } else { _reactionPanel->hide(); } }, _field->lifetime()); _reactionPanel->chosen( ) | rpl::on_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) { if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) { return true; } return false; }; InitMessageFieldHandlers({ .session = &show->session(), .show = show, .field = _field, .customEmojiPaused = [=] { return show->paused(ChatHelpers::PauseReason::Layer); }, .allowPremiumEmoji = allow, .fieldStyle = &st.files.caption, .allowMarkdownTags = { Ui::InputField::kTagBold, Ui::InputField::kTagItalic, Ui::InputField::kTagUnderline, Ui::InputField::kTagStrikeOut, Ui::InputField::kTagSpoiler, }, }); Ui::Emoji::SuggestionsController::Init( _parent, _field, &_show->session(), { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow, .st = &st.suggestions, }); _send = Ui::CreateChild(_wrap.get(), st.send); _send->show(); using Selector = ChatHelpers::TabbedSelector; _emojiPanel = std::make_unique( _parent, ChatHelpers::TabbedPanelDescriptor{ .ownedSelector = object_ptr( nullptr, ChatHelpers::TabbedSelectorDescriptor{ .show = _show, .st = st.tabbed, .level = ChatHelpers::PauseReason::Layer, .mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly, .features = { .stickersSettings = false, .openStickerSets = false, }, }), }); const auto panel = _emojiPanel.get(); panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, st::emojiPanMinHeight); panel->hide(); panel->selector()->setCurrentPeer(peer); panel->selector()->emojiChosen( ) | rpl::on_next([=](ChatHelpers::EmojiChosen data) { Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji); }, lifetime()); panel->selector()->customEmojiChosen( ) | rpl::on_next([=](ChatHelpers::FileChosen data) { const auto info = data.document->sticker(); if (info && info->setType == Data::StickersType::Emoji && !_show->session().premium()) { ShowPremiumPreviewBox( _show, PremiumFeature::AnimatedEmoji); } else { Data::InsertCustomEmoji(_field, data.document); } }, lifetime()); _emojiToggle = Ui::CreateChild(_wrap.get(), st.emoji); _emojiToggle->show(); _emojiToggle->installEventFilter(panel); _emojiToggle->addClickHandler([=] { panel->toggleAnimated(); }); _width.value( ) | rpl::filter( rpl::mappers::_1 > 0 ) | rpl::on_next([=](int newWidth) { const auto fieldWidth = newWidth - st::historySendPadding - _emojiToggle->width() - _send->width(); _field->resizeToWidth(fieldWidth); _field->moveToLeft( st::historySendPadding, st::historySendPadding, newWidth); updateWrapSize(newWidth); }, _lifetime); rpl::combine( _width.value(), _field->heightValue() ) | rpl::on_next([=](int width, int height) { if (width <= 0) { return; } const auto minHeight = st::historySendSize.height() - 2 * st::historySendPadding; _send->moveToRight(0, height - minHeight, width); _emojiToggle->moveToRight(_send->width(), height - minHeight, width); updateWrapSize(); }, _lifetime); _field->cancelled() | rpl::on_next([=] { _closeRequests.fire({}); }, _lifetime); const auto updateLimitPosition = [=](QSize parent, QSize label) { const auto skip = st::historySendPadding; return QPoint(parent.width() - label.width() - skip, skip); }; Ui::AddLengthLimitLabel(_field, _limit, { .customParent = _wrap.get(), .customUpdatePosition = updateLimitPosition, }); rpl::merge( _field->submits() | rpl::to_empty, _send->clicks() | rpl::to_empty ) | rpl::on_next([=] { auto text = _field->getTextWithTags(); if (text.text.size() <= _limit) { _submitted.fire(std::move(text)); } }, _lifetime); } void MessageField::updateEmojiPanelGeometry() { const auto global = _emojiToggle->mapToGlobal({ 0, 0 }); const auto local = _parent->mapFromGlobal(global); _emojiPanel->moveBottomRight( local.y(), local.x() + _emojiToggle->width() * 3); } void MessageField::setupBackground() { _wrap->paintRequest() | rpl::on_next([=] { const auto radius = st::historySendSize.height() / 2.; auto p = QPainter(_wrap.get()); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::storiesComposeBg); p.drawRoundedRect(_wrap->rect(), radius, radius); }, _lifetime); } void MessageField::resizeToWidth(int newWidth) { _width = newWidth; if (_wrap->isHidden()) { Ui::SendPendingMoveResizeEvents(_wrap.get()); } updateEmojiPanelGeometry(); } void MessageField::move(int x, int y) { _wrap->move(x, y); if (_cache) { _cache->move(x, y); } } void MessageField::toggle(bool shown) { if (_shown == shown) { return; } else if (shown) { Assert(_width.current() > 0); Ui::SendPendingMoveResizeEvents(_wrap.get()); } else if (Ui::InFocusChain(_field)) { _parent->setFocus(); } _shown = shown; if (!anim::Disabled()) { if (!_cache) { auto image = Ui::GrabWidgetToImage(_wrap.get()); _cache = std::make_unique(_parent); const auto raw = _cache.get(); raw->paintRequest() | rpl::on_next([=] { auto p = QPainter(raw); auto hq = PainterHighQualityEnabler(p); const auto scale = raw->height() / float64(_wrap->height()); const auto target = _wrap->rect(); const auto center = target.center(); p.translate(center); p.scale(scale, scale); p.translate(-center); p.drawImage(target, image); }, raw->lifetime()); raw->show(); raw->move(_wrap->pos()); raw->resize(_wrap->width(), 0); _wrap->hide(); } _shownAnimation.start( [=] { shownAnimationCallback(); }, shown ? 0. : 1., shown ? 1. : 0., st::slideWrapDuration, anim::easeOutCirc); } shownAnimationCallback(); } void MessageField::raise() { _wrap->raise(); if (_cache) { _cache->raise(); } if (_reactionPanel) { _reactionPanel->raise(); } if (_emojiPanel) { _emojiPanel->raise(); } } void MessageField::updateWrapSize(int widthOverride) { const auto width = widthOverride ? widthOverride : _wrap->width(); const auto height = _field->height() + 2 * st::historySendPadding; _wrap->resize(width, height); updateHeight(); } void MessageField::updateHeight() { _height = int(base::SafeRound( _shownAnimation.value(_shown ? 1. : 0.) * _wrap->height())); } void MessageField::shownAnimationCallback() { updateHeight(); if (_shownAnimation.animating()) { Assert(_cache != nullptr); _cache->resize(_cache->width(), _height.current()); _cache->update(); } else if (_shown) { _cache = nullptr; _wrap->show(); _field->setFocusFast(); } else { _closed.fire({}); } } int MessageField::height() const { return _height.current(); } rpl::producer MessageField::heightValue() const { return _height.value(); } rpl::producer MessageField::submitted() const { return _submitted.events(); } rpl::producer<> MessageField::closeRequests() const { return _closeRequests.events(); } rpl::producer<> MessageField::closed() const { return _closed.events(); } rpl::lifetime &MessageField::lifetime() { return _lifetime; } } // namespace Calls::Group