| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760 |
- /*
- 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 "boxes/peers/edit_peer_requests_box.h"
- #include "api/api_invite_links.h"
- #include "apiwrap.h"
- #include "base/unixtime.h"
- #include "boxes/peer_list_controllers.h"
- #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration
- #include "boxes/peers/edit_peer_invite_link.h" // PrepareRequestedRowStatus
- #include "boxes/peers/edit_peer_requests_box.h"
- #include "data/data_channel.h"
- #include "data/data_chat.h"
- #include "data/data_peer.h"
- #include "data/data_session.h"
- #include "data/data_user.h"
- #include "history/view/history_view_requests_bar.h" // kRecentRequestsLimit
- #include "info/info_controller.h"
- #include "info/info_memento.h"
- #include "info/requests_list/info_requests_list_widget.h"
- #include "lang/lang_keys.h"
- #include "main/main_session.h"
- #include "mtproto/sender.h"
- #include "ui/effects/ripple_animation.h"
- #include "ui/painter.h"
- #include "ui/round_rect.h"
- #include "ui/text/text_utilities.h"
- #include "window/window_session_controller.h"
- #include "styles/style_boxes.h"
- namespace {
- constexpr auto kFirstPageCount = 16;
- constexpr auto kPerPage = 200;
- constexpr auto kServerSearchDelay = crl::time(1000);
- constexpr auto kAcceptButton = 1;
- constexpr auto kRejectButton = 2;
- class RowDelegate {
- public:
- [[nodiscard]] virtual QSize rowAcceptButtonSize() = 0;
- [[nodiscard]] virtual QSize rowRejectButtonSize() = 0;
- virtual void rowPaintAccept(
- Painter &p,
- QRect geometry,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- int outerWidth,
- bool over) = 0;
- virtual void rowPaintReject(
- Painter &p,
- QRect geometry,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- int outerWidth,
- bool over) = 0;
- };
- class Row final : public PeerListRow {
- public:
- Row(
- not_null<RowDelegate*> delegate,
- not_null<UserData*> user,
- TimeId date);
- int elementsCount() const override;
- QRect elementGeometry(int element, int outerWidth) const override;
- bool elementDisabled(int element) const override;
- bool elementOnlySelect(int element) const override;
- void elementAddRipple(
- int element,
- QPoint point,
- Fn<void()> updateCallback) override;
- void elementsStopLastRipple() override;
- void elementsPaint(
- Painter &p,
- int outerWidth,
- bool selected,
- int selectedElement) override;
- private:
- const not_null<RowDelegate*> _delegate;
- std::unique_ptr<Ui::RippleAnimation> _acceptRipple;
- std::unique_ptr<Ui::RippleAnimation> _rejectRipple;
- };
- Row::Row(
- not_null<RowDelegate*> delegate,
- not_null<UserData*> user,
- TimeId date)
- : PeerListRow(user)
- , _delegate(delegate) {
- setCustomStatus(PrepareRequestedRowStatus(date));
- }
- int Row::elementsCount() const {
- return 2;
- }
- QRect Row::elementGeometry(int element, int outerWidth) const {
- switch (element) {
- case kAcceptButton: {
- const auto size = _delegate->rowAcceptButtonSize();
- return QRect(st::requestAcceptPosition, size);
- } break;
- case kRejectButton: {
- const auto accept = _delegate->rowAcceptButtonSize();
- const auto size = _delegate->rowRejectButtonSize();
- return QRect(
- (st::requestAcceptPosition
- + QPoint(accept.width() + st::requestButtonsSkip, 0)),
- size);
- } break;
- }
- return QRect();
- }
- bool Row::elementDisabled(int element) const {
- return false;
- }
- bool Row::elementOnlySelect(int element) const {
- return true;
- }
- void Row::elementAddRipple(
- int element,
- QPoint point,
- Fn<void()> updateCallback) {
- const auto pointer = (element == kAcceptButton)
- ? &_acceptRipple
- : (element == kRejectButton)
- ? &_rejectRipple
- : nullptr;
- if (!pointer) {
- return;
- }
- auto &ripple = *pointer;
- if (!ripple) {
- auto mask = Ui::RippleAnimation::RoundRectMask(
- (element == kAcceptButton
- ? _delegate->rowAcceptButtonSize()
- : _delegate->rowRejectButtonSize()),
- st::buttonRadius);
- ripple = std::make_unique<Ui::RippleAnimation>(
- (element == kAcceptButton
- ? st::requestsAcceptButton.ripple
- : st::requestsRejectButton.ripple),
- std::move(mask),
- std::move(updateCallback));
- }
- ripple->add(point);
- }
- void Row::elementsStopLastRipple() {
- if (_acceptRipple) {
- _acceptRipple->lastStop();
- }
- if (_rejectRipple) {
- _rejectRipple->lastStop();
- }
- }
- void Row::elementsPaint(
- Painter &p,
- int outerWidth,
- bool selected,
- int selectedElement) {
- const auto accept = elementGeometry(kAcceptButton, outerWidth);
- const auto reject = elementGeometry(kRejectButton, outerWidth);
- const auto over = [&](int element) {
- return (selectedElement == element);
- };
- _delegate->rowPaintAccept(
- p,
- accept,
- _acceptRipple,
- outerWidth,
- over(kAcceptButton));
- _delegate->rowPaintReject(
- p,
- reject,
- _rejectRipple,
- outerWidth,
- over(kRejectButton));
- }
- } // namespace
- class RequestsBoxController::RowHelper final : public RowDelegate {
- public:
- explicit RowHelper(bool isGroup);
- [[nodiscard]] QSize rowAcceptButtonSize() override;
- [[nodiscard]] QSize rowRejectButtonSize() override;
- void rowPaintAccept(
- Painter &p,
- QRect geometry,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- int outerWidth,
- bool over) override;
- void rowPaintReject(
- Painter &p,
- QRect geometry,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- int outerWidth,
- bool over) override;
- private:
- void paintButton(
- Painter &p,
- QRect geometry,
- const style::RoundButton &st,
- const Ui::RoundRect &rect,
- const Ui::RoundRect &rectOver,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- const QString &text,
- int textWidth,
- int outerWidth,
- bool over);
- Ui::RoundRect _acceptRect;
- Ui::RoundRect _acceptRectOver;
- Ui::RoundRect _rejectRect;
- Ui::RoundRect _rejectRectOver;
- QString _acceptText;
- QString _rejectText;
- int _acceptTextWidth = 0;
- int _rejectTextWidth = 0;
- };
- RequestsBoxController::RowHelper::RowHelper(bool isGroup)
- : _acceptRect(st::buttonRadius, st::requestsAcceptButton.textBg)
- , _acceptRectOver(st::buttonRadius, st::requestsAcceptButton.textBgOver)
- , _rejectRect(st::buttonRadius, st::requestsRejectButton.textBg)
- , _rejectRectOver(st::buttonRadius, st::requestsRejectButton.textBgOver)
- , _acceptText(isGroup
- ? tr::lng_group_requests_add(tr::now)
- : tr::lng_group_requests_add_channel(tr::now))
- , _rejectText(tr::lng_group_requests_dismiss(tr::now))
- , _acceptTextWidth(st::requestsAcceptButton.style.font->width(_acceptText))
- , _rejectTextWidth(st::requestsRejectButton.style.font->width(_rejectText)) {
- }
- RequestsBoxController::RequestsBoxController(
- not_null<Window::SessionNavigation*> navigation,
- not_null<PeerData*> peer)
- : PeerListController(CreateSearchController(peer))
- , _navigation(navigation)
- , _helper(std::make_unique<RowHelper>(!peer->isBroadcast()))
- , _peer(peer)
- , _api(&_peer->session().mtp()) {
- setStyleOverrides(&st::requestsBoxList);
- subscribeToMigration();
- }
- RequestsBoxController::~RequestsBoxController() = default;
- void RequestsBoxController::Start(
- not_null<Window::SessionNavigation*> navigation,
- not_null<PeerData*> peer) {
- navigation->showSection(
- std::make_shared<Info::Memento>(
- peer->migrateToOrMe(),
- Info::Section::Type::RequestsList));
- }
- Main::Session &RequestsBoxController::session() const {
- return _peer->session();
- }
- auto RequestsBoxController::CreateSearchController(not_null<PeerData*> peer)
- -> std::unique_ptr<PeerListSearchController> {
- return std::make_unique<RequestsBoxSearchController>(peer);
- }
- std::unique_ptr<PeerListRow> RequestsBoxController::createSearchRow(
- not_null<PeerData*> peer) {
- if (const auto user = peer->asUser()) {
- return createRow(user);
- }
- return nullptr;
- }
- std::unique_ptr<PeerListRow> RequestsBoxController::createRestoredRow(
- not_null<PeerData*> peer) {
- if (const auto user = peer->asUser()) {
- return createRow(user, _dates[user]);
- }
- return nullptr;
- }
- auto RequestsBoxController::saveState() const
- -> std::unique_ptr<PeerListState> {
- auto result = PeerListController::saveState();
- auto my = std::make_unique<SavedState>();
- my->dates = _dates;
- my->offsetDate = _offsetDate;
- my->offsetUser = _offsetUser;
- my->allLoaded = _allLoaded;
- my->wasLoading = (_loadRequestId != 0);
- if (const auto search = searchController()) {
- my->searchState = search->saveState();
- }
- result->controllerState = std::move(my);
- return result;
- }
- void RequestsBoxController::restoreState(
- std::unique_ptr<PeerListState> state) {
- auto typeErasedState = state
- ? state->controllerState.get()
- : nullptr;
- if (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {
- if (const auto requestId = base::take(_loadRequestId)) {
- _api.request(requestId).cancel();
- }
- _dates = std::move(my->dates);
- _offsetDate = my->offsetDate;
- _offsetUser = my->offsetUser;
- _allLoaded = my->allLoaded;
- if (const auto search = searchController()) {
- search->restoreState(std::move(my->searchState));
- }
- if (my->wasLoading) {
- loadMoreRows();
- }
- PeerListController::restoreState(std::move(state));
- if (delegate()->peerListFullRowsCount() || _allLoaded) {
- refreshDescription();
- delegate()->peerListRefreshRows();
- }
- }
- }
- void RequestsBoxController::prepare() {
- delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
- delegate()->peerListSetTitle(_peer->isBroadcast()
- ? tr::lng_manage_peer_requests_channel()
- : tr::lng_manage_peer_requests());
- setDescriptionText(tr::lng_contacts_loading(tr::now));
- setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
- loadMoreRows();
- }
- void RequestsBoxController::loadMoreRows() {
- if (searchController() && searchController()->loadMoreRows()) {
- return;
- } else if (_loadRequestId || _allLoaded) {
- return;
- }
- // First query is small and fast, next loads a lot of rows.
- const auto limit = _offsetDate ? kPerPage : kFirstPageCount;
- using Flag = MTPmessages_GetChatInviteImporters::Flag;
- _loadRequestId = _api.request(MTPmessages_GetChatInviteImporters(
- MTP_flags(Flag::f_requested),
- _peer->input,
- MTPstring(), // link
- MTPstring(), // q
- MTP_int(_offsetDate),
- _offsetUser ? _offsetUser->inputUser : MTP_inputUserEmpty(),
- MTP_int(limit)
- )).done([=](const MTPmessages_ChatInviteImporters &result) {
- const auto firstLoad = !_offsetDate;
- _loadRequestId = 0;
- result.match([&](const MTPDmessages_chatInviteImporters &data) {
- session().data().processUsers(data.vusers());
- const auto &importers = data.vimporters().v;
- auto &owner = _peer->owner();
- for (const auto &importer : importers) {
- importer.match([&](const MTPDchatInviteImporter &data) {
- _offsetDate = data.vdate().v;
- _offsetUser = owner.user(data.vuser_id());
- appendRow(_offsetUser, _offsetDate);
- });
- }
- // To be sure - wait for a whole empty result list.
- _allLoaded = importers.isEmpty();
- });
- if (_allLoaded
- || (firstLoad && delegate()->peerListFullRowsCount() > 0)) {
- refreshDescription();
- }
- delegate()->peerListRefreshRows();
- }).fail([=] {
- _loadRequestId = 0;
- _allLoaded = true;
- }).send();
- }
- void RequestsBoxController::refreshDescription() {
- setDescriptionText((delegate()->peerListFullRowsCount() > 0)
- ? QString()
- : _peer->isBroadcast()
- ? tr::lng_group_requests_none_channel(tr::now)
- : tr::lng_group_requests_none(tr::now));
- }
- void RequestsBoxController::rowClicked(not_null<PeerListRow*> row) {
- _navigation->showPeerInfo(row->peer());
- }
- void RequestsBoxController::rowElementClicked(
- not_null<PeerListRow*> row,
- int element) {
- processRequest(row->peer()->asUser(), (element == kAcceptButton));
- }
- void RequestsBoxController::processRequest(
- not_null<UserData*> user,
- bool approved) {
- const auto remove = [=] {
- if (const auto row = delegate()->peerListFindRow(user->id.value)) {
- delegate()->peerListRemoveRow(row);
- refreshDescription();
- delegate()->peerListRefreshRows();
- }
- static_cast<RequestsBoxSearchController*>(
- searchController())->removeFromCache(user);
- };
- const auto done = crl::guard(this, [=] {
- remove();
- if (approved) {
- delegate()->peerListUiShow()->showToast((_peer->isBroadcast()
- ? tr::lng_group_requests_was_added_channel
- : tr::lng_group_requests_was_added)(
- tr::now,
- lt_user,
- Ui::Text::Bold(user->name()),
- Ui::Text::WithEntities));
- }
- });
- const auto fail = crl::guard(this, remove);
- session().api().inviteLinks().processRequest(
- _peer,
- QString(), // link
- user,
- approved,
- done,
- fail);
- }
- void RequestsBoxController::appendRow(
- not_null<UserData*> user,
- TimeId date) {
- if (!delegate()->peerListFindRow(user->id.value)) {
- _dates.emplace(user, date);
- if (auto row = createRow(user, date)) {
- delegate()->peerListAppendRow(std::move(row));
- setDescriptionText(QString());
- }
- }
- }
- QSize RequestsBoxController::RowHelper::rowAcceptButtonSize() {
- const auto &st = st::requestsAcceptButton;
- return {
- (st.width <= 0) ? (_acceptTextWidth - st.width) : st.width,
- st.height,
- };
- }
- QSize RequestsBoxController::RowHelper::rowRejectButtonSize() {
- const auto &st = st::requestsRejectButton;
- return {
- (st.width <= 0) ? (_rejectTextWidth - st.width) : st.width,
- st.height,
- };
- }
- void RequestsBoxController::RowHelper::rowPaintAccept(
- Painter &p,
- QRect geometry,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- int outerWidth,
- bool over) {
- paintButton(
- p,
- geometry,
- st::requestsAcceptButton,
- _acceptRect,
- _acceptRectOver,
- ripple,
- _acceptText,
- _acceptTextWidth,
- outerWidth,
- over);
- }
- void RequestsBoxController::RowHelper::rowPaintReject(
- Painter &p,
- QRect geometry,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- int outerWidth,
- bool over) {
- paintButton(
- p,
- geometry,
- st::requestsRejectButton,
- _rejectRect,
- _rejectRectOver,
- ripple,
- _rejectText,
- _rejectTextWidth,
- outerWidth,
- over);
- }
- void RequestsBoxController::RowHelper::paintButton(
- Painter &p,
- QRect geometry,
- const style::RoundButton &st,
- const Ui::RoundRect &rect,
- const Ui::RoundRect &rectOver,
- std::unique_ptr<Ui::RippleAnimation> &ripple,
- const QString &text,
- int textWidth,
- int outerWidth,
- bool over) {
- rect.paint(p, geometry);
- if (over) {
- rectOver.paint(p, geometry);
- }
- if (ripple) {
- ripple->paint(p, geometry.x(), geometry.y(), outerWidth);
- if (ripple->empty()) {
- ripple = nullptr;
- }
- }
- const auto textLeft = geometry.x()
- + ((geometry.width() - textWidth) / 2);
- const auto textTop = geometry.y() + st.textTop;
- p.setFont(st.style.font);
- p.setPen(over ? st.textFgOver : st.textFg);
- p.drawTextLeft(textLeft, textTop, outerWidth, text);
- }
- std::unique_ptr<PeerListRow> RequestsBoxController::createRow(
- not_null<UserData*> user,
- TimeId date) {
- if (!date) {
- const auto search = static_cast<RequestsBoxSearchController*>(
- searchController());
- date = search->dateForUser(user);
- _dates.emplace(user, date);
- }
- return std::make_unique<Row>(_helper.get(), user, date);
- }
- void RequestsBoxController::subscribeToMigration() {
- const auto chat = _peer->asChat();
- if (!chat) {
- return;
- }
- SubscribeToMigration(
- chat,
- lifetime(),
- [=](not_null<ChannelData*> channel) { migrate(chat, channel); });
- }
- void RequestsBoxController::migrate(
- not_null<ChatData*> chat,
- not_null<ChannelData*> channel) {
- _peer = channel;
- }
- RequestsBoxSearchController::RequestsBoxSearchController(
- not_null<PeerData*> peer)
- : _peer(peer)
- , _api(&_peer->session().mtp()) {
- _timer.setCallback([=] { searchOnServer(); });
- }
- void RequestsBoxSearchController::searchQuery(const QString &query) {
- if (_query != query) {
- _query = query;
- _offsetDate = 0;
- _offsetUser = nullptr;
- _requestId = 0;
- _allLoaded = false;
- if (!_query.isEmpty() && !searchInCache()) {
- _timer.callOnce(kServerSearchDelay);
- } else {
- _timer.cancel();
- }
- }
- }
- void RequestsBoxSearchController::searchOnServer() {
- Expects(!_query.isEmpty());
- loadMoreRows();
- }
- bool RequestsBoxSearchController::isLoading() {
- return _timer.isActive() || _requestId;
- }
- void RequestsBoxSearchController::removeFromCache(not_null<UserData*> user) {
- for (auto &entry : _cache) {
- auto &items = entry.second.items;
- const auto j = ranges::remove(items, user, &Item::user);
- if (j != end(items)) {
- entry.second.requestedCount -= (end(items) - j);
- items.erase(j, end(items));
- }
- }
- }
- TimeId RequestsBoxSearchController::dateForUser(not_null<UserData*> user) {
- if (const auto i = _dates.find(user); i != end(_dates)) {
- return i->second;
- }
- return {};
- }
- auto RequestsBoxSearchController::saveState() const
- -> std::unique_ptr<SavedStateBase> {
- auto result = std::make_unique<SavedState>();
- result->query = _query;
- result->offsetDate = _offsetDate;
- result->offsetUser = _offsetUser;
- result->allLoaded = _allLoaded;
- result->wasLoading = (_requestId != 0);
- return result;
- }
- void RequestsBoxSearchController::restoreState(
- std::unique_ptr<SavedStateBase> state) {
- if (auto my = dynamic_cast<SavedState*>(state.get())) {
- if (auto requestId = base::take(_requestId)) {
- _api.request(requestId).cancel();
- }
- _cache.clear();
- _queries.clear();
- _allLoaded = my->allLoaded;
- _offsetDate = my->offsetDate;
- _offsetUser = my->offsetUser;
- _query = my->query;
- if (my->wasLoading) {
- searchOnServer();
- }
- }
- }
- bool RequestsBoxSearchController::searchInCache() {
- const auto i = _cache.find(_query);
- if (i != _cache.cend()) {
- _requestId = 0;
- searchDone(
- _requestId,
- i->second.items,
- i->second.requestedCount);
- return true;
- }
- return false;
- }
- bool RequestsBoxSearchController::loadMoreRows() {
- if (_query.isEmpty()) {
- return false;
- } else if (_allLoaded || isLoading()) {
- return true;
- }
- // For search we request a lot of rows from the first query.
- // (because we've waited for search request by timer already,
- // so we don't expect it to be fast, but we want to fill cache).
- const auto limit = kPerPage;
- using Flag = MTPmessages_GetChatInviteImporters::Flag;
- _requestId = _api.request(MTPmessages_GetChatInviteImporters(
- MTP_flags(Flag::f_requested | Flag::f_q),
- _peer->input,
- MTPstring(), // link
- MTP_string(_query),
- MTP_int(_offsetDate),
- _offsetUser ? _offsetUser->inputUser : MTP_inputUserEmpty(),
- MTP_int(limit)
- )).done([=](
- const MTPmessages_ChatInviteImporters &result,
- mtpRequestId requestId) {
- auto items = std::vector<Item>();
- result.match([&](const MTPDmessages_chatInviteImporters &data) {
- const auto &importers = data.vimporters().v;
- auto &owner = _peer->owner();
- owner.processUsers(data.vusers());
- items.reserve(importers.size());
- for (const auto &importer : importers) {
- importer.match([&](const MTPDchatInviteImporter &data) {
- items.push_back({
- owner.user(data.vuser_id()),
- data.vdate().v,
- });
- });
- }
- });
- searchDone(requestId, items, limit);
- auto it = _queries.find(requestId);
- if (it != _queries.cend()) {
- const auto &query = it->second.text;
- if (it->second.offsetDate == 0) {
- auto &entry = _cache[query];
- entry.items = std::move(items);
- entry.requestedCount = limit;
- }
- _queries.erase(it);
- }
- }).fail([=](const MTP::Error &error, mtpRequestId requestId) {
- if (_requestId == requestId) {
- _requestId = 0;
- _allLoaded = true;
- delegate()->peerListSearchRefreshRows();
- }
- }).send();
- auto entry = Query();
- entry.text = _query;
- entry.offsetDate = _offsetDate;
- _queries.emplace(_requestId, entry);
- return true;
- }
- void RequestsBoxSearchController::searchDone(
- mtpRequestId requestId,
- const std::vector<Item> &items,
- int requestedCount) {
- if (_requestId != requestId) {
- return;
- }
- _requestId = 0;
- if (!_offsetDate) {
- _dates.clear();
- }
- for (const auto &[user, date] : items) {
- _offsetDate = date;
- _offsetUser = user;
- _dates.emplace(user, date);
- delegate()->peerListSearchAddRow(user);
- }
- if (items.size() < requestedCount) {
- // We want cache to have full information about a query with
- // small results count (that we don't need the second request).
- // So we don't wait for empty list unlike the non-search case.
- _allLoaded = true;
- }
- delegate()->peerListSearchRefreshRows();
- }
|