From 2ce2a142286ffe41de8141cf145a7f2ee324cf19 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 Mar 2017 21:38:50 +0300 Subject: [PATCH] Add global search by username in block user box. --- Telegram/SourceFiles/boxes/peer_list_box.cpp | 240 +++++++++++++++--- Telegram/SourceFiles/boxes/peer_list_box.h | 43 +++- .../settings_blocked_box_controller.cpp | 9 +- .../settings_blocked_box_controller.h | 1 + 4 files changed, 258 insertions(+), 35 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index d63f1f438..9233109c6 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -152,9 +152,9 @@ void PeerListBox::refreshRows() { _inner->refreshRows(); } -void PeerListBox::setSearchable(bool searchable) { - _inner->setSearchable(searchable); - if (searchable) { +void PeerListBox::setSearchMode(SearchMode mode) { + _inner->setSearchMode(mode); + if (mode != SearchMode::None) { if (!_select) { _select = createMultiSelect(); _select->entity()->setSubmittedCallback([this](bool chtrlShiftEnter) { _inner->submitted(); }); @@ -180,6 +180,18 @@ void PeerListBox::setSearchNoResults(object_ptr searchNoResults) _inner->setSearchNoResults(std::move(searchNoResults)); } +void PeerListBox::setSearchLoadingText(const QString &searchLoadingText) { + if (searchLoadingText.isEmpty()) { + setSearchLoading(nullptr); + } else { + setSearchLoading(object_ptr(this, searchLoadingText, Ui::FlatLabel::InitType::Simple, st::membersAbout)); + } +} + +void PeerListBox::setSearchLoading(object_ptr searchLoading) { + _inner->setSearchLoading(std::move(searchLoading)); +} + PeerListBox::Row::Row(PeerData *peer) : _peer(peer) { } @@ -294,14 +306,34 @@ void PeerListBox::Inner::appendRow(std::unique_ptr row) { } } +void PeerListBox::Inner::appendGlobalSearchRow(std::unique_ptr row) { + t_assert(showingSearch()); + if (_rowsByPeer.find(row->peer()) == _rowsByPeer.cend()) { + row->setAbsoluteIndex(_globalSearchRows.size()); + row->setIsGlobalSearchResult(true); + addRowEntry(row.get()); + _filterResults.push_back(row.get()); + _globalSearchRows.push_back(std::move(row)); + } +} + void PeerListBox::Inner::addRowEntry(Row *row) { _rowsByPeer.emplace(row->peer(), row); - if (_searchable) { + if (addingToSearchIndex()) { addToSearchIndex(row); } } +bool PeerListBox::Inner::addingToSearchIndex() const { + // If we started indexing already, we continue. + return (_searchMode != SearchMode::None) || !_searchIndex.empty(); +} + void PeerListBox::Inner::addToSearchIndex(Row *row) { + if (row->isGlobalSearchResult()) { + return; + } + removeFromSearchIndex(row); row->setNameFirstChars(row->peer()->chars); for_const (auto ch, row->nameFirstChars()) { @@ -348,20 +380,21 @@ PeerListBox::Row *PeerListBox::Inner::findRow(PeerData *peer) { void PeerListBox::Inner::removeRow(Row *row) { auto index = row->absoluteIndex(); - t_assert(index >= 0 && index < _rows.size()); - t_assert(_rows[index].get() == row); + auto isGlobalSearchResult = row->isGlobalSearchResult(); + auto &eraseFrom = isGlobalSearchResult ? _globalSearchRows : _rows; + + t_assert(index >= 0 && index < eraseFrom.size()); + t_assert(eraseFrom[index].get() == row); setSelected(Selected()); setPressed(Selected()); _rowsByPeer.erase(row->peer()); - if (_searchable) { - removeFromSearchIndex(row); - } + removeFromSearchIndex(row); _filterResults.erase(std::find(_filterResults.begin(), _filterResults.end(), row), _filterResults.end()); - _rows.erase(_rows.begin() + index); - for (auto i = index, count = int(_rows.size()); i != count; ++i) { - _rows[i]->setAbsoluteIndex(i); + eraseFrom.erase(eraseFrom.begin() + index); + for (auto i = index, count = int(eraseFrom.size()); i != count; ++i) { + eraseFrom[i]->setAbsoluteIndex(i); } restoreSelection(); @@ -379,16 +412,22 @@ void PeerListBox::Inner::setAbout(object_ptr about) { } int PeerListBox::Inner::labelHeight() const { - if (showingSearch()) { - if (_filterResults.empty() && _searchNoResults) { - return st::membersAboutLimitPadding.top() + _searchNoResults->height() + st::membersAboutLimitPadding.bottom(); + auto computeLabelHeight = [](auto &label) { + if (!label) { + return 0; } - return 0; + return st::membersAboutLimitPadding.top() + label->height() + st::membersAboutLimitPadding.bottom(); + }; + if (showingSearch()) { + if (!_filterResults.empty()) { + return 0; + } + if (globalSearchLoading()) { + return computeLabelHeight(_searchLoading); + } + return computeLabelHeight(_searchNoResults); } - if (_about) { - return st::membersAboutLimitPadding.top() + _about->height() + st::membersAboutLimitPadding.bottom(); - } - return 0; + return computeLabelHeight(_about); } void PeerListBox::Inner::refreshRows() { @@ -400,7 +439,11 @@ void PeerListBox::Inner::refreshRows() { } if (_searchNoResults) { _searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top()); - _searchNoResults->setVisible(showingSearch() && _filterResults.empty()); + _searchNoResults->setVisible(showingSearch() && _filterResults.empty() && !globalSearchLoading()); + } + if (_searchLoading) { + _searchLoading->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top()); + _searchLoading->setVisible(showingSearch() && _filterResults.empty() && globalSearchLoading()); } if (_visibleBottom > 0) { checkScrollForPreload(); @@ -408,13 +451,27 @@ void PeerListBox::Inner::refreshRows() { update(); } -void PeerListBox::Inner::setSearchable(bool searchable) { - // We don't destroy a search index if we have one already. - if (searchable && !_searchable) { - _searchable = true; - for_const (auto &row, _rows) { - addToSearchIndex(row.get()); +void PeerListBox::Inner::setSearchMode(SearchMode mode) { + if (_searchMode != mode) { + if (!addingToSearchIndex()) { + for_const (auto &row, _rows) { + addToSearchIndex(row.get()); + } } + _searchMode = mode; + if (_searchMode == SearchMode::Global) { + if (!_searchLoading) { + setSearchLoading(object_ptr(this, lang(lng_contacts_loading), Ui::FlatLabel::InitType::Simple, st::membersAbout)); + } + } else { + clearGlobalSearchRows(); + } + } +} + +void PeerListBox::Inner::clearGlobalSearchRows() { + while (!_globalSearchRows.empty()) { + removeRow(_globalSearchRows.back().get()); } } @@ -425,6 +482,13 @@ void PeerListBox::Inner::setSearchNoResults(object_ptr searchNoRe } } +void PeerListBox::Inner::setSearchLoading(object_ptr searchLoading) { + _searchLoading = std::move(searchLoading); + if (_searchLoading) { + _searchLoading->setParent(this); + } +} + void PeerListBox::Inner::paintEvent(QPaintEvent *e) { QRect r(e->rect()); Painter p(this); @@ -545,10 +609,37 @@ void PeerListBox::Inner::paintRow(Painter &p, TimeMs ms, RowIndex index) { p.drawTextRight(actionRight, actionTop, width(), row->action(), actionWidth); } - auto statusHasOnlineColor = (row->statusType() == Row::StatusType::Online); p.setFont(st::contactsStatusFont); - p.setPen(statusHasOnlineColor ? st::contactsStatusFgOnline : (selected ? st::contactsStatusFgOver : st::contactsStatusFg)); - p.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), row->status()); + if (row->isGlobalSearchResult() && !peer->userName().isEmpty()) { + auto username = peer->userName(); + if (!_globalSearchHighlight.isEmpty() && username.startsWith(_globalSearchHighlight, Qt::CaseInsensitive)) { + auto availableWidth = width() - namex - st::contactsPadding.right(); + auto highlightedPart = '@' + username.mid(0, _globalSearchHighlight.size()); + auto grayedPart = username.mid(_globalSearchHighlight.size()); + auto highlightedWidth = st::contactsStatusFont->width(highlightedPart); + if (highlightedWidth >= availableWidth || grayedPart.isEmpty()) { + if (highlightedWidth > availableWidth) { + highlightedPart = st::contactsStatusFont->elided(highlightedPart, availableWidth); + } + p.setPen(st::contactsStatusFgOnline); + p.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), highlightedPart); + } else { + grayedPart = st::contactsStatusFont->elided(grayedPart, availableWidth - highlightedWidth); + auto grayedWidth = st::contactsStatusFont->width(grayedPart); + p.setPen(st::contactsStatusFgOnline); + p.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), highlightedPart); + p.setPen(selected ? st::contactsStatusFgOver : st::contactsStatusFg); + p.drawTextLeft(namex + highlightedWidth, st::contactsPadding.top() + st::contactsStatusTop, width(), grayedPart); + } + } else { + p.setPen(st::contactsStatusFgOnline); + p.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), '@' + username); + } + } else { + auto statusHasOnlineColor = (row->statusType() == Row::StatusType::Online); + p.setPen(statusHasOnlineColor ? st::contactsStatusFgOnline : (selected ? st::contactsStatusFgOver : st::contactsStatusFg)); + p.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), row->status()); + } } void PeerListBox::Inner::selectSkip(int direction) { @@ -664,6 +755,7 @@ void PeerListBox::Inner::searchQueryChanged(QString query) { _searchQuery = query; _filterResults.clear(); + clearGlobalSearchRows(); if (!searchWordsList.isEmpty()) { auto minimalList = (const std::vector*)nullptr; for_const (auto &searchWord, searchWordsList) { @@ -703,11 +795,96 @@ void PeerListBox::Inner::searchQueryChanged(QString query) { } } } + if (_searchMode == SearchMode::Global) { + needGlobalSearch(); + } refreshRows(); restoreSelection(); } } +void PeerListBox::Inner::needGlobalSearch() { + if (!globalSearchInCache()) { + if (!_globalSearchTimer) { + _globalSearchTimer = object_ptr(this); + _globalSearchTimer->setTimeoutHandler([this] { globalSearchOnServer(); }); + } + _globalSearchTimer->start(AutoSearchTimeout); + } +} + +bool PeerListBox::Inner::globalSearchInCache() { + auto it = _globalSearchCache.find(_searchQuery); + if (it != _globalSearchCache.cend()) { + _globalSearchQuery = _searchQuery; + _globalSearchRequestId = 0; + globalSearchDone(it->second, _globalSearchRequestId); + return true; + } + return false; +} + +void PeerListBox::Inner::globalSearchOnServer() { + _globalSearchQuery = _searchQuery; + _globalSearchRequestId = MTP::send(MTPcontacts_Search(MTP_string(_globalSearchQuery), MTP_int(SearchPeopleLimit)), ::rpcDone(base::lambda_guarded(this, [this](const MTPcontacts_Found &result, mtpRequestId requestId) { + globalSearchDone(result, requestId); + })), ::rpcFail(base::lambda_guarded(this, [this](const RPCError &error, mtpRequestId requestId) { + return globalSearchFail(error, requestId); + }))); + _globalSearchQueries.emplace(_globalSearchRequestId, _globalSearchQuery); +} + +void PeerListBox::Inner::globalSearchDone(const MTPcontacts_Found &result, mtpRequestId requestId) { + auto query = _globalSearchQuery; + auto it = _globalSearchQueries.find(requestId); + if (it != _globalSearchQueries.cend()) { + query = it->second; + _globalSearchCache[query] = result; + _globalSearchQueries.erase(it); + } + + if (_globalSearchRequestId == requestId) { + _globalSearchRequestId = 0; + if (result.type() == mtpc_contacts_found) { + auto &contacts = result.c_contacts_found(); + App::feedUsers(contacts.vusers); + App::feedChats(contacts.vchats); + + _globalSearchHighlight = query; + if (!_globalSearchHighlight.isEmpty() && _globalSearchHighlight[0] == '@') { + _globalSearchHighlight = _globalSearchHighlight.mid(1); + } + + for_const (auto &mtpPeer, contacts.vresults.v) { + if (auto peer = App::peerLoaded(peerFromMTP(mtpPeer))) { + if (findRow(peer)) { + continue; + } + if (auto row = _controller->createGlobalRow(peer)) { + appendGlobalSearchRow(std::move(row)); + } + } + } + } + refreshRows(); + updateSelection(); + } +} + +bool PeerListBox::Inner::globalSearchFail(const RPCError &error, mtpRequestId requestId) { + if (MTP::isDefaultHandledError(error)) return false; + + if (_globalSearchRequestId == requestId) { + _globalSearchRequestId = 0; + refreshRows(); + } + return true; +} + +bool PeerListBox::Inner::globalSearchLoading() const { + return (_globalSearchTimer && _globalSearchTimer->isActive()) || _globalSearchRequestId; +} + void PeerListBox::Inner::submitted() { if (auto row = getRow(_selected.index)) { _controller->rowClicked(row->peer()); @@ -834,6 +1011,7 @@ PeerListBox::Row *PeerListBox::Inner::getRow(RowIndex index) { PeerListBox::Inner::RowIndex PeerListBox::Inner::findRowIndex(Row *row, RowIndex hint) { if (!showingSearch()) { + t_assert(!row->isGlobalSearchResult()); return RowIndex(row->absoluteIndex()); } @@ -854,7 +1032,7 @@ PeerListBox::Inner::RowIndex PeerListBox::Inner::findRowIndex(Row *row, RowIndex void PeerListBox::Inner::onPeerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars) { if (auto row = findRow(peer)) { - if (_searchable) { + if (addingToSearchIndex()) { addToSearchIndex(row); } row->refreshName(); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 6e2ec4360..07d296e35 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -83,6 +83,12 @@ public: bool disabled() const { return _disabled; } + bool isGlobalSearchResult() const { + return _isGlobalSearchResult; + } + void setIsGlobalSearchResult(bool isGlobalSearchResult) { + _isGlobalSearchResult = isGlobalSearchResult; + } template void addRipple(QSize size, QPoint point, UpdateCallback updateCallback); @@ -110,6 +116,7 @@ public: bool _disabled = false; int _absoluteIndex = -1; OrderedSet _nameFirstChars; + bool _isGlobalSearchResult = false; }; @@ -121,6 +128,9 @@ public: } virtual void preloadRows() { } + virtual std::unique_ptr createGlobalRow(PeerData *peer) { + return std::unique_ptr(); + } virtual ~Controller() = default; @@ -152,9 +162,16 @@ public: void setAboutText(const QString &aboutText); void setAbout(object_ptr about); void refreshRows(); - void setSearchable(bool searchable); + enum class SearchMode { + None, + Local, + Global, + }; + void setSearchMode(SearchMode mode); void setSearchNoResultsText(const QString &noResultsText); void setSearchNoResults(object_ptr searchNoResults); + void setSearchLoadingText(const QString &searchLoadingText); + void setSearchLoading(object_ptr searchLoading); // callback takes two iterators, like [](auto &begin, auto &end). template @@ -212,8 +229,9 @@ public: int fullRowsCount() const; void setAbout(object_ptr about); void refreshRows(); - void setSearchable(bool searchable); + void setSearchMode(SearchMode mode); void setSearchNoResults(object_ptr searchNoResults); + void setSearchLoading(object_ptr searchLoading); template void reorderRows(ReorderCallback &&callback) { @@ -241,6 +259,7 @@ protected: private: void refreshIndices(); + void appendGlobalSearchRow(std::unique_ptr row); struct RowIndex { RowIndex() = default; @@ -289,6 +308,7 @@ private: void addRowEntry(Row *row); void addToSearchIndex(Row *row); + bool addingToSearchIndex() const; void removeFromSearchIndex(Row *row); bool showingSearch() const { return !_searchQuery.isEmpty(); @@ -303,6 +323,14 @@ private: int labelHeight() const; + void needGlobalSearch(); + bool globalSearchInCache(); + void globalSearchOnServer(); + void globalSearchDone(const MTPcontacts_Found &result, mtpRequestId requestId); + bool globalSearchFail(const RPCError &error, mtpRequestId requestId); + bool globalSearchLoading() const; + void clearGlobalSearchRows(); + Controller *_controller = nullptr; int _rowHeight = 0; int _visibleTop = 0; @@ -315,14 +343,23 @@ private: std::vector> _rows; std::map _rowsByPeer; - bool _searchable = false; + SearchMode _searchMode = SearchMode::None; std::map> _searchIndex; QString _searchQuery; std::vector _filterResults; object_ptr _about = { nullptr }; object_ptr _searchNoResults = { nullptr }; + object_ptr _searchLoading = { nullptr }; QPoint _lastMousePosition; + std::vector> _globalSearchRows; + object_ptr _globalSearchTimer = { nullptr }; + QString _globalSearchQuery; + QString _globalSearchHighlight; + mtpRequestId _globalSearchRequestId = 0; + std::map _globalSearchCache; + std::map _globalSearchQueries; + }; diff --git a/Telegram/SourceFiles/settings/settings_blocked_box_controller.cpp b/Telegram/SourceFiles/settings/settings_blocked_box_controller.cpp index 5a99fb652..9bfcfe8e4 100644 --- a/Telegram/SourceFiles/settings/settings_blocked_box_controller.cpp +++ b/Telegram/SourceFiles/settings/settings_blocked_box_controller.cpp @@ -166,7 +166,7 @@ std::unique_ptr BlockedBoxController::createRow(UserData *user void BlockUserBoxController::prepare() { view()->setTitle(lang(lng_blocked_list_add_title)); view()->addButton(lang(lng_cancel), [this] { view()->closeBox(); }); - view()->setSearchable(true); + view()->setSearchMode(PeerListBox::SearchMode::Global); view()->setSearchNoResultsText(lang(lng_blocked_list_not_found)); rebuildRows(); @@ -249,6 +249,13 @@ void BlockUserBoxController::rowClicked(PeerData *peer) { view()->closeBox(); } +std::unique_ptr BlockUserBoxController::createGlobalRow(PeerData *peer) { + if (auto user = peer->asUser()) { + return createRow(App::history(user)); + } + return std::unique_ptr(); +} + bool BlockUserBoxController::appendRow(History *history) { if (auto row = view()->findRow(history->peer)) { updateIsBlocked(row, history->peer->asUser()); diff --git a/Telegram/SourceFiles/settings/settings_blocked_box_controller.h b/Telegram/SourceFiles/settings/settings_blocked_box_controller.h index 3a92d6e37..a30c732d8 100644 --- a/Telegram/SourceFiles/settings/settings_blocked_box_controller.h +++ b/Telegram/SourceFiles/settings/settings_blocked_box_controller.h @@ -50,6 +50,7 @@ class BlockUserBoxController : public QObject, public PeerListBox::Controller, p public: void prepare() override; void rowClicked(PeerData *peer) override; + std::unique_ptr createGlobalRow(PeerData *peer) override; private: void rebuildRows();