/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_search_posts.h" #include "apiwrap.h" #include "base/unixtime.h" #include "data/data_session.h" #include "data/data_peer_values.h" #include "history/history.h" #include "main/main_session.h" namespace Dialogs { namespace { constexpr auto kQueryDelay = crl::time(500); constexpr auto kPerPage = 50; } // namespace PostsSearch::PostsSearch(not_null session) : _session(session) , _api(&_session->api().instance()) , _timer([=] { applyQuery(); }) , _recheckTimer([=] { recheck(); }) { Data::AmPremiumValue(_session) | rpl::start_with_next([=] { maybePushPremiumUpdate(); }, _lifetime); } rpl::producer PostsSearch::stateUpdates() const { return _stateUpdates.events(); } void PostsSearch::requestMore() { if (_query) { requestSearch(*_query); } } void PostsSearch::setQuery(const QString &query) { if (_query == query) { return; } _query = query; const auto i = _entries.find(query); if (i != end(_entries)) { pushStateUpdate(i->second); } else if (query.isEmpty()) { applyQuery(); } else { _timer.callOnce(kQueryDelay); } } void PostsSearch::setAllowedStars(int stars) { if (_query) { _entries[*_query].allowedStars = stars; requestSearch(*_query); } } void PostsSearch::pushStateUpdate(const Entry &entry) { if (!entry.pages.empty() || entry.loaded) { _stateUpdates.fire(PostsSearchState{ .first = (entry.pages.empty() ? std::vector>() : entry.pages.front()), .totalCount = entry.totalCount, }); } else if (entry.checkId || entry.searchId) { _stateUpdates.fire(PostsSearchState{ .loading = true, }); } else { Assert(_floodState.has_value()); auto copy = _floodState; copy->query = *_query; copy->needsPremium = !_session->premium(); _stateUpdates.fire(PostsSearchState{ .intro = std::move(copy), }); } } void PostsSearch::maybePushPremiumUpdate() { if (!_floodState || !_query) { return; } auto &entry = _entries[*_query]; if (!entry.pages.empty() || entry.loaded || entry.checkId || entry.searchId) { return; } pushStateUpdate(entry); } void PostsSearch::applyQuery() { Expects(_query.has_value()); _timer.cancel(); if (_query->isEmpty()) { requestSearch(QString()); } else { requestState(*_query); } } void PostsSearch::requestSearch(const QString &query) { auto &entry = _entries[query]; if (entry.searchId || entry.loaded) { return; } using Flag = MTPchannels_SearchPosts::Flag; entry.searchId = _api.request(MTPchannels_SearchPosts( MTP_flags(Flag::f_query | (entry.allowedStars ? Flag::f_allow_paid_stars : Flag())), MTP_string(), // hashtag MTP_string(query), MTP_int(entry.offsetRate), (entry.offsetPeer ? entry.offsetPeer->input : MTP_inputPeerEmpty()), MTP_int(entry.offsetId), MTP_int(kPerPage), MTP_long(entry.allowedStars) )).done([=](const MTPmessages_Messages &result) { auto &entry = _entries[query]; entry.searchId = 0; const auto initial = !entry.offsetId; const auto owner = &_session->data(); const auto processList = [&](const MTPVector &messages) { auto result = std::vector>(); for (const auto &message : messages.v) { const auto msgId = IdFromMessage(message); const auto peerId = PeerFromMessage(message); const auto lastDate = DateFromMessage(message); if (const auto peer = owner->peerLoaded(peerId)) { if (lastDate) { const auto item = owner->addNewMessage( message, MessageFlags(), NewMessageType::Existing); result.push_back(item); } entry.offsetPeer = peer; } else { LOG(("API Error: a search results with not loaded peer %1" ).arg(peerId.value)); } entry.offsetId = msgId; } return result; }; auto totalCount = 0; auto messages = result.match([&](const MTPDmessages_messages &data) { owner->processUsers(data.vusers()); owner->processChats(data.vchats()); entry.loaded = true; auto list = processList(data.vmessages()); totalCount = list.size(); return list; }, [&](const MTPDmessages_messagesSlice &data) { owner->processUsers(data.vusers()); owner->processChats(data.vchats()); auto list = processList(data.vmessages()); const auto nextRate = data.vnext_rate(); const auto rateUpdated = nextRate && (nextRate->v != entry.offsetRate); const auto finished = list.empty(); if (rateUpdated) { entry.offsetRate = nextRate->v; } if (finished) { entry.loaded = true; } totalCount = data.vcount().v; if (const auto flood = data.vsearch_flood()) { setFloodStateFrom(flood->data()); } return list; }, [&](const MTPDmessages_channelMessages &data) { LOG(("API Error: " "received messages.channelMessages when no channel " "was passed! (PostsSearch::performSearch)")); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); auto list = processList(data.vmessages()); if (list.empty()) { entry.loaded = true; } totalCount = data.vcount().v; return list; }, [&](const MTPDmessages_messagesNotModified &) { LOG(("API Error: received messages.messagesNotModified! " "(PostsSearch::performSearch)")); entry.loaded = true; return std::vector>(); }); if (initial) { entry.pages.clear(); } entry.pages.push_back(std::move(messages)); const auto count = int(entry.pages.size()); const auto full = entry.loaded ? count : std::max(count, totalCount); entry.totalCount = full; if (initial && _query == query) { pushStateUpdate(entry); } }).fail([=](const MTP::Error &error) { auto &entry = _entries[query]; entry.searchId = 0; const auto initial = !entry.offsetId; const auto &type = error.type(); if (initial && type.startsWith(u"FLOOD_WAIT_"_q)) { requestState(query); } else { entry.loaded = true; } }).handleFloodErrors().send(); } void PostsSearch::setFloodStateFrom(const MTPDsearchPostsFlood &data) { _recheckTimer.cancel(); const auto left = data.vremains().v; const auto next = data.vwait_till().value_or_empty(); if (!left && next > 0) { const auto now = base::unixtime::now(); const auto delay = std::clamp(next - now, 1, 86401); _recheckTimer.callOnce(delay * crl::time(1000)); } _floodState = PostsSearchIntroState{ .freeSearchesPerDay = data.vtotal_daily().v, .freeSearchesLeft = left, .nextFreeSearchTime = next, .starsPerPaidSearch = uint32(data.vstars_amount().v), }; } void PostsSearch::recheck() { requestState(*_query, true); } void PostsSearch::requestState(const QString &query, bool force) { auto &entry = _entries[query]; if (force) { _api.request(base::take(entry.checkId)).cancel(); } else if (entry.checkId || entry.loaded) { return; } using Flag = MTPchannels_CheckSearchPostsFlood::Flag; entry.checkId = _api.request(MTPchannels_CheckSearchPostsFlood( MTP_flags(Flag::f_query), MTP_string(query) )).done([=](const MTPSearchPostsFlood &result) { auto &entry = _entries[query]; entry.checkId = 0; const auto &data = result.data(); setFloodStateFrom(data); if (data.is_query_is_free()) { if (!entry.loaded) { requestSearch(query); } } else if (_query == query) { pushStateUpdate(entry); } }).fail([=](const MTP::Error &error) { auto &entry = _entries[query]; entry.checkId = 0; entry.loaded = true; }).handleFloodErrors().send(); } } // namespace Dialogs