/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "boxes/peer_list_box.h" #include "styles/style_boxes.h" #include "styles/style_dialogs.h" #include "ui/widgets/scroll_area.h" #include "ui/effects/ripple_animation.h" #include "observer_peer.h" #include "auth_session.h" #include "mainwidget.h" #include "storage/file_download.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/labels.h" #include "ui/effects/widget_slide_wrap.h" #include "lang.h" PeerListBox::PeerListBox(QWidget*, std::unique_ptr controller) : _controller(std::move(controller)) { } object_ptr> PeerListBox::createMultiSelect() { auto entity = object_ptr(this, st::contactsMultiSelect, lang(lng_participant_filter)); auto margins = style::margins(0, 0, 0, 0); auto callback = [this] { updateScrollSkips(); }; return object_ptr>(this, std::move(entity), margins, std::move(callback)); } int PeerListBox::getTopScrollSkip() const { auto result = 0; if (_select && !_select->isHidden()) { result += _select->height(); } return result; } void PeerListBox::updateScrollSkips() { setInnerTopSkip(getTopScrollSkip(), true); } void PeerListBox::prepare() { _inner = setInnerWidget(object_ptr(this, _controller.get()), st::boxLayerScroll); _controller->setView(this); setDimensions(st::boxWideWidth, st::boxMaxListHeight); if (_select) { _select->finishAnimation(); onScrollToY(0); } connect(_inner, SIGNAL(mustScrollTo(int, int)), this, SLOT(onScrollToY(int, int))); } void PeerListBox::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Down) { _inner->selectSkip(1); } else if (e->key() == Qt::Key_Up) { _inner->selectSkip(-1); } else if (e->key() == Qt::Key_PageDown) { _inner->selectSkipPage(height(), 1); } else if (e->key() == Qt::Key_PageUp) { _inner->selectSkipPage(height(), -1); } else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) { _select->entity()->clearQuery(); } else { BoxContent::keyPressEvent(e); } } void PeerListBox::searchQueryChanged(const QString &query) { onScrollToY(0); _inner->searchQueryChanged(query); } void PeerListBox::resizeEvent(QResizeEvent *e) { BoxContent::resizeEvent(e); if (_select) { _select->resizeToWidth(width()); _select->moveToLeft(0, 0); updateScrollSkips(); } _inner->resize(width(), _inner->height()); } void PeerListBox::setInnerFocus() { if (!_select || _select->isHidden()) { _inner->setFocus(); } else { _select->entity()->setInnerFocus(); } } void PeerListBox::appendRow(std::unique_ptr row) { _inner->appendRow(std::move(row)); } void PeerListBox::prependRow(std::unique_ptr row) { _inner->prependRow(std::move(row)); } PeerListBox::Row *PeerListBox::findRow(PeerData *peer) { return _inner->findRow(peer); } void PeerListBox::updateRow(Row *row) { _inner->updateRow(row); } void PeerListBox::removeRow(Row *row) { _inner->removeRow(row); } int PeerListBox::fullRowsCount() const { return _inner->fullRowsCount(); } void PeerListBox::setAboutText(const QString &aboutText) { if (aboutText.isEmpty()) { setAbout(nullptr); } else { setAbout(object_ptr(this, aboutText, Ui::FlatLabel::InitType::Simple, st::membersAbout)); } } void PeerListBox::setAbout(object_ptr about) { _inner->setAbout(std::move(about)); } void PeerListBox::refreshRows() { _inner->refreshRows(); } void PeerListBox::setSearchable(bool searchable) { _inner->setSearchable(searchable); if (searchable) { if (!_select) { _select = createMultiSelect(); _select->entity()->setSubmittedCallback([this](bool chtrlShiftEnter) { _inner->submitted(); }); _select->entity()->setQueryChangedCallback([this](const QString &query) { searchQueryChanged(query); }); _select->resizeToWidth(width()); _select->moveToLeft(0, 0); } _select->slideDown(); } else if (_select) { _select->slideUp(); } } void PeerListBox::setSearchNoResultsText(const QString &searchNoResultsText) { if (searchNoResultsText.isEmpty()) { setSearchNoResults(nullptr); } else { setSearchNoResults(object_ptr(this, searchNoResultsText, Ui::FlatLabel::InitType::Simple, st::membersAbout)); } } void PeerListBox::setSearchNoResults(object_ptr searchNoResults) { _inner->setSearchNoResults(std::move(searchNoResults)); } PeerListBox::Row::Row(PeerData *peer) : _peer(peer) { } void PeerListBox::Row::setDisabled(bool disabled) { _disabled = disabled; } void PeerListBox::Row::setActionLink(const QString &action) { _action = action; refreshActionLink(); } void PeerListBox::Row::refreshActionLink() { if (!_initialized) return; _actionWidth = _action.isEmpty() ? 0 : st::normalFont->width(_action); } void PeerListBox::Row::setCustomStatus(const QString &status) { _status = status; _statusType = StatusType::Custom; } void PeerListBox::Row::clearCustomStatus() { _statusType = StatusType::Online; refreshStatus(); } void PeerListBox::Row::refreshStatus() { if (!_initialized || _statusType == StatusType::Custom) { return; } if (auto user = peer()->asUser()) { auto time = unixtime(); _status = App::onlineText(user, time); _statusType = App::onlineColorUse(user, time) ? StatusType::Online : StatusType::LastSeen; } } PeerListBox::Row::StatusType PeerListBox::Row::statusType() const { return _statusType; } void PeerListBox::Row::refreshName() { if (!_initialized) { return; } _name.setText(st::contactsNameStyle, peer()->name, _textNameOptions); } QString PeerListBox::Row::status() const { return _status; } QString PeerListBox::Row::action() const { return _action; } int PeerListBox::Row::actionWidth() const { return _actionWidth; } PeerListBox::Row::~Row() = default; template void PeerListBox::Row::addRipple(QSize size, QPoint point, UpdateCallback updateCallback) { if (!_ripple) { auto mask = Ui::RippleAnimation::rectMask(size); _ripple = std::make_unique(st::contactsRipple, std::move(mask), std::move(updateCallback)); } _ripple->add(point); } void PeerListBox::Row::stopLastRipple() { if (_ripple) { _ripple->lastStop(); } } void PeerListBox::Row::paintRipple(Painter &p, int x, int y, int outerWidth, TimeMs ms) { if (_ripple) { _ripple->paint(p, x, y, outerWidth, ms); if (_ripple->empty()) { _ripple.reset(); } } } void PeerListBox::Row::lazyInitialize() { if (_initialized) { return; } _initialized = true; refreshActionLink(); refreshName(); refreshStatus(); } PeerListBox::Inner::Inner(QWidget *parent, Controller *controller) : TWidget(parent) , _controller(controller) , _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom()) { subscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] { update(); }); connect(App::main(), SIGNAL(peerNameChanged(PeerData*, const PeerData::Names&, const PeerData::NameFirstChars&)), this, SLOT(onPeerNameChanged(PeerData*, const PeerData::Names&, const PeerData::NameFirstChars&))); connect(App::main(), SIGNAL(peerPhotoChanged(PeerData*)), this, SLOT(peerUpdated(PeerData*))); } void PeerListBox::Inner::appendRow(std::unique_ptr row) { if (_rowsByPeer.find(row->peer()) == _rowsByPeer.cend()) { row->setAbsoluteIndex(_rows.size()); addRowEntry(row.get()); _rows.push_back(std::move(row)); } } void PeerListBox::Inner::addRowEntry(Row *row) { _rowsByPeer.emplace(row->peer(), row); if (_searchable) { addToSearchIndex(row); } } void PeerListBox::Inner::addToSearchIndex(Row *row) { removeFromSearchIndex(row); row->setNameFirstChars(row->peer()->chars); for_const (auto ch, row->nameFirstChars()) { _searchIndex[ch].push_back(row); } } void PeerListBox::Inner::removeFromSearchIndex(Row *row) { auto &nameFirstChars = row->nameFirstChars(); if (!nameFirstChars.empty()) { for_const (auto ch, row->nameFirstChars()) { auto it = _searchIndex.find(ch); if (it != _searchIndex.cend()) { auto &entry = it->second; entry.erase(std::remove(entry.begin(), entry.end(), row), entry.end()); if (entry.empty()) { _searchIndex.erase(it); } } } row->setNameFirstChars(OrderedSet()); } } void PeerListBox::Inner::prependRow(std::unique_ptr row) { if (_rowsByPeer.find(row->peer()) == _rowsByPeer.cend()) { addRowEntry(row.get()); _rows.insert(_rows.begin(), std::move(row)); refreshIndices(); } } void PeerListBox::Inner::refreshIndices() { auto index = 0; for (auto &row : _rows) { row->setAbsoluteIndex(index++); } } PeerListBox::Row *PeerListBox::Inner::findRow(PeerData *peer) { auto it = _rowsByPeer.find(peer); return (it == _rowsByPeer.cend()) ? nullptr : it->second; } void PeerListBox::Inner::removeRow(Row *row) { auto index = row->absoluteIndex(); t_assert(index >= 0 && index < _rows.size()); t_assert(_rows[index].get() == row); setSelected(Selected()); setPressed(Selected()); _rowsByPeer.erase(row->peer()); if (_searchable) { 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); } restoreSelection(); } int PeerListBox::Inner::fullRowsCount() const { return _rows.size(); } void PeerListBox::Inner::setAbout(object_ptr about) { _about = std::move(about); if (_about) { _about->setParent(this); } } int PeerListBox::Inner::labelHeight() const { if (showingSearch()) { if (_filterResults.empty() && _searchNoResults) { return st::membersAboutLimitPadding.top() + _searchNoResults->height() + st::membersAboutLimitPadding.bottom(); } return 0; } if (_about) { return st::membersAboutLimitPadding.top() + _about->height() + st::membersAboutLimitPadding.bottom(); } return 0; } void PeerListBox::Inner::refreshRows() { auto labelTop = st::membersMarginTop + qMax(1, shownRowsCount()) * _rowHeight; resize(width(), labelTop + labelHeight() + st::membersMarginBottom); if (_about) { _about->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top()); _about->setVisible(!showingSearch()); } if (_searchNoResults) { _searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top()); _searchNoResults->setVisible(showingSearch() && _filterResults.empty()); } if (_visibleBottom > 0) { checkScrollForPreload(); } 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::setSearchNoResults(object_ptr searchNoResults) { _searchNoResults = std::move(searchNoResults); if (_searchNoResults) { _searchNoResults->setParent(this); } } void PeerListBox::Inner::paintEvent(QPaintEvent *e) { QRect r(e->rect()); Painter p(this); p.fillRect(r, st::contactsBg); auto ms = getms(); auto yFrom = r.y() - st::membersMarginTop; auto yTo = r.y() + r.height() - st::membersMarginTop; p.translate(0, st::membersMarginTop); auto count = shownRowsCount(); if (count > 0) { auto from = floorclamp(yFrom, _rowHeight, 0, count); auto to = ceilclamp(yTo, _rowHeight, 0, count); p.translate(0, from * _rowHeight); for (auto index = from; index != to; ++index) { paintRow(p, ms, RowIndex(index)); p.translate(0, _rowHeight); } } } void PeerListBox::Inner::enterEventHook(QEvent *e) { setMouseTracking(true); } void PeerListBox::Inner::leaveEventHook(QEvent *e) { _mouseSelection = false; setMouseTracking(false); setSelected(Selected()); } void PeerListBox::Inner::mouseMoveEvent(QMouseEvent *e) { auto position = e->globalPos(); if (_mouseSelection || _lastMousePosition != position) { _lastMousePosition = position; _mouseSelection = true; updateSelection(); } } void PeerListBox::Inner::mousePressEvent(QMouseEvent *e) { _mouseSelection = true; _lastMousePosition = e->globalPos(); updateSelection(); setPressed(_selected); if (!_selected.action) { if (auto row = getRow(_selected.index)) { auto size = QSize(width(), _rowHeight); auto point = mapFromGlobal(QCursor::pos()) - QPoint(0, getRowTop(_selected.index)); auto hint = _selected.index; row->addRipple(size, point, [this, row, hint] { updateRow(row, hint); }); } } } void PeerListBox::Inner::mouseReleaseEvent(QMouseEvent *e) { updateRow(_pressed.index); updateRow(_selected.index); auto pressed = _pressed; setPressed(Selected()); if (e->button() == Qt::LeftButton && pressed == _selected) { if (auto row = getRow(pressed.index)) { auto peer = row->peer(); if (pressed.action) { _controller->rowActionClicked(peer); } else { _controller->rowClicked(peer); } } } } void PeerListBox::Inner::setPressed(Selected pressed) { if (auto row = getRow(_pressed.index)) { row->stopLastRipple(); } _pressed = pressed; } void PeerListBox::Inner::paintRow(Painter &p, TimeMs ms, RowIndex index) { auto row = getRow(index); t_assert(row != nullptr); row->lazyInitialize(); auto peer = row->peer(); auto user = peer->asUser(); auto active = (_pressed.index.value >= 0) ? _pressed : _selected; auto selected = (active.index == index); auto actionSelected = (selected && active.action); p.fillRect(0, 0, width(), _rowHeight, selected ? st::contactsBgOver : st::contactsBg); row->paintRipple(p, 0, 0, width(), ms); peer->paintUserpicLeft(p, st::contactsPadding.left(), st::contactsPadding.top(), width(), st::contactsPhotoSize); p.setPen(st::contactsNameFg); auto actionWidth = row->actionWidth(); auto &name = row->name(); auto namex = st::contactsPadding.left() + st::contactsPhotoSize + st::contactsPadding.left(); auto namew = width() - namex - st::contactsPadding.right() - (actionWidth ? (actionWidth + st::contactsCheckPosition.x() * 2) : 0); if (peer->isVerified()) { auto icon = &st::dialogsVerifiedIcon; namew -= icon->width(); icon->paint(p, namex + qMin(name.maxWidth(), namew), st::contactsPadding.top() + st::contactsNameTop, width()); } name.drawLeftElided(p, namex, st::contactsPadding.top() + st::contactsNameTop, namew, width()); if (actionWidth) { p.setFont(actionSelected ? st::linkOverFont : st::linkFont); p.setPen(actionSelected ? st::defaultLinkButton.overColor : st::defaultLinkButton.color); auto actionRight = st::contactsPadding.right() + st::contactsCheckPosition.x(); auto actionTop = (_rowHeight - st::normalFont->height) / 2; 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()); } void PeerListBox::Inner::selectSkip(int direction) { if (_pressed.index.value >= 0) { return; } _mouseSelection = false; auto newSelectedIndex = _selected.index.value + direction; auto rowsCount = shownRowsCount(); auto index = 0; auto firstEnabled = -1, lastEnabled = -1; enumerateShownRows([&firstEnabled, &lastEnabled, &index](Row *row) { if (!row->disabled()) { if (firstEnabled < 0) { firstEnabled = index; } lastEnabled = index; } ++index; return true; }); if (firstEnabled < 0) { firstEnabled = rowsCount; lastEnabled = firstEnabled - 1; } t_assert(lastEnabled < rowsCount); t_assert(firstEnabled - 1 <= lastEnabled); // Always pass through the first enabled item when changing from / to none selected. if ((_selected.index.value > firstEnabled && newSelectedIndex < firstEnabled) || (_selected.index.value < firstEnabled && newSelectedIndex > firstEnabled)) { newSelectedIndex = firstEnabled; } // Snap the index. newSelectedIndex = snap(newSelectedIndex, firstEnabled - 1, lastEnabled); // Skip the disabled rows. if (newSelectedIndex < firstEnabled) { newSelectedIndex = -1; } else if (newSelectedIndex > lastEnabled) { newSelectedIndex = lastEnabled; } else if (getRow(RowIndex(newSelectedIndex))->disabled()) { auto delta = (direction > 0) ? 1 : -1; for (newSelectedIndex += delta; ; newSelectedIndex += delta) { // We must find an enabled row, firstEnabled <= us <= lastEnabled. t_assert(newSelectedIndex >= 0 && newSelectedIndex < rowsCount); if (!getRow(RowIndex(newSelectedIndex))->disabled()) { break; } } } _selected.index.value = newSelectedIndex; _selected.action = false; if (newSelectedIndex >= 0) { auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : 0; auto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height(); emit mustScrollTo(top, bottom); } update(); } void PeerListBox::Inner::selectSkipPage(int height, int direction) { auto rowsToSkip = height / _rowHeight; if (!rowsToSkip) return; selectSkip(rowsToSkip * direction); } void PeerListBox::Inner::loadProfilePhotos() { if (_visibleTop >= _visibleBottom) return; auto yFrom = _visibleTop; auto yTo = _visibleBottom + (_visibleBottom - _visibleTop) * PreloadHeightsCount; AuthSession::Current().downloader()->clearPriorities(); if (yTo < 0) return; if (yFrom < 0) yFrom = 0; auto rowsCount = shownRowsCount(); if (rowsCount > 0) { auto from = yFrom / _rowHeight; if (from < 0) from = 0; if (from < rowsCount) { auto to = (yTo / _rowHeight) + 1; if (to > rowsCount) to = rowsCount; for (auto index = from; index != to; ++index) { getRow(RowIndex(index))->peer()->loadUserpic(); } } } } void PeerListBox::Inner::checkScrollForPreload() { if (_visibleBottom + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) { _controller->preloadRows(); } } void PeerListBox::Inner::searchQueryChanged(QString query) { auto searchWordsList = query.isEmpty() ? QStringList() : query.split(cWordSplit(), QString::SkipEmptyParts); if (!searchWordsList.isEmpty()) { query = searchWordsList.join(' '); } if (_searchQuery != query) { setSelected(Selected()); setPressed(Selected()); _searchQuery = query; _filterResults.clear(); if (!searchWordsList.isEmpty()) { auto minimalList = (const std::vector*)nullptr; for_const (auto &searchWord, searchWordsList) { auto searchWordStart = searchWord[0].toLower(); auto it = _searchIndex.find(searchWordStart); if (it == _searchIndex.cend()) { // Some word can't be found in any row. minimalList = nullptr; break; } else if (!minimalList || minimalList->size() > it->second.size()) { minimalList = &it->second; } } if (minimalList) { auto searchWordInNames = [](PeerData *peer, const QString &searchWord) { for_const (auto &nameWord, peer->names) { if (nameWord.startsWith(searchWord)) { return true; } } return false; }; auto allSearchWordsInNames = [searchWordInNames, &searchWordsList](PeerData *peer) { for_const (auto &searchWord, searchWordsList) { if (!searchWordInNames(peer, searchWord)) { return false; } } return true; }; _filterResults.reserve(minimalList->size()); for_const (auto row, *minimalList) { if (allSearchWordsInNames(row->peer())) { _filterResults.push_back(row); } } } } refreshRows(); restoreSelection(); } } void PeerListBox::Inner::submitted() { if (auto row = getRow(_selected.index)) { _controller->rowClicked(row->peer()); } } void PeerListBox::Inner::setVisibleTopBottom(int visibleTop, int visibleBottom) { _visibleTop = visibleTop; _visibleBottom = visibleBottom; loadProfilePhotos(); checkScrollForPreload(); } void PeerListBox::Inner::setSelected(Selected selected) { updateRow(_selected.index); _selected = selected; updateRow(_selected.index); } void PeerListBox::Inner::restoreSelection() { _lastMousePosition = QCursor::pos(); updateSelection(); } void PeerListBox::Inner::updateSelection() { if (!_mouseSelection) return; auto rowsTop = st::membersMarginTop; auto point = mapFromGlobal(_lastMousePosition); auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(_lastMousePosition)); auto selected = Selected(); auto rowsPointY = point.y() - rowsTop; selected.index.value = (in && rowsPointY >= 0) ? snap(rowsPointY / _rowHeight, 0, shownRowsCount() - 1) : -1; if (selected.index.value >= 0) { auto row = getRow(selected.index); if (row->disabled()) { selected = Selected(); } else { auto actionRight = st::contactsPadding.right() + st::contactsCheckPosition.x(); auto actionTop = (_rowHeight - st::normalFont->height) / 2; auto actionWidth = row->actionWidth(); auto actionLeft = width() - actionWidth - actionRight; auto rowTop = getRowTop(selected.index); auto actionRect = myrtlrect(actionLeft, rowTop + actionTop, actionWidth, st::normalFont->height); if (actionRect.contains(point)) { selected.action = true; } } } if (_selected != selected) { setSelected(selected); setCursor(_selected.action ? style::cur_pointer : style::cur_default); } } void PeerListBox::Inner::peerUpdated(PeerData *peer) { update(); } int PeerListBox::Inner::getRowTop(RowIndex index) const { if (index.value >= 0) { return st::membersMarginTop + index.value * _rowHeight; } return -1; } void PeerListBox::Inner::updateRow(Row *row, RowIndex hint) { updateRow(findRowIndex(row, hint)); } void PeerListBox::Inner::updateRow(RowIndex index) { if (index.value >= 0) { if (getRow(index)->disabled()) { if (index == _selected.index) { setSelected(Selected()); } if (index == _pressed.index) { setPressed(Selected()); } } update(0, getRowTop(index), width(), _rowHeight); } } template bool PeerListBox::Inner::enumerateShownRows(Callback callback) { return enumerateShownRows(0, shownRowsCount(), std::move(callback)); } template bool PeerListBox::Inner::enumerateShownRows(int from, int to, Callback callback) { t_assert(0 <= from); t_assert(from <= to); if (showingSearch()) { t_assert(to <= _filterResults.size()); for (auto i = from; i != to; ++i) { if (!callback(_filterResults[i])) { return false; } } } else { t_assert(to <= _rows.size()); for (auto i = from; i != to; ++i) { if (!callback(_rows[i].get())) { return false; } } } return true; } PeerListBox::Row *PeerListBox::Inner::getRow(RowIndex index) { if (index.value >= 0) { if (showingSearch()) { if (index.value < _filterResults.size()) { return _filterResults[index.value]; } } else if (index.value < _rows.size()) { return _rows[index.value].get(); } } return nullptr; } PeerListBox::Inner::RowIndex PeerListBox::Inner::findRowIndex(Row *row, RowIndex hint) { if (!showingSearch()) { return RowIndex(row->absoluteIndex()); } auto result = hint; if (getRow(result) == row) { return result; } auto count = shownRowsCount(); for (result.value = 0; result.value != count; ++result.value) { if (getRow(result) == row) { return result; } } result.value = -1; return result; } void PeerListBox::Inner::onPeerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars) { if (auto row = findRow(peer)) { if (_searchable) { addToSearchIndex(row); } row->refreshName(); updateRow(row); } }