diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 3550707494..25a9a27446 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -40,4 +40,13 @@ struct SetHistoryArgs { rpl::producer> writeRestriction; }; +struct ReplyNextRequest { + enum class Direction { + Next, + Previous, + }; + const FullMsgId replyId; + const Direction direction; +}; + } // namespace HistoryView::Controls 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 097504ce5a..85da5e24df 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_controls.h" #include "base/event_filter.h" +#include "base/platform/base_platform_info.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -64,6 +65,11 @@ constexpr auto kMouseEvents = { QEvent::MouseButtonRelease }; +constexpr auto kCommonModifiers = 0 + | Qt::ShiftModifier + | Qt::MetaModifier + | Qt::ControlModifier; + using FileChosen = ComposeControls::FileChosen; using PhotoChosen = ComposeControls::PhotoChosen; using MessageToEdit = ComposeControls::MessageToEdit; @@ -722,6 +728,11 @@ auto ComposeControls::editLastMessageRequests() const return _editLastMessageRequests.events(); } +auto ComposeControls::replyNextRequests() const +-> rpl::producer { + return _replyNextRequests.events(); +} + auto ComposeControls::sendContentRequests(SendRequestType requestType) const { auto filter = rpl::filter([=] { const auto type = (_mode == Mode::Normal) @@ -1077,6 +1088,37 @@ void ComposeControls::initKeyHandler() { _scrollKeyEvents.fire(std::move(keyEvent)); } }, _wrap->lifetime()); + + base::install_event_filter(_wrap.get(), _field, [=](not_null e) { + using Result = base::EventFilterResult; + if (e->type() != QEvent::KeyPress) { + return Result::Continue; + } + const auto k = static_cast(e.get()); + + if ((k->modifiers() & kCommonModifiers) == Qt::ControlModifier) { + const auto isUp = (k->key() == Qt::Key_Up); + const auto isDown = (k->key() == Qt::Key_Down); + if (isUp || isDown) { + if (Platform::IsMac()) { + // Cmd + Up is used instead of Home. + if ((isUp && (!_field->textCursor().atStart())) + // Cmd + Down is used instead of End. + || (isDown && (!_field->textCursor().atEnd()))) { + return Result::Continue; + } + } + _replyNextRequests.fire({ + .replyId = replyingToMessage(), + .direction = (isDown + ? ReplyNextRequest::Direction::Next + : ReplyNextRequest::Direction::Previous) + }); + return Result::Cancel; + } + } + return Result::Continue; + }); } void ComposeControls::initField() { 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 fbb2975e89..c107a3f0c0 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -82,6 +82,7 @@ public: using VoiceToSend = Controls::VoiceToSend; using SendActionUpdate = Controls::SendActionUpdate; using SetHistoryArgs = Controls::SetHistoryArgs; + using ReplyNextRequest = Controls::ReplyNextRequest; using FieldHistoryAction = Ui::InputField::HistoryAction; enum class Mode { @@ -125,6 +126,8 @@ public: -> rpl::producer>; [[nodiscard]] auto editLastMessageRequests() const -> rpl::producer>; + [[nodiscard]] auto replyNextRequests() const + -> rpl::producer; using MimeDataHook = Fn data, @@ -297,6 +300,7 @@ private: rpl::event_stream> _scrollKeyEvents; rpl::event_stream> _editLastMessageRequests; rpl::event_stream<> _attachRequests; + rpl::event_stream _replyNextRequests; TextUpdateEvents _textUpdateEvents = TextUpdateEvents() | TextUpdateEvent::SaveDraft diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index fe21642b30..45048eeb3f 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -525,6 +525,11 @@ void ListWidget::updateHighlightedMessage() { _highlightedMessageId = FullMsgId(); } +void ListWidget::clearHighlightedMessage() { + _highlightedMessageId = FullMsgId(); + updateHighlightedMessage(); +} + void ListWidget::checkUnreadBarCreation() { if (!_bar.element) { if (auto data = _delegate->listMessagesBar(_items); data.bar.element) { @@ -2759,6 +2764,49 @@ rpl::producer ListWidget::readMessageRequested() const { return _requestedToReadMessage.events(); } +rpl::producer ListWidget::showMessageRequested() const { + return _requestedToShowMessage.events(); +} + +void ListWidget::replyNextMessage(FullMsgId fullId, bool next) { + const auto reply = [&](Element *view) { + if (view) { + const auto newFullId = view->data()->fullId(); + replyToMessageRequestNotify(newFullId); + _requestedToShowMessage.fire_copy(newFullId); + } else { + replyToMessageRequestNotify(FullMsgId()); + clearHighlightedMessage(); + } + }; + const auto replyFirst = [&] { + reply(next ? nullptr : _items.back().get()); + }; + if (!fullId) { + replyFirst(); + return; + } + + auto proj = [&](not_null view) { + return view->data()->fullId() == fullId; + }; + const auto &list = ranges::view::reverse(_items); + const auto it = ranges::find_if(list, std::move(proj)); + if (it == end(list)) { + replyFirst(); + return; + } else { + const auto nextIt = it + (next ? -1 : 1); + if (nextIt == end(list)) { + return; + } else if (next && (it == begin(list))) { + reply(nullptr); + } else { + reply(nextIt->get()); + } + } +} + ListWidget::~ListWidget() = default; void ConfirmDeleteSelectedItems(not_null widget) { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 95a2174cd0..8d6cf9169b 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -211,6 +211,8 @@ public: [[nodiscard]] rpl::producer replyToMessageRequested() const; void replyToMessageRequestNotify(FullMsgId item); [[nodiscard]] rpl::producer readMessageRequested() const; + [[nodiscard]] rpl::producer showMessageRequested() const; + void replyNextMessage(FullMsgId fullId, bool next = true); // ElementDelegate interface. Context elementContext() override; @@ -448,6 +450,7 @@ private: void scrollToAnimationCallback(FullMsgId attachToId, int relativeTo); void updateHighlightedMessage(); + void clearHighlightedMessage(); // This function finds all history items that are displayed and calls template method // for each found message (in given direction) in the passed history with passed top offset. @@ -555,6 +558,7 @@ private: rpl::event_stream _requestedToEditMessage; rpl::event_stream _requestedToReplyToMessage; rpl::event_stream _requestedToReadMessage; + rpl::event_stream _requestedToShowMessage; rpl::lifetime _viewerLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 9044de419c..e76676f225 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -221,6 +221,13 @@ RepliesWidget::RepliesWidget( replyToMessage(fullId); }, _inner->lifetime()); + _inner->showMessageRequested( + ) | rpl::start_with_next([=](auto fullId) { + if (const auto item = session().data().message(fullId)) { + showAtPosition(item->position()); + } + }, _inner->lifetime()); + _composeControls->sendActionUpdates( ) | rpl::start_with_next([=](ComposeControls::SendActionUpdate &&data) { session().sendProgressManager().update( @@ -500,6 +507,14 @@ void RepliesWidget::setupComposeControls() { } }, lifetime()); + _composeControls->replyNextRequests( + ) | rpl::start_with_next([=](ComposeControls::ReplyNextRequest &&data) { + using Direction = ComposeControls::ReplyNextRequest::Direction; + _inner->replyNextMessage( + data.replyId, + data.direction == Direction::Next); + }, lifetime()); + _composeControls->setMimeDataHook([=]( not_null data, Ui::InputField::MimeAction action) {