diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 274d889a90..6767c52f99 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_three_state_icon.h" +#include "dialogs/ui/chat_search_empty.h" #include "dialogs/ui/chat_search_tabs.h" #include "dialogs/ui/dialogs_layout.h" #include "dialogs/ui/dialogs_stories_content.h" @@ -84,7 +85,7 @@ constexpr auto kHashtagResultsLimit = 5; constexpr auto kStartReorderThreshold = 30; constexpr auto kChatPreviewDelay = crl::time(1000); -int FixedOnTopDialogsCount(not_null list) { +[[nodiscard]] int FixedOnTopDialogsCount(not_null list) { auto result = 0; for (const auto &row : *list) { if (!row->entry()->fixedOnTopIndex()) { @@ -95,7 +96,7 @@ int FixedOnTopDialogsCount(not_null list) { return result; } -int PinnedDialogsCount( +[[nodiscard]] int PinnedDialogsCount( FilterId filterId, not_null list) { auto result = 0; @@ -110,6 +111,52 @@ int PinnedDialogsCount( return result; } +[[nodiscard]] object_ptr MakeSearchEmpty( + QWidget *parent, + SearchState state) { + const auto query = state.query.trimmed(); + const auto hashtag = !query.isEmpty() && (query[0] == '#'); + const auto trimmed = hashtag ? query.mid(1).trimmed() : query; + const auto waiting = trimmed.isEmpty() + && state.tags.empty() + && !state.fromPeer; + const auto icon = waiting + ? SearchEmptyIcon::Search + : SearchEmptyIcon::NoResults; + auto text = TextWithEntities(); + if (waiting) { + if (hashtag) { + text.append(tr::lng_search_tab_by_hashtag(tr::now)); + } else { + text.append( + tr::lng_dlg_search_for_messages(tr::now) + ).append('\n').append(Ui::Text::Link(tr::lng_cancel(tr::now))); + } + } else { + text.append(tr::lng_search_tab_no_results( + tr::now, + Ui::Text::Bold)); + if (!trimmed.isEmpty()) { + text.append("\n").append( + tr::lng_search_tab_no_results_text( + tr::now, + lt_query, + trimmed)); + if (hashtag) { + text.append("\n").append( + tr::lng_search_tab_no_results_retry(tr::now)); + } + } + } + auto result = object_ptr( + parent, + icon, + rpl::single(std::move(text))); + result->show(); + result->resizeToWidth(parent->width()); + return result; +} + } // namespace struct InnerWidget::CollapsedRow { @@ -186,7 +233,7 @@ InnerWidget::InnerWidget( session().data().contactsLoaded().changes( ) | rpl::start_with_next([=] { refresh(); - refreshEmptyLabel(); + refreshEmpty(); }, lifetime()); session().data().itemRemoved( @@ -1906,7 +1953,7 @@ void InnerWidget::resizeEvent(QResizeEvent *e) { if (_searchTags) { _searchTags->resizeToWidth(width() - 2 * _searchTagsLeft); } - resizeEmptyLabel(); + resizeEmpty(); moveCancelSearchButtons(); } @@ -2587,12 +2634,13 @@ void InnerWidget::applySearchState(SearchState state) { append(owner->contactsNoChatsList()); } } - refresh(true); } clearMouseSelection(true); } if (_state != WidgetState::Default) { + _searchLoading = true; _searchMessages.fire({}); + refresh(true); } } @@ -2791,6 +2839,10 @@ rpl::producer<> InnerWidget::cancelSearchFromUserRequests() const { return _cancelSearchFromUser->clicks() | rpl::to_empty; } +rpl::producer<> InnerWidget::cancelSearchRequests() const { + return _cancelSearch.events(); +} + rpl::producer InnerWidget::mustScrollTo() const { return _mustScrollTo.events(); } @@ -2888,6 +2940,8 @@ void InnerWidget::searchReceived( HistoryItem *inject, SearchRequestType type, int fullCount) { + _searchLoading = false; + const auto uniquePeers = uniqueSearchResults(); if (type == SearchRequestType::FromStart || type == SearchRequestType::PeerFromStart) { @@ -3018,7 +3072,7 @@ void InnerWidget::refresh(bool toTop) { } else if (needCollapsedRowsRefresh()) { return refreshWithCollapsedRows(toTop); } - refreshEmptyLabel(); + refreshEmpty(); if (_searchTags) { _searchTagsLeft = st::dialogsFilterSkip + st::dialogsFilterPadding.x(); @@ -3032,7 +3086,11 @@ void InnerWidget::refresh(bool toTop) { h = dialogsOffset() + _shownList->height(); } } else if (_state == WidgetState::Filtered) { - if (_waitingForSearch) { + if (_searchEmpty && !_searchEmpty->isHidden()) { + h = searchedOffset() + st::recentPeersEmptyHeightMin; + _searchEmpty->setMinimalHeight(st::recentPeersEmptyHeightMin); + _searchEmpty->move(0, h - st::recentPeersEmptyHeightMin); + } else if (_waitingForSearch) { h = searchedOffset() + (_searchResults.size() * _st->height) + ((_searchResults.empty() && !_searchState.inChat) ? -st::searchedBarHeight : 0); } else { h = searchedOffset() + (_searchResults.size() * _st->height); @@ -3047,7 +3105,32 @@ void InnerWidget::refresh(bool toTop) { update(); } -void InnerWidget::refreshEmptyLabel() { +void InnerWidget::refreshEmpty() { + if (_state == WidgetState::Filtered) { + const auto empty = _filterResults.empty() + && _searchResults.empty() + && _peerSearchResults.empty() + && _hashtagResults.empty(); + if (_searchLoading || !empty) { + if (_searchEmpty) { + _searchEmpty->hide(); + } + } else if (_searchEmptyState != _searchState) { + _searchEmptyState = _searchState; + _searchEmpty = MakeSearchEmpty(this, _searchState); + _searchEmpty->linkClicks() | rpl::start_with_next([=] { + _cancelSearch.fire({}); + }, _searchEmpty->lifetime()); + if (_controller->session().data().chatsListLoaded()) { + _searchEmpty->animate(); + } + } else if (_searchEmpty) { + _searchEmpty->show(); + } + } else { + _searchEmpty.destroy(); + } + const auto data = &session().data(); const auto state = !_shownList->empty() ? EmptyState::None @@ -3100,7 +3183,7 @@ void InnerWidget::refreshEmptyLabel() { return result; }); _empty.create(this, std::move(full), st::dialogsEmptyLabel); - resizeEmptyLabel(); + resizeEmpty(); _empty->overrideLinkClickHandler([=] { if (_emptyState == EmptyState::NoContacts) { _controller->showAddContact(); @@ -3114,13 +3197,16 @@ void InnerWidget::refreshEmptyLabel() { _empty->setVisible(_state == WidgetState::Default); } -void InnerWidget::resizeEmptyLabel() { - if (!_empty) { - return; +void InnerWidget::resizeEmpty() { + if (_empty) { + const auto skip = st::dialogsEmptySkip; + _empty->resizeToWidth(width() - 2 * skip); + _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); + } + if (_searchEmpty) { + _searchEmpty->resizeToWidth(width()); + _searchEmpty->move(0, searchedOffset()); } - const auto skip = st::dialogsEmptySkip; - _empty->resizeToWidth(width() - 2 * skip); - _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); } void InnerWidget::clearMouseSelection(bool clearSelection) { @@ -3481,7 +3567,7 @@ void InnerWidget::switchToFilter(FilterId filterId) { refreshShownList(); refreshWithCollapsedRows(true); } - refreshEmptyLabel(); + refreshEmpty(); { const auto skip = found // Don't save a scroll state for very flexible chat filters. diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 8cde8de924..a7e3ae619d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -60,6 +60,7 @@ class Row; class FakeRow; class IndexedList; class SearchTags; +class SearchEmpty; struct ChosenRow { Key key; @@ -119,8 +120,8 @@ public: void clearFilter(); void refresh(bool toTop = false); - void refreshEmptyLabel(); - void resizeEmptyLabel(); + void refreshEmpty(); + void resizeEmpty(); [[nodiscard]] bool isUserpicPress() const; [[nodiscard]] bool isUserpicPressOnWide() const; @@ -158,6 +159,7 @@ public: void setLoadMoreFilteredCallback(Fn callback); [[nodiscard]] rpl::producer<> listBottomReached() const; [[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const; + [[nodiscard]] rpl::producer<> cancelSearchRequests() const; [[nodiscard]] rpl::producer chosenRow() const; [[nodiscard]] rpl::producer<> updated() const; @@ -386,7 +388,6 @@ private: const Ui::Text::String &text) const; void refreshSearchInChatLabel(); void repaintSearchResult(int index); - void paintEmpty(QPainter &p, int top); Ui::VideoUserpic *validateVideoUserpic(not_null row); Ui::VideoUserpic *validateVideoUserpic(not_null history); @@ -395,7 +396,6 @@ private: void clearSearchResults(bool clearPeerSearchResults = true); void updateSelectedRow(Key key = Key()); void trackSearchResultsHistory(not_null history); - void trackSearchResultsForum(Data::Forum *forum); [[nodiscard]] QBrush currentBg() const; [[nodiscard]] Key computeChatPreviewRow() const; @@ -485,8 +485,11 @@ private: WidgetState _state = WidgetState::Default; + object_ptr _searchEmpty = { nullptr }; + SearchState _searchEmptyState; object_ptr _empty = { nullptr }; object_ptr _cancelSearchFromUser; + rpl::event_stream<> _cancelSearch; Ui::DraggingScrollManager _draggingScroll; @@ -526,6 +529,7 @@ private: bool _geometryInited = false; bool _savedSublists = false; + bool _searchLoading = false; base::unique_qptr _menu; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp index 8a8cf398cd..7c1be62aa9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp @@ -87,6 +87,7 @@ PeerData *Key::peer() const { [[nodiscard]] bool SearchState::empty() const { return !inChat + && tags.empty() && QStringView(query).trimmed().isEmpty(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index a9278e08c7..2f631288e2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -147,6 +147,4 @@ struct SearchState { const SearchState&) = default; }; -; - } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index db8724a61b..67155eb3df 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -345,6 +345,10 @@ Widget::Widget( } applySearchState(std::move(copy)); }, lifetime()); + _inner->cancelSearchRequests( + ) | rpl::start_with_next([=] { + applySearchState({}); + }, lifetime()); _inner->chosenRow( ) | rpl::start_with_next([=](const ChosenRow &row) { chosenRow(row); @@ -1988,6 +1992,9 @@ bool Widget::search(bool inCache) { auto result = false; const auto query = _searchState.query.trimmed(); + const auto trimmed = (query.isEmpty() || query[0] != '#') + ? query + : query.mid(1).trimmed(); const auto inPeer = searchInPeer(); const auto fromPeer = searchFromPeer(); const auto &inTags = searchInTags(); @@ -1995,9 +2002,7 @@ bool Widget::search(bool inCache) { const auto fromStartType = inPeer ? SearchRequestType::PeerFromStart : SearchRequestType::FromStart; - const auto skipRequest = (query.isEmpty() && !fromPeer && inTags.empty()) - || (tab == ChatSearchTab::PublicPosts && query.size() < 2); - if (skipRequest) { + if (trimmed.isEmpty() && !fromPeer && inTags.empty()) { cancelSearchRequest(); searchApplyEmpty(fromStartType, 0); _api.request(base::take(_peerSearchRequest)).cancel(); @@ -2021,10 +2026,7 @@ bool Widget::search(bool inCache) { _searchNextRate = 0; _searchFull = _searchFullMigrated = false; cancelSearchRequest(); - searchReceived( - fromStartType, - i->second, - 0); + searchReceived(fromStartType, i->second, 0); result = true; } } else if (_searchQuery != query diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp new file mode 100644 index 0000000000..855112f850 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp @@ -0,0 +1,82 @@ +/* +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/ui/chat_search_empty.h" + +#include "base/object_ptr.h" +#include "lottie/lottie_icon.h" +#include "settings/settings_common.h" +#include "ui/widgets/labels.h" +#include "styles/style_dialogs.h" + +namespace Dialogs { + +SearchEmpty::SearchEmpty( + QWidget *parent, + Icon icon, + rpl::producer text) +: RpWidget(parent) { + setup(icon, std::move(text)); +} + +void SearchEmpty::setMinimalHeight(int minimalHeight) { + const auto minimal = st::recentPeersEmptyHeightMin; + resize(width(), std::max(minimalHeight, minimal)); +} + +void SearchEmpty::setup(Icon icon, rpl::producer text) { + const auto label = Ui::CreateChild( + this, + std::move(text), + st::defaultPeerListAbout); + label->setClickHandlerFilter([=](const auto &, Qt::MouseButton button) { + if (button == Qt::LeftButton) { + _linkClicks.fire({}); + } + return false; + }); + const auto size = st::recentPeersEmptySize; + const auto animation = [&] { + switch (icon) { + case Icon::Search: return u"search"_q; + case Icon::NoResults: return u"noresults"_q; + } + Unexpected("Icon in SearchEmpty::setup."); + }(); + const auto [widget, animate] = Settings::CreateLottieIcon( + this, + { + .name = animation, + .sizeOverride = { size, size }, + }, + st::recentPeersEmptyMargin); + const auto animated = widget.data(); + + sizeValue() | rpl::start_with_next([=](QSize size) { + const auto padding = st::recentPeersEmptyMargin; + const auto paddings = padding.left() + padding.right(); + label->resizeToWidth(size.width() - paddings); + const auto x = (size.width() - animated->width()) / 2; + const auto y = (size.height() - animated->height()) / 3; + const auto top = y + animated->height() + st::recentPeersEmptySkip; + const auto sub = std::max(top + label->height() - size.height(), 0); + animated->move(x, y - sub); + label->move((size.width() - label->width()) / 2, top - sub); + }, lifetime()); + + _animate = [animate] { + animate(anim::repeat::once); + }; +} + +void SearchEmpty::animate() { + if (const auto onstack = _animate) { + onstack(); + } +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h new file mode 100644 index 0000000000..6ddf52676c --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h @@ -0,0 +1,43 @@ +/* +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 "ui/rp_widget.h" + +namespace Dialogs { + +enum class SearchEmptyIcon { + Search, + NoResults, +}; + +class SearchEmpty final : public Ui::RpWidget { +public: + using Icon = SearchEmptyIcon; + + SearchEmpty( + QWidget *parent, + Icon icon, + rpl::producer text); + + void setMinimalHeight(int minimalHeight); + [[nodiscard]] rpl::producer<> linkClicks() const { + return _linkClicks.events(); + } + + void animate(); + +private: + void setup(Icon icon, rpl::producer text); + + Fn _animate; + rpl::event_stream<> _linkClicks; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 868b3b74c0..9a86cca750 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -20,12 +20,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_user.h" +#include "dialogs/ui/chat_search_empty.h" #include "history/history.h" #include "lang/lang_keys.h" -#include "lottie/lottie_icon.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" @@ -1360,53 +1361,30 @@ object_ptr> Suggestions::setupRecentPeers( } object_ptr> Suggestions::setupEmptyRecent() { - return setupEmpty(_chatsContent, "search", tr::lng_recent_none()); + const auto icon = SearchEmptyIcon::Search; + return setupEmpty(_chatsContent, icon, tr::lng_recent_none()); } object_ptr> Suggestions::setupEmptyChannels() { - return setupEmpty( - _channelsContent, - "noresults", - tr::lng_channels_none_about()); + const auto icon = SearchEmptyIcon::NoResults; + return setupEmpty(_channelsContent, icon, tr::lng_channels_none_about()); } object_ptr> Suggestions::setupEmpty( not_null parent, - const QString &animation, + SearchEmptyIcon icon, rpl::producer text) { - auto content = object_ptr(parent); + auto content = object_ptr( + parent, + icon, + std::move(text) | Ui::Text::ToWithEntities()); + const auto raw = content.data(); - - const auto label = Ui::CreateChild( - raw, - std::move(text), - st::defaultPeerListAbout); - const auto size = st::recentPeersEmptySize; - const auto [widget, animate] = Settings::CreateLottieIcon( - raw, - { - .name = animation, - .sizeOverride = { size, size }, - }, - st::recentPeersEmptyMargin); - const auto icon = widget.data(); - rpl::combine( _chatsScroll->heightValue(), _topPeersWrap->heightValue() ) | rpl::start_with_next([=](int height, int top) { - raw->resize( - raw->width(), - std::max(height - top, st::recentPeersEmptyHeightMin)); - }, raw->lifetime()); - - raw->sizeValue() | rpl::start_with_next([=](QSize size) { - const auto x = (size.width() - icon->width()) / 2; - const auto y = (size.height() - icon->height()) / 3; - icon->move(x, y); - label->move( - (size.width() - label->width()) / 2, - y + icon->height() + st::recentPeersEmptySkip); + raw->setMinimalHeight(height - top); }, raw->lifetime()); auto result = object_ptr>( @@ -1417,7 +1395,7 @@ object_ptr> Suggestions::setupEmpty( result->toggledValue() | rpl::filter([=](bool shown) { return shown && _controller->session().data().chatsListLoaded(); }) | rpl::start_with_next([=] { - animate(anim::repeat::once); + raw->animate(); }, raw->lifetime()); return result; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index 947bf2fa9d..27b9b527fd 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -34,6 +34,8 @@ class SessionController; namespace Dialogs { +enum class SearchEmptyIcon; + struct RecentPeersList { std::vector> list; }; @@ -112,7 +114,7 @@ private: -> object_ptr>; [[nodiscard]] object_ptr> setupEmpty( not_null parent, - const QString &animation, + SearchEmptyIcon icon, rpl::producer text); void switchTab(Tab tab); diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 533fe85ef1..adf5b8f11e 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -85,6 +85,8 @@ PRIVATE data/data_subscription_option.h dialogs/dialogs_three_state_icon.h + dialogs/ui/chat_search_empty.cpp + dialogs/ui/chat_search_empty.h dialogs/ui/chat_search_tabs.cpp dialogs/ui/chat_search_tabs.h dialogs/ui/dialogs_stories_list.cpp