diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 500f65679..62a50f27b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4146,6 +4146,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_search_messages_from" = "Show messages from"; "lng_search_messages_n_of_amount" = "{n} of {amount}"; "lng_search_messages_none" = "No results"; +"lng_search_filter_all" = "All chats"; +"lng_search_filter_private" = "Private chats"; +"lng_search_filter_group" = "Group chats"; +"lng_search_filter_channel" = "Channels"; "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; @@ -5998,6 +6002,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_search_tab_no_results_text" = "There were no results for \"{query}\"."; "lng_search_tab_no_results_retry" = "Try another hashtag."; "lng_search_tab_by_hashtag" = "Enter a hashtag to find messages containing it."; +"lng_search_tab_try_in_all" = "Search in All Messages"; "lng_contact_details_button" = "View Contact"; "lng_contact_details_title" = "Contact details"; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index cb6bd28a1..daa218816 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -79,6 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_color_indices.h" #include "styles/style_window.h" +#include "styles/style_media_player.h" #include "styles/style_menu_icons.h" #include @@ -143,7 +144,8 @@ constexpr auto kPreviewPostsLimit = 3; [[nodiscard]] object_ptr MakeSearchEmpty( QWidget *parent, - SearchState state) { + SearchState state, + Fn resetChatTypeFilter) { const auto query = state.query.trimmed(); const auto hashtag = !query.isEmpty() && (query[0] == '#'); const auto trimmed = hashtag ? query.mid(1).trimmed() : query; @@ -157,6 +159,9 @@ constexpr auto kPreviewPostsLimit = 3; const auto waiting = trimmed.isEmpty() && state.tags.empty() && !fromPeer; + const auto suggestAllChats = !waiting + && state.tab == ChatSearchTab::MyMessages + && state.filter != ChatTypeFilter::All; const auto icon = waiting ? SearchEmptyIcon::Search : SearchEmptyIcon::NoResults; @@ -180,7 +185,10 @@ constexpr auto kPreviewPostsLimit = 3; tr::now, lt_query, trimmed.mid(0, kQueryPreviewLimit))); - if (hashtag) { + if (suggestAllChats) { + text.append("\n\n").append( + Ui::Text::Link(tr::lng_search_tab_try_in_all(tr::now))); + } else if (hashtag) { text.append("\n").append( tr::lng_search_tab_no_results_retry(tr::now)); } @@ -190,11 +198,29 @@ constexpr auto kPreviewPostsLimit = 3; parent, icon, rpl::single(std::move(text))); + if (suggestAllChats) { + result->handlerActivated( + ) | rpl::start_with_next(resetChatTypeFilter, result->lifetime()); + } result->show(); result->resizeToWidth(parent->width()); return result; } +[[nodiscard]] QString ChatTypeFilterLabel(ChatTypeFilter filter) { + switch (filter) { + case ChatTypeFilter::All: + return tr::lng_search_filter_all(tr::now); + case ChatTypeFilter::Private: + return tr::lng_search_filter_private(tr::now); + case ChatTypeFilter::Groups: + return tr::lng_search_filter_group(tr::now); + case ChatTypeFilter::Channels: + return tr::lng_search_filter_channel(tr::now); + } + Unexpected("Chat type filter in search results."); +} + } // namespace struct InnerWidget::CollapsedRow { @@ -1184,6 +1210,25 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setFont(st::searchedBarFont); p.setPen(st::searchedBarFg); p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); + const auto filterOver = _selectedChatTypeFilter + || _pressedChatTypeFilter; + const auto filterFont = filterOver + ? st::searchedBarFont->underline() + : st::searchedBarFont; + if (_searchState.tab == ChatSearchTab::MyMessages) { + const auto text = ChatTypeFilterLabel(_searchState.filter); + if (!_chatTypeFilterWidth) { + _chatTypeFilterWidth = filterFont->width(text); + } + p.setFont(filterFont); + p.drawTextLeft( + (width() + - st::searchedBarPosition.x() + - _chatTypeFilterWidth), + st::searchedBarPosition.y(), + width(), + text); + } p.translate(0, st::searchedBarHeight); auto skip = searchedOffset(); @@ -1701,6 +1746,20 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { _searchedSelected = searchedSelected; updateSelectedRow(); } + auto selectedChatTypeFilter = false; + const auto from = skip - st::searchedBarHeight; + if (mouseY <= skip && mouseY >= from) { + const auto left = width() + - _chatTypeFilterWidth + - 2 * st::searchedBarPosition.x(); + if (_chatTypeFilterWidth > 0 && local.x() >= left) { + selectedChatTypeFilter = true; + } + } + if (_selectedChatTypeFilter != selectedChatTypeFilter) { + update(0, from, width(), st::searchedBarHeight); + _selectedChatTypeFilter = selectedChatTypeFilter; + } } if (!inTags && wasSelected != isSelected()) { setCursor(wasSelected ? style::cur_default : style::cur_pointer); @@ -1742,6 +1801,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { setPreviewPressed(_previewSelected); setSearchedPressed(_searchedSelected); _pressedMorePosts = _selectedMorePosts; + _pressedChatTypeFilter = _selectedChatTypeFilter; const auto alt = (e->modifiers() & Qt::AltModifier); if (alt && showChatPreview()) { @@ -2139,6 +2199,8 @@ void InnerWidget::mousePressReleased( setSearchedPressed(-1); const auto pressedMorePosts = _pressedMorePosts; _pressedMorePosts = false; + const auto pressedChatTypeFilter = _pressedChatTypeFilter; + _pressedChatTypeFilter = false; if (wasDragging) { selectByMouse(globalPosition); } @@ -2163,7 +2225,9 @@ void InnerWidget::mousePressReleased( || (searchedPressed >= 0 && searchedPressed == _searchedSelected) || (pressedMorePosts - && pressedMorePosts == _selectedMorePosts)) { + && pressedMorePosts == _selectedMorePosts) + || (pressedChatTypeFilter + && pressedChatTypeFilter == _selectedChatTypeFilter)) { if (pressedBotApp && (pressed || filteredPressed >= 0)) { const auto &row = pressed ? pressed @@ -2690,6 +2754,7 @@ void InnerWidget::clearSelection() { updateSelectedRow(); _collapsedSelected = -1; _selectedMorePosts = false; + _selectedChatTypeFilter = false; _selected = nullptr; _filteredSelected = _searchedSelected @@ -3001,6 +3066,10 @@ void InnerWidget::applySearchState(SearchState state) { if (state.inChat) { onHashtagFilterUpdate(QStringView()); } + if (state.filter != _searchState.filter) { + _chatTypeFilterWidth = 0; + update(); + } _searchState = std::move(state); _searchHashOrCashtag = IsHashOrCashtagSearchQuery(_searchState.query); _searchWithPostsPreview = computeSearchWithPostsPreview(); @@ -3269,6 +3338,11 @@ rpl::producer InnerWidget::changeSearchTabRequests() const { return _changeSearchTabRequests.events(); } +auto InnerWidget::changeSearchFilterRequests() const +-> rpl::producer{ + return _changeSearchFilterRequests.events(); +} + rpl::producer<> InnerWidget::cancelSearchRequests() const { return _cancelSearchRequests.events(); } @@ -3557,7 +3631,9 @@ void InnerWidget::refreshEmpty() { } } else if (_searchEmptyState != _searchState) { _searchEmptyState = _searchState; - _searchEmpty = MakeSearchEmpty(this, _searchState); + _searchEmpty = MakeSearchEmpty(this, _searchState, [=] { + _changeSearchFilterRequests.fire(ChatTypeFilter::All); + }); if (_controller->session().data().chatsListLoaded()) { _searchEmpty->animate(); } @@ -4288,7 +4364,7 @@ ChosenRow InnerWidget::computeChosenRow() const { } bool InnerWidget::isUserpicPress() const { - return (_lastRowLocalMouseX >= 0) + return (_lastRowLocalMouseX >= 0) && (_lastRowLocalMouseX < _st->nameLeft) && (_collapsedSelected < 0 || _collapsedSelected >= _collapsedRows.size()); @@ -4308,6 +4384,24 @@ bool InnerWidget::chooseRow( _changeSearchTabRequests.fire(ChatSearchTab::PublicPosts); } return true; + } else if (_selectedChatTypeFilter) { + _menu = base::make_unique_q( + this, + st::popupMenuWithIcons); + for (const auto tab : { + ChatTypeFilter::All, + ChatTypeFilter::Private, + ChatTypeFilter::Groups, + ChatTypeFilter::Channels, + }) { + _menu->addAction(ChatTypeFilterLabel(tab), [=] { + _changeSearchFilterRequests.fire_copy(tab); + }, (tab == _searchState.filter) + ? &st::mediaPlayerMenuCheck + : nullptr); + } + _menu->popup(QCursor::pos()); + return true; } const auto modifyChosenRow = [&]( ChosenRow row, diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 48c409234..1f7310153 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -65,6 +65,7 @@ class SearchEmpty; class ChatSearchIn; enum class HashOrCashtag : uchar; struct RightButton; +enum class ChatTypeFilter : uchar; struct ChosenRow { Key key; @@ -176,6 +177,8 @@ public: [[nodiscard]] rpl::producer<> listBottomReached() const; [[nodiscard]] auto changeSearchTabRequests() const -> rpl::producer; + [[nodiscard]] auto changeSearchFilterRequests() const + -> rpl::producer; [[nodiscard]] rpl::producer<> cancelSearchRequests() const; [[nodiscard]] rpl::producer<> cancelSearchFromRequests() const; [[nodiscard]] rpl::producer<> changeSearchFromRequests() const; @@ -308,7 +311,8 @@ private: || (_peerSearchPressed >= 0) || (_previewPressed >= 0) || (_searchedPressed >= 0) - || _pressedMorePosts; + || _pressedMorePosts + || _pressedChatTypeFilter; } bool isSelected() const { return (_collapsedSelected >= 0) @@ -318,7 +322,8 @@ private: || (_peerSearchSelected >= 0) || (_previewSelected >= 0) || (_searchedSelected >= 0) - || _selectedMorePosts; + || _selectedMorePosts + || _selectedChatTypeFilter; } bool uniqueSearchResults() const; bool hasHistoryInResults(not_null history) const; @@ -486,6 +491,8 @@ private: std::vector> _collapsedRows; not_null _st; mutable std::unique_ptr _topicJumpCache; + bool _selectedChatTypeFilter = false; + bool _pressedChatTypeFilter = false; bool _selectedMorePosts = false; bool _pressedMorePosts = false; int _collapsedSelected = -1; @@ -543,6 +550,7 @@ private: int _previewSelected = -1; int _previewPressed = -1; int _morePostsWidth = 0; + int _chatTypeFilterWidth = 0; std::vector> _searchResults; int _searchedCount = 0; @@ -554,6 +562,7 @@ private: std::unique_ptr _searchIn; rpl::event_stream _changeSearchTabRequests; + rpl::event_stream _changeSearchFilterRequests; rpl::event_stream<> _cancelSearchRequests; rpl::event_stream<> _cancelSearchFromRequests; rpl::event_stream<> _changeSearchFromRequests; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 52c07fa17..58b0284dc 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -129,11 +129,19 @@ struct EntryState { const EntryState&) = default; }; +enum class ChatTypeFilter : uchar { + All, + Private, + Groups, + Channels, +}; + struct SearchState { Key inChat; PeerData *fromPeer = nullptr; std::vector tags; ChatSearchTab tab = {}; + ChatTypeFilter filter = ChatTypeFilter::All; QString query; [[nodiscard]] bool empty() const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index e71d0ad19..496ad41f0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -453,6 +453,15 @@ Widget::Widget( copy.tab = tab; applySearchState(std::move(copy)); }, lifetime()); + _inner->changeSearchFilterRequests( + ) | rpl::filter([=](ChatTypeFilter filter) { + return (_searchState.filter != filter) + && (_searchState.tab == ChatSearchTab::MyMessages); + }) | rpl::start_with_next([=](ChatTypeFilter filter) { + auto copy = _searchState; + copy.filter = filter; + applySearchState(copy); + }, lifetime()); _inner->cancelSearchRequests( ) | rpl::start_with_next([=] { cancelSearch({ @@ -2201,6 +2210,7 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { const auto fromPeer = searchFromPeer(); const auto &inTags = searchInTags(); const auto tab = _searchState.tab; + const auto filter = _searchState.filter; const auto fromStartType = SearchRequestType{ .start = true, .peer = (inPeer != nullptr), @@ -2232,6 +2242,7 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { _searchQueryFrom = fromPeer; _searchQueryTags = inTags; _searchQueryTab = tab; + _searchQueryFilter = filter; process->nextRate = 0; process->full = false; _migratedProcess.full = false; @@ -2242,12 +2253,14 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { } else if (_searchQuery != query || _searchQueryFrom != fromPeer || _searchQueryTags != inTags - || _searchQueryTab != tab) { + || _searchQueryTab != tab + || _searchQueryFilter != filter) { const auto process = currentSearchProcess(); _searchQuery = query; _searchQueryFrom = fromPeer; _searchQueryTags = inTags; _searchQueryTab = tab; + _searchQueryFilter = filter; process->nextRate = 0; process->full = false; _migratedProcess.full = false; @@ -2326,7 +2339,6 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { _peerSearchQuery = peerQuery; _peerSearchRequest = 0; peerSearchReceived(i->second, 0); - result = true; } } else if (_peerSearchQuery != peerQuery) { _peerSearchQuery = peerQuery; @@ -2598,9 +2610,18 @@ void Widget::requestMessages(bool fromStart) { const auto type = SearchRequestType{ .start = fromStart, }; - const auto flags = session().settings().skipArchiveInSearch() - ? MTPmessages_SearchGlobal::Flag::f_folder_id - : MTPmessages_SearchGlobal::Flag(0); + using Flag = MTPmessages_SearchGlobal::Flag; + const auto flags = Flag() + | (session().settings().skipArchiveInSearch() + ? Flag::f_folder_id + : Flag()) + | (_searchQueryFilter == ChatTypeFilter::Private + ? Flag::f_users_only + : _searchQueryFilter == ChatTypeFilter::Groups + ? Flag::f_groups_only + : _searchQueryFilter == ChatTypeFilter::Channels + ? Flag::f_broadcasts_only + : Flag()); const auto folderId = 0; _searchProcess.requestId = session().api().request( MTPmessages_SearchGlobal( @@ -3184,6 +3205,10 @@ bool Widget::applySearchState(SearchState state) { const auto queryEmptyChanged = queryChanged ? (_searchState.query.isEmpty() != state.query.isEmpty()) : false; + if (queryEmptyChanged || tabChanged) { + state.filter = ChatTypeFilter::All; + } + const auto filterChanged = (_searchState.filter != state.filter); if (forum) { if (_openedForum == forum) { @@ -3245,6 +3270,7 @@ bool Widget::applySearchState(SearchState state) { if (searchCleared || inChatChanged || fromPeerChanged + || filterChanged || tagsChanged || tabChanged) { clearSearchCache(searchCleared); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 6f2bab88e..46da4b695 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -381,6 +381,7 @@ private: PeerData *_searchQueryFrom = nullptr; std::vector _searchQueryTags; ChatSearchTab _searchQueryTab = {}; + ChatTypeFilter _searchQueryFilter = {}; SearchProcessState _searchProcess; SearchProcessState _migratedProcess; diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp index 7c3c7f393..028452e36 100644 --- a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp @@ -62,6 +62,13 @@ void SearchEmpty::setup(Icon icon, rpl::producer text) { label->move((size.width() - label->width()) / 2, top - sub); }, lifetime()); + label->setClickHandlerFilter([=]( + const ClickHandlerPtr &handler, + Qt::MouseButton) { + _handlerActivated.fire_copy(handler); + return false; + }); + _animate = [animate] { animate(anim::repeat::once); }; diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h index 0ad901ad9..2d1d830a2 100644 --- a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h @@ -29,10 +29,15 @@ public: void animate(); + [[nodiscard]] rpl::producer handlerActivated() const { + return _handlerActivated.events(); + } + private: void setup(Icon icon, rpl::producer text); Fn _animate; + rpl::event_stream _handlerActivated; };