From 9c151ca151b20a7400e13c48064f853c111cd243 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 5 Jan 2024 11:25:45 +0400 Subject: [PATCH] Allow filtering Saved Messages search by tags. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/dialogs/dialogs.style | 3 + .../dialogs/dialogs_inner_widget.cpp | 115 +++++++- .../dialogs/dialogs_inner_widget.h | 8 + .../dialogs/dialogs_search_tags.cpp | 246 ++++++++++++++++++ .../SourceFiles/dialogs/dialogs_search_tags.h | 78 ++++++ .../SourceFiles/dialogs/dialogs_widget.cpp | 49 +++- Telegram/SourceFiles/dialogs/dialogs_widget.h | 4 + .../view/reactions/history_view_reactions.cpp | 46 ++-- .../view/reactions/history_view_reactions.h | 3 + 10 files changed, 518 insertions(+), 36 deletions(-) create mode 100644 Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp create mode 100644 Telegram/SourceFiles/dialogs/dialogs_search_tags.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 4f9db79398..59c7013400 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -609,6 +609,8 @@ PRIVATE dialogs/dialogs_row.h dialogs/dialogs_search_from_controllers.cpp dialogs/dialogs_search_from_controllers.h + dialogs/dialogs_search_tags.cpp + dialogs/dialogs_search_tags.h dialogs/dialogs_widget.cpp dialogs/dialogs_widget.h dialogs/ui/dialogs_layout.cpp diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 055675b2d4..c45ea636fc 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -623,3 +623,6 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) { searchedBarHeight: 32px; searchedBarFont: normalFont; searchedBarPosition: point(17px, 7px); + +dialogsSearchTagSkip: point(8px, 4px); +dialogsSearchTagBottom: 10px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index afbf0ca77a..a9f1d1b419 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_indexed_list.h" #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" +#include "dialogs/dialogs_search_tags.h" #include "history/history.h" #include "history/history_item.h" #include "core/shortcuts.h" @@ -40,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_message_reactions.h" #include "data/data_saved_messages.h" #include "data/data_stories.h" #include "data/stickers/data_stickers.h" @@ -477,18 +479,26 @@ int InnerWidget::peerSearchOffset() const { + st::searchedBarHeight; } -int InnerWidget::searchedOffset() const { - auto result = peerSearchOffset(); +int InnerWidget::searchInChatOffset() const { + auto result = peerSearchOffset() - st::searchedBarHeight; if (!_peerSearchResults.empty()) { result += (_peerSearchResults.size() * st::dialogsRowHeight) + st::searchedBarHeight; } - result += searchInChatSkip(); return result; } +int InnerWidget::searchedOffset() const { + return searchInChatOffset() + + searchInChatSkip() + + st::searchedBarHeight; +} + int InnerWidget::searchInChatSkip() const { auto result = 0; + if (_searchTags) { + result += _searchTags->height(); + } if (_searchInChat) { result += st::searchedBarHeight + st::dialogsSearchInHeight; } @@ -1111,12 +1121,20 @@ void InnerWidget::paintSearchInChat( auto height = searchInChatSkip(); auto top = 0; + if (_searchTags) { + const auto height = _searchTags->height(); + p.fillRect(0, top, width(), height, currentBg()); + const auto position = QPoint(_searchTagsLeft, 0); + _searchTags->paint(p, position, context.now, context.paused); + top += height; + } p.setFont(st::searchedBarFont); if (_searchInChat) { - top += st::searchedBarHeight; - p.fillRect(0, 0, width(), top, st::searchedBarBg); + const auto bar = st::searchedBarHeight; + p.fillRect(0, top, width(), top + bar, st::searchedBarBg); p.setPen(st::searchedBarFg); - p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now)); + p.drawTextLeft(st::searchedBarPosition.x(), top + st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now)); + top += bar; } auto fullRect = QRect(0, top, width(), height - top); p.fillRect(fullRect, currentBg()); @@ -1276,6 +1294,21 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { _lastMousePosition = globalPosition; _lastRowLocalMouseX = local.x(); + const auto tagBase = QPoint(_searchTagsLeft, searchInChatOffset()); + const auto tagPoint = local - tagBase; + const auto inTags = _searchTags + && QRect( + tagBase, + QSize(width() - 2 * _searchTagsLeft, _searchTags->height()) + ).contains(local); + const auto tagLink = inTags + ? _searchTags->lookupHandler(tagPoint) + : nullptr; + ClickHandler::setActive(tagLink); + if (inTags) { + setCursor(tagLink ? style::cur_pointer : style::cur_default); + } + const auto w = width(); const auto mouseY = local.y(); clearIrrelevantState(); @@ -1370,7 +1403,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { updateSelectedRow(); } } - if (wasSelected != isSelected()) { + if (!inTags && wasSelected != isSelected()) { setCursor(wasSelected ? style::cur_default : style::cur_pointer); } } @@ -1452,6 +1485,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { QSize(width(), _st->height), row->repaint()); } + ClickHandler::pressed(); if (anim::Disabled() && (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) { mousePressReleased(e->globalPos(), e->button(), e->modifiers()); @@ -1743,6 +1777,9 @@ void InnerWidget::mousePressReleased( chooseRow(modifiers, pressedTopicRootId); } } + if (auto activated = ClickHandler::unpressed()) { + ActivateClickHandler(window(), activated, { button }); + } } void InnerWidget::setCollapsedPressed(int pressed) { @@ -1825,9 +1862,10 @@ void InnerWidget::moveCancelSearchButtons() { st::columnMinimalWidthLeft - _narrowWidth); const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchInChat->width(); const auto top = (st::dialogsSearchInHeight - st::dialogsCancelSearchInPeer.height) / 2; - _cancelSearchInChat->moveToLeft(left, st::searchedBarHeight + top); - const auto skip = _searchInChat ? (st::searchedBarHeight + st::dialogsSearchInHeight + st::lineWidth) : 0; - _cancelSearchFromUser->moveToLeft(left, skip + top); + const auto skip = st::searchedBarHeight + (_searchTags ? _searchTags->height() : 0); + _cancelSearchInChat->moveToLeft(left, skip + top); + const auto next = _searchInChat ? (skip + st::dialogsSearchInHeight + st::lineWidth) : 0; + _cancelSearchFromUser->moveToLeft(left, next + top); } void InnerWidget::dialogRowReplaced( @@ -2330,7 +2368,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { newFilter = words.isEmpty() ? QString() : words.join(' '); if (newFilter != _filter || force) { _filter = newFilter; - if (_filter.isEmpty() && !_searchFromPeer) { + if (_filter.isEmpty() + && !_searchFromPeer + && _searchTagsSelected.empty()) { clearFilter(); } else { setState(WidgetState::Filtered); @@ -2350,7 +2390,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { top += i->row->height(); } }; - if (!_searchInChat && !_searchFromPeer && !words.isEmpty()) { + if (!_searchInChat + && !_searchFromPeer + && !words.isEmpty()) { if (_savedSublists) { const auto owner = &session().data(); append(owner->savedMessages().chatsList()->indexed()); @@ -2791,6 +2833,11 @@ void InnerWidget::refresh(bool toTop) { return refreshWithCollapsedRows(toTop); } refreshEmptyLabel(); + if (_searchTags) { + _searchTagsLeft = st::dialogsFilterSkip + + st::dialogsFilterPadding.x(); + _searchTags->resizeToWidth(width() - 2 * _searchTagsLeft); + } auto h = 0; if (_state == WidgetState::Default) { if (_shownList->empty()) { @@ -2926,6 +2973,43 @@ void InnerWidget::searchInChat(Key key, PeerData *from) { } else if (const auto migrateFrom = peer->migrateFrom()) { _searchInMigrated = peer->owner().history(migrateFrom); } + + if (peer->isSelf()) { + const auto reactions = &peer->owner().reactions(); + const auto list = [=] { + return reactions->list(Data::Reactions::Type::MyTags); + }; + _searchTags = std::make_unique( + &peer->owner(), + rpl::single( + list() + ) | rpl::then( + reactions->myTagsUpdates() | rpl::map(list) + )); + + _searchTags->selectedValue( + ) | rpl::start_with_next([=](std::vector &&list) { + _searchTagsSelected = std::move(list); + }, _searchTags->lifetime()); + + _searchTags->repaintRequests() | rpl::start_with_next([=] { + const auto height = _searchTags->height(); + update(0, searchInChatOffset(), width(), height); + }, _searchTags->lifetime()); + + _searchTags->heightValue() | rpl::filter( + rpl::mappers::_1 > 0 + ) | rpl::start_with_next([=] { + refresh(); + moveCancelSearchButtons(); + }, _searchTags->lifetime()); + } else { + _searchTags = nullptr; + _searchTagsSelected.clear(); + } + } else { + _searchTags = nullptr; + _searchTagsSelected.clear(); } _searchInChat = key; _searchFromPeer = from; @@ -2957,6 +3041,13 @@ void InnerWidget::searchInChat(Key key, PeerData *from) { _searchInChat || !_filter.isEmpty()); } +auto InnerWidget::searchTagsValue() const +-> rpl::producer> { + return _searchTags + ? _searchTags->selectedValue() + : rpl::single(std::vector()); +} + void InnerWidget::refreshSearchInChatLabel() { const auto dialog = [&] { if (const auto topic = _searchInChat.topic()) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 7915bd912c..d511c65e8e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -43,6 +43,7 @@ namespace Data { class Thread; class Folder; class Forum; +struct ReactionId; } // namespace Data namespace Dialogs::Ui { @@ -57,6 +58,7 @@ namespace Dialogs { class Row; class FakeRow; class IndexedList; +class SearchTags; struct ChosenRow { Key key; @@ -138,6 +140,8 @@ public: [[nodiscard]] bool hasFilteredResults() const; void searchInChat(Key key, PeerData *from); + [[nodiscard]] auto searchTagsValue() const + -> rpl::producer>; void applyFilterUpdate(QString newFilter, bool force = false); void onHashtagFilterUpdate(QStringView newFilter); @@ -325,6 +329,7 @@ private: [[nodiscard]] int filteredIndex(int y) const; [[nodiscard]] int filteredHeight(int till = -1) const; [[nodiscard]] int peerSearchOffset() const; + [[nodiscard]] int searchInChatOffset() const; [[nodiscard]] int searchedOffset() const; [[nodiscard]] int searchInChatSkip() const; @@ -482,6 +487,9 @@ private: mutable Ui::PeerUserpicView _searchFromUserUserpic; Ui::Text::String _searchInChatText; Ui::Text::String _searchFromUserText; + std::unique_ptr _searchTags; + std::vector _searchTagsSelected; + int _searchTagsLeft = 0; RowDescriptor _menuRow; base::flat_map< diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp new file mode 100644 index 0000000000..22a15ec556 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -0,0 +1,246 @@ +/* +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 "dialogs/dialogs_search_tags.h" + +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" +#include "data/data_message_reactions.h" +#include "data/data_session.h" +#include "history/view/reactions/history_view_reactions.h" +#include "ui/effects/animation_value.h" +#include "ui/power_saving.h" +#include "styles/style_chat.h" +#include "styles/style_dialogs.h" + +namespace Dialogs { + +struct SearchTags::Tag { + Data::ReactionId id; + std::unique_ptr custom; + mutable QImage image; + QRect geometry; + ClickHandlerPtr link; + bool selected = false; +}; + +SearchTags::SearchTags( + not_null owner, + rpl::producer> tags) +: _owner(owner) { + std::move( + tags + ) | rpl::start_with_next([=](const std::vector &list) { + fill(list); + }, _lifetime); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _normalBg = _selectedBg = QImage(); + }, _lifetime); +} + +SearchTags::~SearchTags() = default; + +void SearchTags::fill(const std::vector &list) { + const auto selected = collectSelected(); + _tags.clear(); + _tags.reserve(list.size()); + const auto link = [&](Data::ReactionId id) { + return std::make_shared(crl::guard(this, [=] { + const auto i = ranges::find(_tags, id, &Tag::id); + if (i != end(_tags)) { + i->selected = !i->selected; + _selectedChanges.fire({}); + } + })); + }; + for (const auto &reaction : list) { + const auto id = reaction.id; + const auto customId = id.custom(); + _tags.push_back({ + .id = id, + .custom = (customId + ? _owner->customEmojiManager().create( + customId, + [=] { _repaintRequests.fire({}); }) + : nullptr), + .link = link(id), + .selected = ranges::contains(selected, id), + }); + if (!customId) { + _owner->reactions().preloadImageFor(id); + } + } + if (_width > 0) { + layout(); + } +} + +void SearchTags::layout() { + Expects(_width > 0); + + const auto &bg = validateBg(false); + const auto skip = st::dialogsSearchTagSkip; + const auto size = bg.size() / bg.devicePixelRatio(); + const auto xsingle = size.width() + skip.x(); + const auto ysingle = size.height() + skip.y(); + const auto columns = std::max((_width + skip.x()) / xsingle, 1); + const auto rows = (_tags.size() + columns - 1) / columns; + for (auto row = 0; row != rows; ++row) { + for (auto column = 0; column != columns; ++column) { + const auto index = row * columns + column; + if (index >= _tags.size()) { + break; + } + const auto x = column * xsingle; + const auto y = row * ysingle; + _tags[index].geometry = QRect(QPoint(x, y), size); + } + } + const auto bottom = st::dialogsSearchTagBottom; + _height = rows ? (rows * ysingle - skip.y() + bottom) : 0; +} + +void SearchTags::resizeToWidth(int width) { + if (_width == width || width <= 0) { + return; + } + _width = width; + layout(); +} + +int SearchTags::height() const { + return _height.current(); +} + +rpl::producer SearchTags::heightValue() const { + return _height.value(); +} + +rpl::producer<> SearchTags::repaintRequests() const { + return _repaintRequests.events(); +} + +ClickHandlerPtr SearchTags::lookupHandler(QPoint point) const { + for (const auto &tag : _tags) { + if (tag.geometry.contains(point.x(), point.y())) { + return tag.link; + } + } + return nullptr; +} + +auto SearchTags::selectedValue() const +-> rpl::producer> { + return _selectedChanges.events() | rpl::map([=] { + return collectSelected(); + }); +} + +void SearchTags::paintCustomFrame( + QPainter &p, + not_null emoji, + QPoint innerTopLeft, + crl::time now, + bool paused, + const QColor &textColor) const { + if (_customCache.isNull()) { + using namespace Ui::Text; + const auto size = st::emojiSize; + const auto factor = style::DevicePixelRatio(); + const auto adjusted = AdjustCustomEmojiSize(size); + _customCache = QImage( + QSize(adjusted, adjusted) * factor, + QImage::Format_ARGB32_Premultiplied); + _customCache.setDevicePixelRatio(factor); + _customSkip = (size - adjusted) / 2; + } + _customCache.fill(Qt::transparent); + auto q = QPainter(&_customCache); + emoji->paint(q, { + .textColor = textColor, + .now = now, + .paused = paused || On(PowerSaving::kEmojiChat), + }); + q.end(); + _customCache = Images::Round( + std::move(_customCache), + (Images::Option::RoundLarge + | Images::Option::RoundSkipTopRight + | Images::Option::RoundSkipBottomRight)); + + p.drawImage( + innerTopLeft + QPoint(_customSkip, _customSkip), + _customCache); +} + +void SearchTags::paint( + QPainter &p, + QPoint position, + crl::time now, + bool paused) const { + const auto size = st::reactionInlineSize; + const auto skip = (size - st::reactionInlineImage) / 2; + const auto padding = st::reactionInlinePadding; + for (const auto &tag : _tags) { + const auto geometry = tag.geometry.translated(position); + p.drawImage(geometry.topLeft(), validateBg(tag.selected)); + if (!tag.custom && tag.image.isNull()) { + tag.image = _owner->reactions().resolveImageFor( + tag.id, + ::Data::Reactions::ImageSize::InlineList); + } + const auto inner = geometry.marginsRemoved(padding); + const auto image = QRect( + inner.topLeft() + QPoint(skip, skip), + QSize(st::reactionInlineImage, st::reactionInlineImage)); + if (const auto custom = tag.custom.get()) { + const auto textFg = tag.selected + ? st::dialogsNameFgActive->c + : st::dialogsNameFgOver->c; + paintCustomFrame( + p, + custom, + inner.topLeft(), + now, + paused, + textFg); + } else if (!tag.image.isNull()) { + p.drawImage(image.topLeft(), tag.image); + } + } +} + +const QImage &SearchTags::validateBg(bool selected) const { + using namespace HistoryView::Reactions; + auto &image = selected ? _selectedBg : _normalBg; + if (image.isNull()) { + const auto tagBg = selected + ? st::dialogsBgActive->c + : st::dialogsBgOver->c; + const auto dotBg = selected + ? anim::with_alpha(tagBg, InlineList::TagDotAlpha()) + : st::windowSubTextFg->c; + image = InlineList::PrepareTagBg(tagBg, dotBg); + } + return image; +} + +std::vector SearchTags::collectSelected() const { + return _tags | ranges::views::filter( + &Tag::selected + ) | ranges::views::transform( + &Tag::id + ) | ranges::to_vector; +} + +rpl::lifetime &SearchTags::lifetime() { + return _lifetime; +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.h b/Telegram/SourceFiles/dialogs/dialogs_search_tags.h new file mode 100644 index 0000000000..b1c1938783 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.h @@ -0,0 +1,78 @@ +/* +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 +*/ +#pragma once + +#include "base/weak_ptr.h" + +namespace Data { +class Session; +struct Reaction; +struct ReactionId; +} // namespace Data + +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + +namespace Dialogs { + +class SearchTags final : public base::has_weak_ptr { +public: + SearchTags( + not_null owner, + rpl::producer> tags); + ~SearchTags(); + + void resizeToWidth(int width); + [[nodiscard]] int height() const; + [[nodiscard]] rpl::producer heightValue() const; + [[nodiscard]] rpl::producer<> repaintRequests() const; + + [[nodiscard]] ClickHandlerPtr lookupHandler(QPoint point) const; + [[nodiscard]] auto selectedValue() const + -> rpl::producer>; + + void paint( + QPainter &p, + QPoint position, + crl::time now, + bool paused) const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + struct Tag; + + void fill(const std::vector &list); + void paintCustomFrame( + QPainter &p, + not_null emoji, + QPoint innerTopLeft, + crl::time now, + bool paused, + const QColor &textColor) const; + void layout(); + [[nodiscard]] std::vector collectSelected() const; + [[nodiscard]] const QImage &validateBg(bool selected) const; + + const not_null _owner; + std::vector _tags; + rpl::event_stream<> _selectedChanges; + rpl::event_stream<> _repaintRequests; + mutable QImage _normalBg; + mutable QImage _selectedBg; + mutable QImage _customCache; + mutable int _customSkip = 0; + rpl::variable _height; + int _width = 0; + + rpl::lifetime _lifetime; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index f6ace227ad..2f19148faa 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1713,7 +1713,7 @@ void Widget::loadMoreBlockedByDate() { bool Widget::searchMessages(bool searchCache) { auto result = false; auto q = currentSearchQuery().trimmed(); - if (q.isEmpty() && !_searchFromAuthor) { + if (q.isEmpty() && !_searchFromAuthor && _searchTags.empty()) { cancelSearchRequest(); _api.request(base::take(_peerSearchRequest)).cancel(); _api.request(base::take(_topicSearchRequest)).cancel(); @@ -1730,6 +1730,7 @@ bool Widget::searchMessages(bool searchCache) { if (i != _searchCache.end()) { _searchQuery = q; _searchQueryFrom = _searchFromAuthor; + _searchQueryTags = _searchTags; _searchNextRate = 0; _searchFull = _searchFullMigrated = false; cancelSearchRequest(); @@ -1741,9 +1742,12 @@ bool Widget::searchMessages(bool searchCache) { 0); result = true; } - } else if (_searchQuery != q || _searchQueryFrom != _searchFromAuthor) { + } else if (_searchQuery != q + || _searchQueryFrom != _searchFromAuthor + || _searchQueryTags != _searchTags) { _searchQuery = q; _searchQueryFrom = _searchFromAuthor; + _searchQueryTags = _searchTags; _searchNextRate = 0; _searchFull = _searchFullMigrated = false; cancelSearchRequest(); @@ -1757,14 +1761,20 @@ bool Widget::searchMessages(bool searchCache) { using Flag = MTPmessages_Search::Flag; _searchRequest = session().api().request(MTPmessages_Search( MTP_flags((topic ? Flag::f_top_msg_id : Flag()) - | (_searchQueryFrom ? Flag::f_from_id : Flag())), + | (_searchQueryFrom ? Flag::f_from_id : Flag()) + | (_searchQueryTags.empty() + ? Flag() + : Flag::f_saved_reaction)), peer->input, MTP_string(_searchQuery), (_searchQueryFrom ? _searchQueryFrom->input : MTP_inputPeerEmpty()), MTPInputPeer(), // saved_peer_id - MTPVector(), // saved_reaction + MTP_vector_from_range( + _searchQueryTags | ranges::views::transform( + Data::ReactionToMTP + )), MTP_int(topic ? topic->rootId() : 0), MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date @@ -1868,6 +1878,7 @@ bool Widget::searchMessages(bool searchCache) { bool Widget::searchForPeersRequired(const QString &query) const { return !_searchInChat && !_searchFromAuthor + && _searchTags.empty() && !_openedForum && !query.isEmpty() && (query[0] != '#'); @@ -1876,6 +1887,7 @@ bool Widget::searchForPeersRequired(const QString &query) const { bool Widget::searchForTopicsRequired(const QString &query) const { return !_searchInChat && !_searchFromAuthor + && _searchTags.empty() && _openedForum && !query.isEmpty() && (query[0] != '#') @@ -2000,14 +2012,20 @@ void Widget::searchMore() { using Flag = MTPmessages_Search::Flag; _searchRequest = session().api().request(MTPmessages_Search( MTP_flags((topic ? Flag::f_top_msg_id : Flag()) - | (_searchQueryFrom ? Flag::f_from_id : Flag())), + | (_searchQueryFrom ? Flag::f_from_id : Flag()) + | (_searchQueryTags.empty() + ? Flag() + : Flag::f_saved_reaction)), peer->input, MTP_string(_searchQuery), (_searchQueryFrom ? _searchQueryFrom->input : MTP_inputPeerEmpty()), MTPInputPeer(), // saved_peer_id - MTPVector(), // saved_reaction + MTP_vector_from_range( + _searchQueryTags | ranges::views::transform( + Data::ReactionToMTP + )), MTP_int(topic ? topic->rootId() : 0), MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date @@ -2401,7 +2419,7 @@ void Widget::applyFilterUpdate(bool force) { updateStoriesVisibility(); const auto filterText = currentSearchQuery(); _inner->applyFilterUpdate(filterText, force); - if (filterText.isEmpty() && !_searchFromAuthor) { + if (filterText.isEmpty() && !_searchFromAuthor && _searchTags.empty()) { clearSearchCache(); } _cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal); @@ -2417,7 +2435,9 @@ void Widget::applyFilterUpdate(bool force) { _peerSearchQuery = QString(); } - if (_chooseFromUser->toggled() || _searchFromAuthor) { + if (_chooseFromUser->toggled() + || _searchFromAuthor + || !_searchTags.empty()) { auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery(); if (_lastFilterText != switchToChooseFrom && switchToChooseFrom.startsWith(_lastFilterText) @@ -2619,6 +2639,18 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) { controller()->closeFolder(); } _inner->searchInChat(_searchInChat, _searchFromAuthor); + _searchTagsLifetime = _inner->searchTagsValue( + ) | rpl::start_with_next([=](std::vector &&list) { + if (_searchTags != list) { + clearSearchCache(); + _searchTags = std::move(list); + if (_searchTags.empty()) { + applyFilterUpdate(true); + } else { + searchMessages(); + } + } + }); if (_subsectionTopBar) { _subsectionTopBar->searchEnableJumpToDate( _openedForum && _searchInChat); @@ -2639,6 +2671,7 @@ void Widget::clearSearchCache() { } _searchQuery = QString(); _searchQueryFrom = nullptr; + _searchQueryTags.clear(); _topicSearchQuery = QString(); _topicSearchOffsetDate = 0; _topicSearchOffsetId = _topicSearchOffsetTopicId = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 711509625d..5ab53e2f94 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -22,6 +22,7 @@ class Error; namespace Data { class Forum; enum class StorySourcesList : uchar; +struct ReactionId; } // namespace Data namespace Main { @@ -285,6 +286,8 @@ private: Dialogs::Key _searchInChat; History *_searchInMigrated = nullptr; PeerData *_searchFromAuthor = nullptr; + std::vector _searchTags; + rpl::lifetime _searchTagsLifetime; QString _lastFilterText; rpl::event_stream> _storiesContents; @@ -313,6 +316,7 @@ private: QString _searchQuery; PeerData *_searchQueryFrom = nullptr; + std::vector _searchQueryTags; int32 _searchNextRate = 0; bool _searchFull = false; bool _searchFullMigrated = false; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index a7e83c0291..2927748995 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -507,12 +507,11 @@ void InlineList::paint( } } -void InlineList::validateTagBg(const QColor &color) const { - if (!_tagBg.isNull() && _tagBgColor == color) { - return; - } - _tagBgColor = color; +float64 InlineList::TagDotAlpha() { + return 0.6; +} +QImage InlineList::PrepareTagBg(QColor tagBg, QColor dotBg) { const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; const auto width = padding.left() @@ -522,20 +521,19 @@ void InlineList::validateTagBg(const QColor &color) const { const auto height = padding.top() + size + padding.bottom(); const auto ratio = style::DevicePixelRatio(); - auto mask = QImage( + auto result = QImage( QSize(width, height) * ratio, QImage::Format_ARGB32_Premultiplied); - mask.setDevicePixelRatio(ratio); + result.setDevicePixelRatio(ratio); - mask.fill(Qt::transparent); - auto p = QPainter(&mask); + result.fill(Qt::transparent); + auto p = QPainter(&result); auto path = QPainterPath(); const auto arrow = st::reactionInlineTagArrow; const auto rradius = st::reactionInlineTagRightRadius * 1.; const auto radius = st::reactionInlineTagLeftRadius - rradius; - const auto fg = QColor(255, 255, 255); - auto pen = QPen(fg); + auto pen = QPen(tagBg); pen.setWidthF(rradius * 2.); pen.setJoinStyle(Qt::RoundJoin); const auto rect = QRectF(0, 0, width, height).marginsRemoved( @@ -548,9 +546,15 @@ void InlineList::validateTagBg(const QColor &color) const { path.lineTo(right, rect.y() + rect.height() / 2); path.lineTo(right - arrow, bottom); path.lineTo(rect.x() + radius, bottom); - path.arcTo(QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), 270, -90); + path.arcTo( + QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), + 270, + -90); path.lineTo(rect.x(), rect.y() + radius); - path.arcTo(QRectF(rect.x(), rect.y(), radius * 2, radius * 2), 180, -90); + path.arcTo( + QRectF(rect.x(), rect.y(), radius * 2, radius * 2), + 180, + -90); path.closeSubpath(); const auto dsize = st::reactionInlineTagDot; @@ -563,16 +567,26 @@ void InlineList::validateTagBg(const QColor &color) const { auto hq = PainterHighQualityEnabler(p); p.setCompositionMode(QPainter::CompositionMode_Source); p.setPen(pen); - p.setBrush(fg); + p.setBrush(tagBg); p.drawPath(path); p.setPen(Qt::NoPen); - p.setBrush(QColor(255, 255, 255, 255 * 0.6)); + p.setBrush(dotBg); p.drawEllipse(dot); p.end(); - _tagBg = style::colorizeImage(mask, color); + return result; +} + +void InlineList::validateTagBg(const QColor &color) const { + if (!_tagBg.isNull() && _tagBgColor == color) { + return; + } + _tagBgColor = color; + auto dot = color; + dot.setAlphaF(dot.alphaF() * TagDotAlpha()); + _tagBg = PrepareTagBg(color, anim::with_alpha(color, TagDotAlpha())); } void InlineList::paintSingleBg( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index cc4fa80d84..619e1f0534 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -93,6 +93,9 @@ public: ReactionId, std::unique_ptr> animations); + [[nodiscard]] static float64 TagDotAlpha(); + [[nodiscard]] static QImage PrepareTagBg(QColor tagBg, QColor dotBg); + private: struct Userpics { QImage image;