From ac02e2be9e8b201a048b7f5c9421949e9a50319d Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Nov 2020 17:10:08 +0300 Subject: [PATCH] Add FieldAutocomplete to ComposeControls. --- .../chat_helpers/field_autocomplete.cpp | 192 +++++++++++----- .../chat_helpers/field_autocomplete.h | 173 ++++----------- .../SourceFiles/history/history_widget.cpp | 4 +- .../history_view_compose_controls.cpp | 209 +++++++++++++++++- .../controls/history_view_compose_controls.h | 12 + .../view/history_view_replies_section.cpp | 32 ++- .../view/history_view_replies_section.h | 1 + .../view/history_view_scheduled_section.cpp | 18 +- .../support/support_autocomplete.h | 2 +- 9 files changed, 444 insertions(+), 199 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 41bce617a5..3c6100de80 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_stickers.h" #include "chat_helpers/send_context_menu.h" // SendMenu::FillSendMenu #include "chat_helpers/stickers_lottie.h" +#include "chat_helpers/message_field.h" // PrepareMentionTag. #include "mainwindow.h" #include "apiwrap.h" #include "main/main_session.h" @@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_single_player.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" +#include "ui/widgets/input_fields.h" #include "ui/image/image.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" @@ -39,15 +41,105 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +class FieldAutocomplete::Inner final + : public Ui::RpWidget + , private base::Subscriber { + +public: + struct ScrollTo { + int top; + int bottom; + }; + + Inner( + not_null controller, + not_null parent, + not_null mrows, + not_null hrows, + not_null brows, + not_null srows); + + void clearSel(bool hidden = false); + bool moveSel(int key); + bool chooseSelected(FieldAutocomplete::ChooseMethod method) const; + bool chooseAtIndex( + FieldAutocomplete::ChooseMethod method, + int index, + Api::SendOptions options = Api::SendOptions()) const; + + void setRecentInlineBotsInRows(int32 bots); + void rowsUpdated(); + + rpl::producer mentionChosen() const; + rpl::producer hashtagChosen() const; + rpl::producer + botCommandChosen() const; + rpl::producer stickerChosen() const; + rpl::producer scrollToRequested() const; + + void onParentGeometryChanged(); + +private: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + void enterEventHook(QEvent *e) override; + void leaveEventHook(QEvent *e) override; + + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + + void updateSelectedRow(); + void setSel(int sel, bool scroll = false); + void showPreview(); + void selectByMouse(QPoint global); + + QSize stickerBoundingBox() const; + void setupLottie(StickerSuggestion &suggestion); + void repaintSticker(not_null document); + std::shared_ptr getLottieRenderer(); + + const not_null _controller; + const not_null _parent; + const not_null _mrows; + const not_null _hrows; + const not_null _brows; + const not_null _srows; + rpl::lifetime _stickersLifetime; + std::weak_ptr _lottieRenderer; + base::unique_qptr _menu; + int _stickersPerRow = 1; + int _recentInlineBotsInRows = 0; + int _sel = -1; + int _down = -1; + std::optional _lastMousePosition; + bool _mouseSelection = false; + + bool _overDelete = false; + + bool _previewShown = false; + + rpl::event_stream _mentionChosen; + rpl::event_stream _hashtagChosen; + rpl::event_stream _botCommandChosen; + rpl::event_stream _stickerChosen; + rpl::event_stream _scrollToRequested; + + base::Timer _previewTimer; + +}; + FieldAutocomplete::FieldAutocomplete( QWidget *parent, not_null controller) : RpWidget(parent) , _controller(controller) , _scroll(this, st::mentionScroll) { - _scroll->setGeometry(rect()); + hide(); - using Inner = internal::FieldAutocompleteInner; + _scroll->setGeometry(rect()); _inner = _scroll->setOwnedWidget( object_ptr( @@ -76,6 +168,10 @@ FieldAutocomplete::FieldAutocomplete( &Inner::onParentGeometryChanged); } +not_null FieldAutocomplete::controller() const { + return _controller; +} + auto FieldAutocomplete::mentionChosen() const -> rpl::producer { return _inner->mentionChosen(); @@ -125,9 +221,9 @@ void FieldAutocomplete::showFiltered( if (query.isEmpty()) { _type = Type::Mentions; rowsUpdated( - internal::MentionRows(), - internal::HashtagRows(), - internal::BotCommandRows(), + MentionRows(), + HashtagRows(), + BotCommandRows(), base::take(_srows), false); return; @@ -171,7 +267,7 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) { base::take(_mrows), base::take(_hrows), base::take(_brows), - internal::StickerRows(), + StickerRows(), false); return; } @@ -203,7 +299,7 @@ inline int indexOfInFirstN(const T &v, const U &elem, int last) { } } -internal::StickerRows FieldAutocomplete::getStickerSuggestions() { +FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() { const auto list = _controller->session().data().stickers().getListByEmoji( _emoji, _stickersSeed @@ -211,7 +307,7 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() { auto result = ranges::view::all( list ) | ranges::view::transform([](not_null sticker) { - return internal::StickerSuggestion{ + return StickerSuggestion{ sticker, sticker->createMediaView() }; @@ -223,7 +319,7 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() { const auto i = ranges::find( result, suggestion.document, - &internal::StickerSuggestion::document); + &StickerSuggestion::document); if (i != end(result)) { i->animated = std::move(suggestion.animated); } @@ -233,10 +329,10 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() { void FieldAutocomplete::updateFiltered(bool resetScroll) { int32 now = base::unixtime::now(), recentInlineBots = 0; - internal::MentionRows mrows; - internal::HashtagRows hrows; - internal::BotCommandRows brows; - internal::StickerRows srows; + MentionRows mrows; + HashtagRows hrows; + BotCommandRows brows; + StickerRows srows; if (_emoji) { srows = getStickerSuggestions(); } else if (_type == Type::Mentions) { @@ -435,10 +531,10 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } void FieldAutocomplete::rowsUpdated( - internal::MentionRows &&mrows, - internal::HashtagRows &&hrows, - internal::BotCommandRows &&brows, - internal::StickerRows &&srows, + MentionRows &&mrows, + HashtagRows &&hrows, + BotCommandRows &&brows, + StickerRows &&srows, bool resetScroll) { if (mrows.empty() && hrows.empty() && brows.empty() && srows.empty()) { if (!isHidden()) { @@ -620,9 +716,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { return QWidget::eventFilter(obj, e); } -namespace internal { - -FieldAutocompleteInner::FieldAutocompleteInner( +FieldAutocomplete::Inner::Inner( not_null controller, not_null parent, not_null mrows, @@ -642,7 +736,7 @@ FieldAutocompleteInner::FieldAutocompleteInner( }, lifetime()); } -void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { +void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { Painter p(this); QRect r(e->rect()); @@ -841,11 +935,11 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerTop(), width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); } -void FieldAutocompleteInner::resizeEvent(QResizeEvent *e) { +void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) { _stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width())); } -void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) { +void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) { const auto globalPosition = e->globalPos(); if (!_lastMousePosition) { _lastMousePosition = globalPosition; @@ -857,7 +951,7 @@ void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) { selectByMouse(globalPosition); } -void FieldAutocompleteInner::clearSel(bool hidden) { +void FieldAutocomplete::Inner::clearSel(bool hidden) { _overDelete = false; _mouseSelection = false; _lastMousePosition = std::nullopt; @@ -868,7 +962,7 @@ void FieldAutocompleteInner::clearSel(bool hidden) { } } -bool FieldAutocompleteInner::moveSel(int key) { +bool FieldAutocomplete::Inner::moveSel(int key) { _mouseSelection = false; _lastMousePosition = std::nullopt; @@ -903,12 +997,12 @@ bool FieldAutocompleteInner::moveSel(int key) { return true; } -bool FieldAutocompleteInner::chooseSelected( +bool FieldAutocomplete::Inner::chooseSelected( FieldAutocomplete::ChooseMethod method) const { return chooseAtIndex(method, _sel); } -bool FieldAutocompleteInner::chooseAtIndex( +bool FieldAutocomplete::Inner::chooseAtIndex( FieldAutocomplete::ChooseMethod method, int index, Api::SendOptions options) const { @@ -955,11 +1049,11 @@ bool FieldAutocompleteInner::chooseAtIndex( return false; } -void FieldAutocompleteInner::setRecentInlineBotsInRows(int32 bots) { +void FieldAutocomplete::Inner::setRecentInlineBotsInRows(int32 bots) { _recentInlineBotsInRows = bots; } -void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) { +void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) { selectByMouse(e->globalPos()); if (e->button() == Qt::LeftButton) { if (_overDelete && _sel >= 0 && _sel < (_mrows->empty() ? _hrows->size() : _recentInlineBotsInRows)) { @@ -999,7 +1093,7 @@ void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) { } } -void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) { +void FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) { _previewTimer.cancel(); int32 pressed = _down; @@ -1017,7 +1111,7 @@ void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) { chooseSelected(FieldAutocomplete::ChooseMethod::ByClick); } -void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) { +void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) { if (_sel < 0 || _srows->empty() || _down >= 0) { return; } @@ -1040,11 +1134,11 @@ void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) { } } -void FieldAutocompleteInner::enterEventHook(QEvent *e) { +void FieldAutocomplete::Inner::enterEventHook(QEvent *e) { setMouseTracking(true); } -void FieldAutocompleteInner::leaveEventHook(QEvent *e) { +void FieldAutocomplete::Inner::leaveEventHook(QEvent *e) { setMouseTracking(false); if (_mouseSelection) { setSel(-1); @@ -1053,7 +1147,7 @@ void FieldAutocompleteInner::leaveEventHook(QEvent *e) { } } -void FieldAutocompleteInner::updateSelectedRow() { +void FieldAutocomplete::Inner::updateSelectedRow() { if (_sel >= 0) { if (_srows->empty()) { update(0, _sel * st::mentionHeight, width(), st::mentionHeight); @@ -1064,7 +1158,7 @@ void FieldAutocompleteInner::updateSelectedRow() { } } -void FieldAutocompleteInner::setSel(int sel, bool scroll) { +void FieldAutocomplete::Inner::setSel(int sel, bool scroll) { updateSelectedRow(); _sel = sel; updateSelectedRow(); @@ -1084,13 +1178,13 @@ void FieldAutocompleteInner::setSel(int sel, bool scroll) { } } -void FieldAutocompleteInner::rowsUpdated() { +void FieldAutocomplete::Inner::rowsUpdated() { if (_srows->empty()) { _stickersLifetime.destroy(); } } -auto FieldAutocompleteInner::getLottieRenderer() +auto FieldAutocomplete::Inner::getLottieRenderer() -> std::shared_ptr { if (auto result = _lottieRenderer.lock()) { return result; @@ -1100,7 +1194,7 @@ auto FieldAutocompleteInner::getLottieRenderer() return result; } -void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) { +void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) { const auto document = suggestion.document; suggestion.animated = ChatHelpers::LottiePlayerFromDocument( suggestion.documentMedia.get(), @@ -1115,13 +1209,13 @@ void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) { }, _stickersLifetime); } -QSize FieldAutocompleteInner::stickerBoundingBox() const { +QSize FieldAutocomplete::Inner::stickerBoundingBox() const { return QSize( st::stickerPanSize.width() - st::buttonRadius * 2, st::stickerPanSize.height() - st::buttonRadius * 2); } -void FieldAutocompleteInner::repaintSticker( +void FieldAutocomplete::Inner::repaintSticker( not_null document) { const auto i = ranges::find( *_srows, @@ -1140,7 +1234,7 @@ void FieldAutocompleteInner::repaintSticker( st::stickerPanSize.height()); } -void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) { +void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) { _mouseSelection = true; _lastMousePosition = globalPosition; const auto mouse = mapFromGlobal(globalPosition); @@ -1186,7 +1280,7 @@ void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) { } } -void FieldAutocompleteInner::onParentGeometryChanged() { +void FieldAutocomplete::Inner::onParentGeometryChanged() { const auto globalPosition = QCursor::pos(); if (rect().contains(mapFromGlobal(globalPosition))) { setMouseTracking(true); @@ -1196,7 +1290,7 @@ void FieldAutocompleteInner::onParentGeometryChanged() { } } -void FieldAutocompleteInner::showPreview() { +void FieldAutocomplete::Inner::showPreview() { if (_down >= 0 && _down < _srows->size()) { if (const auto w = App::wnd()) { w->showMediaPreview( @@ -1207,29 +1301,27 @@ void FieldAutocompleteInner::showPreview() { } } -auto FieldAutocompleteInner::mentionChosen() const +auto FieldAutocomplete::Inner::mentionChosen() const -> rpl::producer { return _mentionChosen.events(); } -auto FieldAutocompleteInner::hashtagChosen() const +auto FieldAutocomplete::Inner::hashtagChosen() const -> rpl::producer { return _hashtagChosen.events(); } -auto FieldAutocompleteInner::botCommandChosen() const +auto FieldAutocomplete::Inner::botCommandChosen() const -> rpl::producer { return _botCommandChosen.events(); } -auto FieldAutocompleteInner::stickerChosen() const +auto FieldAutocomplete::Inner::stickerChosen() const -> rpl::producer { return _stickerChosen.events(); } -auto FieldAutocompleteInner::scrollToRequested() const +auto FieldAutocomplete::Inner::scrollToRequested() const -> rpl::producer { return _scrollToRequested.events(); } - -} // namespace internal diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index e7d3f32056..8adbaa2c39 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class PopupMenu; class ScrollArea; +class InputField; } // namespace Ui namespace Lottie { @@ -32,42 +33,15 @@ class DocumentMedia; class CloudImageView; } // namespace Data -namespace internal { - -struct StickerSuggestion { - not_null document; - std::shared_ptr documentMedia; - std::unique_ptr animated; -}; - -struct MentionRow { - not_null user; - std::shared_ptr userpic; -}; - -struct BotCommandRow { - not_null user; - not_null command; - std::shared_ptr userpic; -}; - -using HashtagRows = std::vector; -using BotCommandRows = std::vector; -using StickerRows = std::vector; -using MentionRows = std::vector; - -class FieldAutocompleteInner; - -} // namespace internal - class FieldAutocomplete final : public Ui::RpWidget { - public: FieldAutocomplete( QWidget *parent, not_null controller); ~FieldAutocomplete(); + [[nodiscard]] not_null controller() const; + bool clearFilteredBotCommands(); void showFiltered( not_null peer, @@ -140,29 +114,54 @@ protected: void paintEvent(QPaintEvent *e) override; private: + class Inner; + friend class Inner; + + struct StickerSuggestion { + not_null document; + std::shared_ptr documentMedia; + std::unique_ptr animated; + }; + + struct MentionRow { + not_null user; + std::shared_ptr userpic; + }; + + struct BotCommandRow { + not_null user; + not_null command; + std::shared_ptr userpic; + }; + + using HashtagRows = std::vector; + using BotCommandRows = std::vector; + using StickerRows = std::vector; + using MentionRows = std::vector; + void animationCallback(); void hideFinish(); void updateFiltered(bool resetScroll = false); void recount(bool resetScroll = false); - internal::StickerRows getStickerSuggestions(); + StickerRows getStickerSuggestions(); const not_null _controller; QPixmap _cache; - internal::MentionRows _mrows; - internal::HashtagRows _hrows; - internal::BotCommandRows _brows; - internal::StickerRows _srows; + MentionRows _mrows; + HashtagRows _hrows; + BotCommandRows _brows; + StickerRows _srows; void rowsUpdated( - internal::MentionRows &&mrows, - internal::HashtagRows &&hrows, - internal::BotCommandRows &&brows, - internal::StickerRows &&srows, + MentionRows &&mrows, + HashtagRows &&hrows, + BotCommandRows &&brows, + StickerRows &&srows, bool resetScroll); object_ptr _scroll; - QPointer _inner; + QPointer _inner; ChatData *_chat = nullptr; UserData *_user = nullptr; @@ -186,100 +185,4 @@ private: Fn _moderateKeyActivateCallback; - friend class internal::FieldAutocompleteInner; - }; - -namespace internal { - -class FieldAutocompleteInner final - : public Ui::RpWidget - , private base::Subscriber { - -public: - struct ScrollTo { - int top; - int bottom; - }; - - FieldAutocompleteInner( - not_null controller, - not_null parent, - not_null mrows, - not_null hrows, - not_null brows, - not_null srows); - - void clearSel(bool hidden = false); - bool moveSel(int key); - bool chooseSelected(FieldAutocomplete::ChooseMethod method) const; - bool chooseAtIndex( - FieldAutocomplete::ChooseMethod method, - int index, - Api::SendOptions options = Api::SendOptions()) const; - - void setRecentInlineBotsInRows(int32 bots); - void rowsUpdated(); - - rpl::producer mentionChosen() const; - rpl::producer hashtagChosen() const; - rpl::producer - botCommandChosen() const; - rpl::producer stickerChosen() const; - rpl::producer scrollToRequested() const; - - void onParentGeometryChanged(); - -private: - void paintEvent(QPaintEvent *e) override; - void resizeEvent(QResizeEvent *e) override; - - void enterEventHook(QEvent *e) override; - void leaveEventHook(QEvent *e) override; - - void mousePressEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - - void updateSelectedRow(); - void setSel(int sel, bool scroll = false); - void showPreview(); - void selectByMouse(QPoint global); - - QSize stickerBoundingBox() const; - void setupLottie(StickerSuggestion &suggestion); - void repaintSticker(not_null document); - std::shared_ptr getLottieRenderer(); - - const not_null _controller; - const not_null _parent; - const not_null _mrows; - const not_null _hrows; - const not_null _brows; - const not_null _srows; - rpl::lifetime _stickersLifetime; - std::weak_ptr _lottieRenderer; - base::unique_qptr _menu; - int _stickersPerRow = 1; - int _recentInlineBotsInRows = 0; - int _sel = -1; - int _down = -1; - std::optional _lastMousePosition; - bool _mouseSelection = false; - - bool _overDelete = false; - - bool _previewShown = false; - - rpl::event_stream _mentionChosen; - rpl::event_stream _hashtagChosen; - rpl::event_stream _botCommandChosen; - rpl::event_stream _stickerChosen; - rpl::event_stream _scrollToRequested; - - base::Timer _previewTimer; - -}; - -} // namespace internal diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 2599b63ca5..07cd555fc0 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -286,7 +286,6 @@ HistoryWidget::HistoryWidget( _unreadMentions->installEventFilter(this); InitMessageField(controller, _field); - _fieldAutocomplete->hide(); _fieldAutocomplete->mentionChosen( ) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) { @@ -1216,6 +1215,9 @@ void HistoryWidget::orderWidgets() { _pinnedBar->raise(); } _topShadow->raise(); + if (_fieldAutocomplete) { + _fieldAutocomplete->raise(); + } if (_membersDropdown) { _membersDropdown->raise(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index ba90623650..8f164e2192 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -15,12 +15,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_section.h" #include "chat_helpers/tabbed_selector.h" +#include "chat_helpers/field_autocomplete.h" #include "core/application.h" #include "core/core_settings.h" #include "data/data_changes.h" #include "data/data_messages.h" #include "data/data_session.h" +#include "data/data_user.h" +#include "data/stickers/data_stickers.h" #include "data/data_web_page.h" +#include "storage/storage_account.h" #include "facades.h" #include "boxes/confirm_box.h" #include "history/history.h" @@ -515,6 +519,9 @@ ComposeControls::ComposeControls( st::historyComposeField, Ui::InputField::Mode::MultiLine, tr::lng_message_ph())) +, _autocomplete(std::make_unique( + parent, + window)) , _header(std::make_unique( _wrap.get(), &_window->session().data())) @@ -565,6 +572,12 @@ void ComposeControls::resizeToWidth(int width) { updateHeight(); } +void ComposeControls::setAutocompleteBoundingRect(QRect rect) { + if (_autocomplete) { + _autocomplete->setBoundings(rect); + } +} + rpl::producer ComposeControls::height() const { using namespace rpl::mappers; return rpl::conditional( @@ -619,6 +632,10 @@ rpl::producer ComposeControls::sendVoiceRequests() const { return _voiceRecordBar->sendVoiceRequests(); } +rpl::producer ComposeControls::sendCommandRequests() const { + return _sendCommandRequests.events(); +} + rpl::producer ComposeControls::editRequests() const { auto toValue = rpl::map([=] { return _header->queryToEdit(); }); auto filter = rpl::filter([=] { @@ -681,6 +698,21 @@ void ComposeControls::showFinished() { _voiceRecordBar->orderControls(); } +void ComposeControls::raisePanels() { + if (_autocomplete) { + _autocomplete->raise(); + } + if (_inlineResults) { + _inlineResults->raise(); + } + if (_tabbedPanel) { + _tabbedPanel->raise(); + } + if (_raiseEmojiSuggestions) { + _raiseEmojiSuggestions(); + } +} + void ComposeControls::showForGrab() { showFinished(); } @@ -708,7 +740,9 @@ void ComposeControls::setText(const TextWithTags &textWithTags) { } void ComposeControls::hidePanelsAnimated() { - //_fieldAutocomplete->hideAnimated(); + if (_autocomplete) { + _autocomplete->hideAnimated(); + } if (_tabbedPanel) { _tabbedPanel->hideAnimated(); } @@ -717,6 +751,36 @@ void ComposeControls::hidePanelsAnimated() { } } +void ComposeControls::checkAutocomplete() { + if (!_history) { + return; + } + + const auto peer = _history->peer; + const auto isInlineBot = false;// _inlineBot && !_inlineLookingUpBot; + const auto autocomplete = isInlineBot + ? AutocompleteQuery() + : ParseMentionHashtagBotCommandQuery(_field); + if (!autocomplete.query.isEmpty()) { + if (autocomplete.query[0] == '#' + && cRecentWriteHashtags().isEmpty() + && cRecentSearchHashtags().isEmpty()) { + peer->session().local().readRecentHashtagsAndBots(); + } else if (autocomplete.query[0] == '@' + && cRecentInlineBots().isEmpty()) { + peer->session().local().readRecentHashtagsAndBots(); + } else if (autocomplete.query[0] == '/' + && peer->isUser() + && !peer->asUser()->isBot()) { + return; + } + } + _autocomplete->showFiltered( + peer, + autocomplete.query, + autocomplete.fromStart); +} + void ComposeControls::init() { initField(); initTabbedSelector(); @@ -817,11 +881,12 @@ void ComposeControls::initField() { _field->setSubmitSettings(Core::App().settings().sendSubmitWay()); //Ui::Connect(_field, &Ui::InputField::submitted, [=] { send(); }); Ui::Connect(_field, &Ui::InputField::cancelled, [=] { escape(); }); - //Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); }); + Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); }); Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); }); //Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); }); Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); }); InitMessageField(_window, _field); + initAutocomplete(); const auto suggestions = Ui::Emoji::SuggestionsController::Init( _parent, _field, @@ -830,6 +895,112 @@ void ComposeControls::initField() { InitSpellchecker(_window, _field); } +void ComposeControls::initAutocomplete() { + const auto insertHashtagOrBotCommand = [=]( + const QString &string, + FieldAutocomplete::ChooseMethod method) { + // Send bot command at once, if it was not inserted by pressing Tab. + if (string.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) { + _sendCommandRequests.fire_copy(string); + setText( + _field->getTextWithTagsPart(_field->textCursor().position())); + } else { + _field->insertTag(string); + } + }; + const auto insertMention = [=](not_null user) { + auto replacement = QString(); + auto entityTag = QString(); + if (user->username.isEmpty()) { + _field->insertTag( + user->firstName.isEmpty() ? user->name : user->firstName, + PrepareMentionTag(user)); + } else { + _field->insertTag('@' + user->username); + } + }; + + _autocomplete->mentionChosen( + ) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) { + insertMention(data.user); + }, _autocomplete->lifetime()); + + _autocomplete->hashtagChosen( + ) | rpl::start_with_next([=](FieldAutocomplete::HashtagChosen data) { + insertHashtagOrBotCommand(data.hashtag, data.method); + }, _autocomplete->lifetime()); + + _autocomplete->botCommandChosen( + ) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) { + insertHashtagOrBotCommand(data.command, data.method); + }, _autocomplete->lifetime()); + + _autocomplete->stickerChosen( + ) | rpl::start_with_next([=](FieldAutocomplete::StickerChosen data) { + setText({}); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + //onCloudDraftSave(); // won't be needed if SendInlineBotResult will clear the cloud draft + _fileChosen.fire(FileChosen{ + .document = data.sticker, + .options = data.options, + }); + }, _autocomplete->lifetime()); + + //_autocomplete->setModerateKeyActivateCallback([=](int key) { + // return _keyboard->isHidden() + // ? false + // : _keyboard->moderateKeyActivate(key); + //}); + + _field->rawTextEdit()->installEventFilter(_autocomplete.get()); + + _window->session().data().botCommandsChanges( + ) | rpl::filter([=](not_null user) { + const auto peer = _history ? _history->peer.get() : nullptr; + return peer && (peer == user || !peer->isUser()); + }) | rpl::start_with_next([=](not_null user) { + if (_autocomplete->clearFilteredBotCommands()) { + checkAutocomplete(); + } + }, _autocomplete->lifetime()); + + _window->session().data().stickers().updated( + ) | rpl::start_with_next([=] { + updateStickersByEmoji(); + }, _autocomplete->lifetime()); + + QObject::connect( + _field->rawTextEdit(), + &QTextEdit::cursorPositionChanged, + _autocomplete.get(), + [=] { checkAutocomplete(); }, + Qt::QueuedConnection); +} + +void ComposeControls::updateStickersByEmoji() { + if (!_history) { + return; + } + const auto emoji = [&] { + const auto errorForStickers = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_stickers); + if (!isEditingMessage() && !errorForStickers) { + const auto &text = _field->getTextWithTags().text; + auto length = 0; + if (const auto emoji = Ui::Emoji::Find(text, &length)) { + if (text.size() <= length) { + return emoji; + } + } + } + return EmojiPtr(nullptr); + }(); + _autocomplete->showStickers(emoji); +} + void ComposeControls::fieldChanged() { if (/*!_inlineBot && */!_header->isEditingMessage() @@ -840,6 +1011,15 @@ void ComposeControls::fieldChanged() { if (showRecordButton()) { //_previewCancelled = false; } + InvokeQueued(_autocomplete.get(), [=] { + updateStickersByEmoji(); + }); +} + +void ComposeControls::fieldTabbed() { + if (!_autocomplete->isHidden()) { + _autocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab); + } } rpl::producer ComposeControls::sendActionUpdates() const { @@ -1136,10 +1316,16 @@ void ComposeControls::updateHeight() { void ComposeControls::editMessage(FullMsgId id) { cancelEditMessage(); _header->editMessage(id); + if (_autocomplete) { + InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); }); + } } void ComposeControls::cancelEditMessage() { _header->editMessage({}); + if (_autocomplete) { + InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); }); + } } void ComposeControls::replyToMessage(FullMsgId id) { @@ -1151,6 +1337,20 @@ void ComposeControls::cancelReplyMessage() { _header->replyToMessage({}); } +bool ComposeControls::handleCancelRequest() { + if (isEditingMessage()) { + cancelEditMessage(); + return true; + } else if (_autocomplete && !_autocomplete->isHidden()) { + _autocomplete->hideAnimated(); + return true; + } else if (replyingToMessage()) { + cancelReplyMessage(); + return true; + } + return false; +} + void ComposeControls::initWebpageProcess() { Expects(_history); const auto peer = _history->peer; @@ -1287,7 +1487,10 @@ void ComposeControls::initWebpageProcess() { Data::PeerUpdate::Flag::Rights ) | rpl::filter([=](const Data::PeerUpdate &update) { return (update.peer.get() == peer); - }) | rpl::start_with_next(checkPreview, lifetime); + }) | rpl::start_with_next([=] { + checkPreview(); + updateStickersByEmoji(); + }, lifetime); _window->session().downloaderTaskFinished( ) | rpl::filter([=] { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 926e3da329..d7c6cc3ce2 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_selector.h" class History; +class FieldAutocomplete; namespace ChatHelpers { class TabbedPanel; @@ -89,6 +90,7 @@ public: void move(int x, int y); void resizeToWidth(int width); + void setAutocompleteBoundingRect(QRect rect); [[nodiscard]] rpl::producer height() const; [[nodiscard]] int heightCurrent() const; @@ -96,6 +98,7 @@ public: [[nodiscard]] rpl::producer<> cancelRequests() const; [[nodiscard]] rpl::producer<> sendRequests() const; [[nodiscard]] rpl::producer sendVoiceRequests() const; + [[nodiscard]] rpl::producer sendCommandRequests() const; [[nodiscard]] rpl::producer editRequests() const; [[nodiscard]] rpl::producer<> attachRequests() const; [[nodiscard]] rpl::producer fileChosen() const; @@ -122,6 +125,7 @@ public: void showForGrab(); void showStarted(); void showFinished(); + void raisePanels(); void editMessage(FullMsgId id); void cancelEditMessage(); @@ -129,6 +133,8 @@ public: void replyToMessage(FullMsgId id); void cancelReplyMessage(); + bool handleCancelRequest(); + [[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const; [[nodiscard]] WebPageId webPageId() const; void setText(const TextWithTags &text); @@ -154,6 +160,7 @@ private: void initWebpageProcess(); void initWriteRestriction(); void initVoiceRecordBar(); + void initAutocomplete(); void updateSendButtonType(); void updateHeight(); void updateWrappingVisibility(); @@ -163,9 +170,12 @@ private: void paintBackground(QRect clip); void orderControls(); + void checkAutocomplete(); + void updateStickersByEmoji(); void escape(); void fieldChanged(); + void fieldTabbed(); void toggleTabbedSelectorMode(); void createTabbedPanel(); void setTabbedPanel(std::unique_ptr panel); @@ -194,6 +204,7 @@ private: const not_null _field; std::unique_ptr _inlineResults; std::unique_ptr _tabbedPanel; + std::unique_ptr _autocomplete; friend class FieldHeader; const std::unique_ptr _header; @@ -204,6 +215,7 @@ private: rpl::event_stream _photoChosen; rpl::event_stream _inlineResultChosen; rpl::event_stream _sendActionUpdates; + rpl::event_stream _sendCommandRequests; TextWithTags _localSavedText; TextUpdateEvents _textUpdateEvents; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index c10451a42e..9f931c52e2 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -253,6 +253,7 @@ RepliesWidget::RepliesWidget( setupScrollDownButton(); setupComposeControls(); + orderWidgets(); } RepliesWidget::~RepliesWidget() { @@ -263,6 +264,17 @@ RepliesWidget::~RepliesWidget() { _history->owner().repliesSendActionPainterRemoved(_history, _rootId); } +void RepliesWidget::orderWidgets() { + if (_topBar) { + _topBar->raise(); + } + if (_rootView) { + _rootView->raise(); + } + _topBarShadow->raise(); + _composeControls->raisePanels(); +} + void RepliesWidget::sendReadTillRequest() { if (!_root) { _readRequestPending = true; @@ -428,6 +440,18 @@ void RepliesWidget::setupComposeControls() { sendVoice(data.bytes, data.waveform, data.duration); }, lifetime()); + _composeControls->sendCommandRequests( + ) | rpl::start_with_next([=](const QString &command) { + if (showSlowmodeError()) { + return; + } + auto message = ApiWrap::MessageToSend(_history); + message.textWithTags = { command }; + message.action.replyTo = replyToId(); + session().api().sendMessage(std::move(message)); + finishSending(); + }, lifetime()); + const auto saveEditMsgRequestId = lifetime().make_state(0); _composeControls->editRequests( ) | rpl::start_with_next([=](auto data) { @@ -1474,6 +1498,7 @@ void RepliesWidget::updateControlsGeometry() { updateInnerVisibleArea(); } _composeControls->move(0, bottom - controlsHeight); + _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); updateScrollDownPosition(); } @@ -1598,12 +1623,7 @@ void RepliesWidget::listCancelRequest() { if (_inner && !_inner->getSelectedItems().empty()) { clearSelected(); return; - } - if (_composeControls->isEditingMessage()) { - _composeControls->cancelEditMessage(); - return; - } else if (_composeControls->replyingToMessage()) { - _composeControls->cancelReplyMessage(); + } else if (_composeControls->handleCancelRequest()) { return; } controller()->showBackFromStack(); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index e031369d5c..a22b5d92f9 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -186,6 +186,7 @@ private: [[nodiscard]] MsgId replyToId() const; [[nodiscard]] HistoryItem *lookupRoot() const; [[nodiscard]] bool computeAreComments() const; + void orderWidgets(); void pushReplyReturn(not_null item); void computeCurrentReplyReturn(); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 4465ac2ffd..0a2e506b52 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -180,6 +180,19 @@ void ScheduledWidget::setupComposeControls() { sendVoice(data.bytes, data.waveform, data.duration); }, lifetime()); + _composeControls->sendCommandRequests( + ) | rpl::start_with_next([=](const QString &command) { + const auto callback = [=](Api::SendOptions options) { + auto message = ApiWrap::MessageToSend(_history); + message.textWithTags = { command }; + message.action.options = options; + session().api().sendMessage(std::move(message)); + }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); + }, lifetime()); + const auto saveEditMsgRequestId = lifetime().make_state(0); _composeControls->editRequests( ) | rpl::start_with_next([=](auto data) { @@ -979,6 +992,7 @@ void ScheduledWidget::updateControlsGeometry() { updateInnerVisibleArea(); } _composeControls->move(0, bottom - controlsHeight); + _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); updateScrollDownPosition(); } @@ -1057,9 +1071,7 @@ void ScheduledWidget::listCancelRequest() { if (_inner && !_inner->getSelectedItems().empty()) { clearSelected(); return; - } - if (_composeControls->isEditingMessage()) { - _composeControls->cancelEditMessage(); + } else if (_composeControls->handleCancelRequest()) { return; } controller()->showBackFromStack(); diff --git a/Telegram/SourceFiles/support/support_autocomplete.h b/Telegram/SourceFiles/support/support_autocomplete.h index bcc961a98a..d2abc4671d 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.h +++ b/Telegram/SourceFiles/support/support_autocomplete.h @@ -35,7 +35,7 @@ struct Contact { QString lastName; }; -class Autocomplete : public Ui::RpWidget { +class Autocomplete final : public Ui::RpWidget { public: Autocomplete(QWidget *parent, not_null session);