/* 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/peer_list_controllers.h" #include "api/api_chat_participants.h" #include "base/random.h" #include "boxes/filters/edit_filter_chats_list.h" #include "ui/boxes/confirm_box.h" #include "ui/effects/round_checkbox.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/popup_menu.h" #include "ui/wrap/padding_wrap.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "main/main_session.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_folder.h" #include "data/data_histories.h" #include "data/data_changes.h" #include "dialogs/ui/dialogs_layout.h" #include "apiwrap.h" #include "mainwidget.h" #include "mainwindow.h" #include "lang/lang_keys.h" #include "history/history.h" #include "history/history_item.h" #include "dialogs/dialogs_main_list.h" #include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" // showAddContact() #include "base/unixtime.h" #include "styles/style_boxes.h" #include "styles/style_profile.h" #include "styles/style_dialogs.h" #include "data/data_stories.h" #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_list.h" namespace { constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000); constexpr auto kSearchPerPage = 50; class StoriesRow final : public PeerListRow { public: StoriesRow( not_null session, const QBrush &unread, const Dialogs::Stories::Element &element, int position); void applySegments(const Dialogs::Stories::Element &element); void updateGradient(QBrush unread); int position = 0; private: void refreshSegments(); QBrush _unread; int _count = 0; int _unreadCount = 0; }; class StoriesController final : public PeerListController , public base::has_weak_ptr { public: using Content = Dialogs::Stories::Content; using Row = StoriesRow; StoriesController( not_null window, rpl::producer content, Fn open, Fn loadMore); Main::Session &session() const override; void prepare() override; void loadMoreRows() override; void rowClicked(not_null row) override; base::unique_qptr rowContextMenu( QWidget *parent, not_null row) override; private: void refresh(const Content &content); const not_null _window; QBrush _unread; rpl::producer _content; Fn _open; Fn _loadMore; bool _positionPositive = false; rpl::lifetime _lifetime; }; [[nodiscard]] std::vector PrepareSegments( int count, int unread, const QBrush &unreadBrush) { Expects(unread <= count); Expects(count > 0); auto result = std::vector(); const auto add = [&](bool unread) { result.push_back({ .brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b, .width = (unread ? st::dialogsStoriesFull.lineTwice / 2. : st::dialogsStoriesFull.lineReadTwice / 2.), }); }; result.reserve(count); for (auto i = 0, till = count - unread; i != till; ++i) { add(false); } for (auto i = 0; i != unread; ++i) { add(true); } return result; } [[nodiscard]] QBrush CreateStoriesGradient() { const auto &st = st::contactsWithStories.item; const auto left = st.photoPosition.x(); const auto top = st.photoPosition.y(); auto gradient = QLinearGradient( QPoint(left + st.photoSize, top), QPoint(left, top + st.photoSize)); gradient.setStops({ { 0., st::groupCallLive1->c }, { 1., st::groupCallMuted1->c }, }); return QBrush(gradient); } StoriesRow::StoriesRow( not_null session, const QBrush &unread, const Dialogs::Stories::Element &element, int position) : PeerListRow(session->data().peer(PeerId(element.id))) , position(position) , _unread(unread) { } void StoriesRow::applySegments(const Dialogs::Stories::Element &element) { Expects(element.unreadCount <= element.count); _count = int(std::max(element.count, 1U)); _unreadCount = element.unreadCount; refreshSegments(); } void StoriesRow::updateGradient(QBrush unread) { _unread = std::move(unread); refreshSegments(); } void StoriesRow::refreshSegments() { setCustomizedCheckSegments( PrepareSegments(_count, _unreadCount, _unread)); } StoriesController::StoriesController( not_null window, rpl::producer content, Fn open, Fn loadMore) : _window(window) , _content(std::move(content)) , _open(std::move(open)) , _loadMore(std::move(loadMore)) { _unread = CreateStoriesGradient(); style::PaletteChanged( ) | rpl::start_with_next([=] { _unread = CreateStoriesGradient(); for (auto i = 0, count = int(delegate()->peerListFullRowsCount()) ; i != count ; ++i) { const auto row = delegate()->peerListRowAt(i).get(); static_cast(row)->updateGradient(_unread); } }, _lifetime); } Main::Session &StoriesController::session() const { return _window->session(); } void StoriesController::prepare() { if (_loadMore) { _loadMore(); } std::move( _content ) | rpl::start_with_next([=](Content content) { refresh(content); }, _lifetime); } void StoriesController::loadMoreRows() { if (_loadMore) { _loadMore(); } } void StoriesController::rowClicked(not_null row) { if (_open) { _open(row->id()); } } base::unique_qptr StoriesController::rowContextMenu( QWidget *parent, not_null row) { auto result = base::make_unique_q(parent); Dialogs::Stories::FillSourceMenu(_window, { .id = row->id(), .callback = Ui::Menu::CreateAddActionCallback(result.get()), }); if (result->empty()) { return nullptr; } return result; } void StoriesController::refresh(const Content &content) { const auto session = &_window->session(); const auto positive = _positionPositive = !_positionPositive; auto position = positive ? 1 : -int(content.elements.size()); for (const auto &element : content.elements) { if (const auto row = delegate()->peerListFindRow(element.id)) { static_cast(row)->position = position; static_cast(row)->applySegments(element); } else { auto added = std::make_unique( session, _unread, element, position); const auto raw = added.get(); delegate()->peerListAppendRow(std::move(added)); delegate()->peerListSetRowChecked(raw, true); raw->applySegments(element); raw->finishCheckedAnimation(); } ++position; } auto count = delegate()->peerListFullRowsCount(); for (auto i = 0; i != count;) { const auto row = delegate()->peerListRowAt(i); const auto position = static_cast(row.get())->position; if (positive ? (position > 0) : (position < 0)) { ++i; } else { delegate()->peerListRemoveRow(row); --count; } } delegate()->peerListSortRows([]( const PeerListRow &a, const PeerListRow &b) { return static_cast(a).position < static_cast(b).position; }); delegate()->peerListRefreshRows(); } [[nodiscard]] object_ptr PrepareHiddenStoriesList( not_null box, not_null sessionController, rpl::producer mode) { auto result = object_ptr>( box, object_ptr(box)); const auto container = result->entity(); const auto stories = &sessionController->session().data().stories(); container->add(CreatePeerListSectionSubtitle( container, tr::lng_contacts_hidden_stories())); auto &lifetime = container->lifetime(); auto list = Dialogs::Stories::ContentForSession( &sessionController->session(), Data::StorySourcesList::Hidden ) | rpl::start_spawning(lifetime); const auto delegate = lifetime.make_state< PeerListContentDelegateSimple >(); const auto open = [=](uint64 id) { sessionController->openPeerStories( PeerId(int64(id)), Data::StorySourcesList::Hidden); }; const auto loadMore = [=] { stories->loadMore(Data::StorySourcesList::Hidden); }; const auto controller = lifetime.make_state( sessionController, rpl::duplicate( list ) | rpl::filter([](const Dialogs::Stories::Content &list) { return !list.elements.empty(); }), open, loadMore); controller->setStyleOverrides(&st::contactsWithStories); const auto content = container->add(object_ptr( container, controller)); delegate->setContent(content); controller->setDelegate(delegate); container->add(CreatePeerListSectionSubtitle( container, rpl::conditional( std::move( mode ) | rpl::map( rpl::mappers::_1 == ContactsBoxController::SortMode::Online ), tr::lng_contacts_by_online(), tr::lng_contacts_by_name()))); stories->incrementPreloadingHiddenSources(); lifetime.add([=] { stories->decrementPreloadingHiddenSources(); }); result->toggleOn(rpl::duplicate( list ) | rpl::map([](const Dialogs::Stories::Content &list) { return !list.elements.empty(); })); result->finishAnimating(); return result; } } // namespace object_ptr PrepareContactsBox( not_null sessionController) { using Mode = ContactsBoxController::SortMode; auto controller = std::make_unique( &sessionController->session()); controller->setStyleOverrides(&st::contactsWithStories); controller->setStoriesShown(true); const auto raw = controller.get(); auto init = [=](not_null box) { using namespace Dialogs::Stories; struct State { QPointer<::Ui::IconButton> toggleSort; rpl::variable mode = Mode::Online; ::Ui::Animations::Simple scrollAnimation; }; const auto stories = &sessionController->session().data().stories(); const auto state = box->lifetime().make_state(); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); box->addLeftButton( tr::lng_profile_add_contact(), [=] { sessionController->showAddContact(); }); state->toggleSort = box->addTopButton(st::contactsSortButton, [=] { const auto online = (state->mode.current() == Mode::Online); const auto mode = online ? Mode::Alphabet : Mode::Online; state->mode = mode; raw->setSortMode(mode); state->toggleSort->setIconOverride( online ? &st::contactsSortOnlineIcon : nullptr, online ? &st::contactsSortOnlineIconOver : nullptr); }); raw->setSortMode(Mode::Online); box->peerListSetAboveWidget(PrepareHiddenStoriesList( box, sessionController, state->mode.value())); }; return Box(std::move(controller), std::move(init)); } void PeerListRowWithLink::setActionLink(const QString &action) { _action = action; refreshActionLink(); } void PeerListRowWithLink::refreshActionLink() { if (!isInitialized()) return; _actionWidth = _action.isEmpty() ? 0 : st::normalFont->width(_action); } void PeerListRowWithLink::lazyInitialize(const style::PeerListItem &st) { PeerListRow::lazyInitialize(st); refreshActionLink(); } QSize PeerListRowWithLink::rightActionSize() const { return QSize(_actionWidth, st::normalFont->height); } QMargins PeerListRowWithLink::rightActionMargins() const { return QMargins( st::contactsCheckPosition.x(), (st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom() - st::normalFont->height) / 2, st::defaultPeerListItem.photoPosition.x() + st::contactsCheckPosition.x(), 0); } void PeerListRowWithLink::rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) { p.setFont(actionSelected ? st::linkOverFont : st::linkFont); p.setPen(actionSelected ? st::defaultLinkButton.overColor : st::defaultLinkButton.color); p.drawTextLeft(x, y, outerWidth, _action, _actionWidth); } PeerListGlobalSearchController::PeerListGlobalSearchController( not_null session) : _session(session) , _api(&session->mtp()) { _timer.setCallback([this] { searchOnServer(); }); } void PeerListGlobalSearchController::searchQuery(const QString &query) { if (_query != query) { _query = query; _requestId = 0; if (!_query.isEmpty() && !searchInCache()) { _timer.callOnce(AutoSearchTimeout); } else { _timer.cancel(); } } } bool PeerListGlobalSearchController::searchInCache() { auto it = _cache.find(_query); if (it != _cache.cend()) { _requestId = 0; searchDone(it->second, _requestId); return true; } return false; } void PeerListGlobalSearchController::searchOnServer() { _requestId = _api.request(MTPcontacts_Search( MTP_string(_query), MTP_int(SearchPeopleLimit) )).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) { searchDone(result, requestId); }).fail([=](const MTP::Error &error, mtpRequestId requestId) { if (_requestId == requestId) { _requestId = 0; delegate()->peerListSearchRefreshRows(); } }).send(); _queries.emplace(_requestId, _query); } void PeerListGlobalSearchController::searchDone( const MTPcontacts_Found &result, mtpRequestId requestId) { Expects(result.type() == mtpc_contacts_found); auto &contacts = result.c_contacts_found(); auto query = _query; if (requestId) { _session->data().processUsers(contacts.vusers()); _session->data().processChats(contacts.vchats()); auto it = _queries.find(requestId); if (it != _queries.cend()) { query = it->second; _cache[query] = result; _queries.erase(it); } } const auto feedList = [&](const MTPVector &list) { for (const auto &mtpPeer : list.v) { const auto peer = _session->data().peerLoaded( peerFromMTP(mtpPeer)); if (peer) { delegate()->peerListSearchAddRow(peer); } } }; if (_requestId == requestId) { _requestId = 0; feedList(contacts.vmy_results()); feedList(contacts.vresults()); delegate()->peerListSearchRefreshRows(); } } bool PeerListGlobalSearchController::isLoading() { return _timer.isActive() || _requestId; } ChatsListBoxController::Row::Row(not_null history) : PeerListRow(history->peer) , _history(history) { } ChatsListBoxController::ChatsListBoxController( not_null session) : ChatsListBoxController( std::make_unique(session)) { } ChatsListBoxController::ChatsListBoxController( std::unique_ptr searchController) : PeerListController(std::move(searchController)) { } void ChatsListBoxController::prepare() { setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); prepareViewHook(); if (!session().data().chatsListLoaded()) { session().data().chatsListLoadedEvents( ) | rpl::filter([=](Data::Folder *folder) { return !folder; }) | rpl::start_with_next([=] { checkForEmptyRows(); }, lifetime()); } session().data().chatsListChanges( ) | rpl::start_with_next([=] { rebuildRows(); }, lifetime()); session().data().contactsLoaded().value( ) | rpl::start_with_next([=] { rebuildRows(); }, lifetime()); } void ChatsListBoxController::rebuildRows() { auto wasEmpty = !delegate()->peerListFullRowsCount(); auto appendList = [this](auto chats) { auto count = 0; for (const auto &row : chats->all()) { if (const auto history = row->history()) { if (appendRow(history)) { ++count; } } } return count; }; auto added = 0; if (respectSavedMessagesChat()) { if (appendRow(session().data().history(session().user()))) { ++added; } } added += appendList(session().data().chatsList()->indexed()); const auto id = Data::Folder::kId; if (const auto folder = session().data().folderLoaded(id)) { added += appendList(folder->chatsList()->indexed()); } added += appendList(session().data().contactsNoChatsList()); if (!wasEmpty && added > 0) { // Place dialogs list before contactsNoDialogs list. delegate()->peerListPartitionRows([](const PeerListRow &a) { const auto history = static_cast(a).history(); return history->inChatList(); }); if (respectSavedMessagesChat()) { delegate()->peerListPartitionRows([](const PeerListRow &a) { return a.peer()->isSelf(); }); } } checkForEmptyRows(); delegate()->peerListRefreshRows(); } void ChatsListBoxController::checkForEmptyRows() { if (delegate()->peerListFullRowsCount()) { setDescriptionText(QString()); } else { const auto loaded = session().data().contactsLoaded().current() && session().data().chatsListLoaded(); setDescriptionText(loaded ? emptyBoxText() : tr::lng_contacts_loading(tr::now)); } } QString ChatsListBoxController::emptyBoxText() const { return tr::lng_contacts_not_found(tr::now); } std::unique_ptr ChatsListBoxController::createSearchRow( not_null peer) { return createRow(peer->owner().history(peer)); } bool ChatsListBoxController::appendRow(not_null history) { if (auto row = delegate()->peerListFindRow(history->peer->id.value)) { updateRowHook(static_cast(row)); return false; } if (auto row = createRow(history)) { delegate()->peerListAppendRow(std::move(row)); return true; } return false; } ContactsBoxController::ContactsBoxController( not_null session) : ContactsBoxController( session, std::make_unique(session)) { } ContactsBoxController::ContactsBoxController( not_null session, std::unique_ptr searchController) : PeerListController(std::move(searchController)) , _session(session) , _sortByOnlineTimer([=] { sort(); }) { } Main::Session &ContactsBoxController::session() const { return *_session; } void ContactsBoxController::prepare() { setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); delegate()->peerListSetTitle(tr::lng_contacts_header()); prepareViewHook(); if (_storiesShown) { _storiesUnread = CreateStoriesGradient(); style::PaletteChanged() | rpl::start_with_next([=] { _storiesUnread = CreateStoriesGradient(); for (auto &entry : _storiesCounts) { entry.second.count = entry.second.unread = -1; } updateStories(); }, lifetime()); const auto stories = &session().data().stories(); rpl::merge( rpl::single(rpl::empty), stories->sourcesChanged(Data::StorySourcesList::NotHidden), stories->sourcesChanged(Data::StorySourcesList::Hidden) ) | rpl::start_with_next([=] { updateStories(); }, lifetime()); stories->sourceChanged() | rpl::start_with_next([=](PeerId id) { const auto source = stories->source(id); const auto info = source ? source->info() : Data::StoriesSourceInfo(); updateStoriesFor(id.value, info.count, info.unreadCount); }, lifetime()); } session().data().contactsLoaded().value( ) | rpl::start_with_next([=] { rebuildRows(); }, lifetime()); } void ContactsBoxController::rebuildRows() { const auto appendList = [&](auto chats) { auto count = 0; for (const auto &row : chats->all()) { if (const auto history = row->history()) { if (const auto user = history->peer->asUser()) { if (appendRow(user)) { ++count; } } } } return count; }; appendList(session().data().contactsList()); checkForEmptyRows(); sort(); delegate()->peerListRefreshRows(); } void ContactsBoxController::checkForEmptyRows() { setDescriptionText(delegate()->peerListFullRowsCount() ? QString() : session().data().contactsLoaded().current() ? tr::lng_contacts_not_found(tr::now) : tr::lng_contacts_loading(tr::now)); } std::unique_ptr ContactsBoxController::createSearchRow( not_null peer) { if (const auto user = peer->asUser()) { return createRow(user); } return nullptr; } void ContactsBoxController::rowClicked(not_null row) { const auto peer = row->peer(); if (const auto window = peer->session().tryResolveWindow()) { if (_storiesShown) { const auto point = delegate()->peerListLastRowMousePosition(); const auto &st = st::contactsWithStories.item; if (point && point->x() < st.photoPosition.x() + st.photoSize) { window->openPeerStories(peer->id); return; } } window->showPeerHistory(row->peer()); } } void ContactsBoxController::setSortMode(SortMode mode) { if (_sortMode == mode) { return; } _sortMode = mode; sort(); if (_sortMode == SortMode::Online) { session().changes().peerUpdates( Data::PeerUpdate::Flag::OnlineStatus ) | rpl::filter([=](const Data::PeerUpdate &update) { return !_sortByOnlineTimer.isActive() && delegate()->peerListFindRow(update.peer->id.value); }) | rpl::start_with_next([=] { _sortByOnlineTimer.callOnce(kSortByOnlineThrottle); }, _sortByOnlineLifetime); } else { _sortByOnlineTimer.cancel(); _sortByOnlineLifetime.destroy(); } } void ContactsBoxController::setStoriesShown(bool shown) { _storiesShown = shown; } void ContactsBoxController::updateStories() { const auto stories = &_session->data().stories(); const auto &a = stories->sources(Data::StorySourcesList::NotHidden); const auto &b = stories->sources(Data::StorySourcesList::Hidden); auto checked = base::flat_set(); for (const auto &info : ranges::views::concat(a, b)) { const auto id = info.id.value; checked.emplace(id); updateStoriesFor(id, info.count, info.unreadCount); } for (auto i = begin(_storiesCounts); i != end(_storiesCounts); ++i) { if (i->second.count && !checked.contains(i->first)) { updateStoriesFor(i->first, 0, 0); } } } void ContactsBoxController::updateStoriesFor( uint64 id, int count, int unread) { if (const auto row = delegate()->peerListFindRow(id)) { applyRowStories(row, count, unread); delegate()->peerListUpdateRow(row); } } void ContactsBoxController::applyRowStories( not_null row, int count, int unread, bool force) { auto &counts = _storiesCounts[row->id()]; if (!force && counts.count == count && counts.unread == unread) { return; } counts.count = count; counts.unread = unread; delegate()->peerListSetRowChecked(row, count > 0); if (count > 0) { row->setCustomizedCheckSegments( PrepareSegments(count, unread, _storiesUnread)); } } void ContactsBoxController::sort() { switch (_sortMode) { case SortMode::Alphabet: sortByName(); break; case SortMode::Online: sortByOnline(); break; default: Unexpected("SortMode in ContactsBoxController."); } } void ContactsBoxController::sortByName() { auto keys = base::flat_map(); keys.reserve(delegate()->peerListFullRowsCount()); const auto key = [&](const PeerListRow &row) { const auto id = row.id(); const auto i = keys.find(id); if (i != end(keys)) { return i->second; } const auto peer = row.peer(); const auto history = peer->owner().history(peer); return keys.emplace(id, history->chatListNameSortKey()).first->second; }; const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) { return (key(a).compare(key(b)) < 0); }; delegate()->peerListSortRows(predicate); } void ContactsBoxController::sortByOnline() { const auto now = base::unixtime::now(); const auto key = [&](const PeerListRow &row) { const auto user = row.peer()->asUser(); return user ? (std::min(user->onlineTill, now) + 1) : TimeId(); }; const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) { return key(a) > key(b); }; delegate()->peerListSortRows(predicate); } bool ContactsBoxController::appendRow(not_null user) { if (auto row = delegate()->peerListFindRow(user->id.value)) { updateRowHook(row); return false; } if (auto row = createRow(user)) { const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); if (_storiesShown) { const auto stories = &session().data().stories(); if (const auto source = stories->source(user->id)) { const auto info = source->info(); applyRowStories(raw, info.count, info.unreadCount, true); } } return true; } return false; } std::unique_ptr ContactsBoxController::createRow( not_null user) { return std::make_unique(user); } ChooseRecipientBoxController::ChooseRecipientBoxController( not_null session, FnMut)> callback, Fn)> filter) : ChatsListBoxController(session) , _session(session) , _callback(std::move(callback)) , _filter(std::move(filter)) { } Main::Session &ChooseRecipientBoxController::session() const { return *_session; } void ChooseRecipientBoxController::prepareViewHook() { delegate()->peerListSetTitle(tr::lng_forward_choose()); } void ChooseRecipientBoxController::rowClicked(not_null row) { auto guard = base::make_weak(this); const auto peer = row->peer(); if (const auto forum = peer->forum()) { const auto weak = std::make_shared>(); auto callback = [=](not_null topic) { const auto exists = guard.get(); if (!exists) { if (*weak) { (*weak)->closeBox(); } return; } auto onstack = std::move(_callback); onstack(topic); if (guard) { _callback = std::move(onstack); } else if (*weak) { (*weak)->closeBox(); } }; const auto filter = [=](not_null topic) { return guard && (!_filter || _filter(topic)); }; auto owned = Box( std::make_unique( forum, std::move(callback), filter), [=](not_null box) { box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); forum->destroyed( ) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); }); *weak = owned.data(); delegate()->peerListShowBox(std::move(owned)); return; } const auto history = peer->owner().history(peer); auto callback = std::move(_callback); callback(history); if (guard) { _callback = std::move(callback); } } auto ChooseRecipientBoxController::createRow( not_null history) -> std::unique_ptr { const auto peer = history->peer; const auto skip = _filter ? !_filter(history) : ((peer->isBroadcast() && !Data::CanSendAnything(peer)) || (peer->isUser() && !Data::CanSendAnything(peer))); return skip ? nullptr : std::make_unique(history); } ChooseTopicSearchController::ChooseTopicSearchController( not_null forum) : _forum(forum) , _api(&forum->session().mtp()) , _timer([=] { searchOnServer(); }) { } void ChooseTopicSearchController::searchQuery(const QString &query) { if (_query != query) { _query = query; _api.request(base::take(_requestId)).cancel(); _offsetDate = 0; _offsetId = 0; _offsetTopicId = 0; _allLoaded = false; if (!_query.isEmpty()) { _timer.callOnce(AutoSearchTimeout); } else { _timer.cancel(); } } } void ChooseTopicSearchController::searchOnServer() { _requestId = _api.request(MTPchannels_GetForumTopics( MTP_flags(MTPchannels_GetForumTopics::Flag::f_q), _forum->channel()->inputChannel, MTP_string(_query), MTP_int(_offsetDate), MTP_int(_offsetId), MTP_int(_offsetTopicId), MTP_int(kSearchPerPage) )).done([=](const MTPmessages_ForumTopics &result) { _requestId = 0; const auto savedTopicId = _offsetTopicId; const auto byCreation = result.data().is_order_by_create_date(); _forum->applyReceivedTopics(result, [&]( not_null topic) { _offsetTopicId = topic->rootId(); if (byCreation) { _offsetDate = topic->creationDate(); if (const auto last = topic->lastServerMessage()) { _offsetId = last->id; } } else if (const auto last = topic->lastServerMessage()) { _offsetId = last->id; _offsetDate = last->date(); } delegate()->peerListSearchAddRow(topic->rootId().bare); }); if (_offsetTopicId == savedTopicId) { _allLoaded = true; } delegate()->peerListSearchRefreshRows(); }).fail([=] { _allLoaded = true; }).send(); } bool ChooseTopicSearchController::isLoading() { return _timer.isActive() || _requestId; } bool ChooseTopicSearchController::loadMoreRows() { if (!isLoading()) { searchOnServer(); } return !_allLoaded; } ChooseTopicBoxController::Row::Row(not_null topic) : PeerListRow(topic->rootId().bare) , _topic(topic) { } QString ChooseTopicBoxController::Row::generateName() { return _topic->title(); } QString ChooseTopicBoxController::Row::generateShortName() { return _topic->title(); } auto ChooseTopicBoxController::Row::generatePaintUserpicCallback( bool forceRound) -> PaintRoundImageCallback { return [=]( Painter &p, int x, int y, int outerWidth, int size) { const auto &st = st::forumTopicRow; x -= st.padding.left(); y -= st.padding.top(); auto view = Ui::PeerUserpicView(); p.translate(x, y); _topic->paintUserpic(p, view, { .st = &st, .currentBg = st::windowBg, .now = crl::now(), .width = outerWidth, .paused = false, }); p.translate(-x, -y); }; } auto ChooseTopicBoxController::Row::generateNameFirstLetters() const -> const base::flat_set & { return _topic->chatListFirstLetters(); } auto ChooseTopicBoxController::Row::generateNameWords() const -> const base::flat_set & { return _topic->chatListNameWords(); } ChooseTopicBoxController::ChooseTopicBoxController( not_null forum, FnMut)> callback, Fn)> filter) : PeerListController(std::make_unique(forum)) , _forum(forum) , _callback(std::move(callback)) , _filter(std::move(filter)) { setStyleOverrides(&st::chooseTopicList); _forum->chatsListChanges( ) | rpl::start_with_next([=] { refreshRows(); }, lifetime()); _forum->topicDestroyed( ) | rpl::start_with_next([=](not_null topic) { const auto id = PeerListRowId(topic->rootId().bare); if (const auto row = delegate()->peerListFindRow(id)) { delegate()->peerListRemoveRow(row); delegate()->peerListRefreshRows(); } }, lifetime()); } Main::Session &ChooseTopicBoxController::session() const { return _forum->session(); } void ChooseTopicBoxController::rowClicked(not_null row) { const auto weak = base::make_weak(this); auto onstack = base::take(_callback); onstack(static_cast(row.get())->topic()); if (weak) { _callback = std::move(onstack); } } void ChooseTopicBoxController::prepare() { delegate()->peerListSetTitle(tr::lng_forward_choose()); setSearchNoResultsText(tr::lng_topics_not_found(tr::now)); delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); refreshRows(true); session().changes().entryUpdates( Data::EntryUpdate::Flag::Repaint ) | rpl::start_with_next([=](const Data::EntryUpdate &update) { if (const auto topic = update.entry->asTopic()) { if (topic->forum() == _forum) { const auto id = topic->rootId().bare; if (const auto row = delegate()->peerListFindRow(id)) { delegate()->peerListUpdateRow(row); } } } }, lifetime()); } void ChooseTopicBoxController::refreshRows(bool initial) { auto added = false; for (const auto &row : _forum->topicsList()->indexed()->all()) { if (const auto topic = row->topic()) { const auto id = topic->rootId().bare; auto already = delegate()->peerListFindRow(id); if (initial || !already) { if (auto created = createRow(topic)) { delegate()->peerListAppendRow(std::move(created)); added = true; } } else if (already->isSearchResult()) { delegate()->peerListAppendFoundRow(already); added = true; } } } if (added) { delegate()->peerListRefreshRows(); } } void ChooseTopicBoxController::loadMoreRows() { _forum->requestTopics(); } std::unique_ptr ChooseTopicBoxController::createSearchRow( PeerListRowId id) { if (const auto topic = _forum->topicFor(MsgId(id))) { return std::make_unique(topic); } return nullptr; } auto ChooseTopicBoxController::createRow(not_null topic) -> std::unique_ptr { const auto skip = _filter && !_filter(topic); return skip ? nullptr : std::make_unique(topic); };