/* 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 "info/global_media/info_global_media_provider.h" #include "apiwrap.h" #include "info/media/info_media_widget.h" #include "info/media/info_media_list_section.h" #include "info/info_controller.h" #include "lang/lang_keys.h" #include "ui/text/format_song_document_name.h" #include "ui/ui_utility.h" #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "main/main_session.h" #include "main/main_account.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/history.h" #include "core/application.h" #include "storage/storage_shared_media.h" #include "layout/layout_selection.h" #include "styles/style_overview.h" namespace Info::GlobalMedia { namespace { constexpr auto kPerPage = 50; constexpr auto kPreloadedScreensCount = 4; constexpr auto kPreloadedScreensCountFull = kPreloadedScreensCount + 1 + kPreloadedScreensCount; } // namespace GlobalMediaSlice::GlobalMediaSlice( Key key, std::vector items, std::optional fullCount, int skippedAfter) : _key(key) , _items(std::move(items)) , _fullCount(fullCount) , _skippedAfter(skippedAfter) { } std::optional GlobalMediaSlice::fullCount() const { return _fullCount; } std::optional GlobalMediaSlice::skippedBefore() const { return _fullCount ? int(*_fullCount - _skippedAfter - _items.size()) : std::optional(); } std::optional GlobalMediaSlice::skippedAfter() const { return _skippedAfter; } std::optional GlobalMediaSlice::indexOf(Value position) const { const auto it = ranges::find(_items, position); return (it != end(_items)) ? std::make_optional(int(it - begin(_items))) : std::nullopt; } int GlobalMediaSlice::size() const { return _items.size(); } GlobalMediaSlice::Value GlobalMediaSlice::operator[](int index) const { Expects(index >= 0 && index < size()); return _items[index]; } std::optional GlobalMediaSlice::distance( const Key &a, const Key &b) const { const auto i = indexOf(a.aroundId); const auto j = indexOf(b.aroundId); return (i && j) ? std::make_optional(*j - *i) : std::nullopt; } std::optional GlobalMediaSlice::nearest( Value position) const { if (_items.empty()) { return std::nullopt; } const auto it = ranges::lower_bound( _items, position, std::greater<>{}); if (it == end(_items)) { return _items.back(); } else if (it == begin(_items)) { return _items.front(); } return *it; } Provider::Provider(not_null controller) : _controller(controller) , _type(_controller->section().mediaType()) , _slice(sliceKey(_aroundId)) { _controller->session().data().itemRemoved( ) | rpl::start_with_next([this](auto item) { itemRemoved(item); }, _lifetime); style::PaletteChanged( ) | rpl::start_with_next([=] { for (auto &layout : _layouts) { layout.second.item->invalidateCache(); } }, _lifetime); } Provider::Type Provider::type() { return _type; } bool Provider::hasSelectRestriction() { return true; } rpl::producer Provider::hasSelectRestrictionChanges() { return rpl::never(); } bool Provider::sectionHasFloatingHeader() { switch (_type) { case Type::Photo: case Type::GIF: case Type::Video: case Type::RoundFile: case Type::RoundVoiceFile: case Type::MusicFile: return false; case Type::File: case Type::Link: return true; } Unexpected("Type in HasFloatingHeader()"); } QString Provider::sectionTitle(not_null item) { return QString(); } bool Provider::sectionItemBelongsHere( not_null item, not_null previous) { return true; } bool Provider::isPossiblyMyItem(not_null item) { return item->media() != nullptr; } std::optional Provider::fullCount() { return _slice.fullCount(); } void Provider::restart() { _layouts.clear(); _aroundId = Data::MaxMessagePosition; _idsLimit = kMinimalIdsLimit; _slice = GlobalMediaSlice(sliceKey(_aroundId)); refreshViewer(); } void Provider::checkPreload( QSize viewport, not_null topLayout, not_null bottomLayout, bool preloadTop, bool preloadBottom) { const auto visibleWidth = viewport.width(); const auto visibleHeight = viewport.height(); const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight; const auto minItemHeight = Media::MinItemHeight(_type, visibleWidth); const auto preloadedCount = preloadedHeight / minItemHeight; const auto preloadIdsLimitMin = (preloadedCount / 2) + 1; const auto preloadIdsLimit = preloadIdsLimitMin + (visibleHeight / minItemHeight); const auto after = _slice.skippedAfter(); const auto topLoaded = after && (*after == 0); const auto before = _slice.skippedBefore(); const auto bottomLoaded = before && (*before == 0); const auto minScreenDelta = kPreloadedScreensCount - Media::kPreloadIfLessThanScreens; const auto minUniversalIdDelta = (minScreenDelta * visibleHeight) / minItemHeight; const auto preloadAroundItem = [&](not_null layout) { auto preloadRequired = false; auto aroundId = layout->getItem()->position(); if (!preloadRequired) { preloadRequired = (_idsLimit < preloadIdsLimitMin); } if (!preloadRequired) { auto delta = _slice.distance( sliceKey(_aroundId), sliceKey(aroundId)); Assert(delta != std::nullopt); preloadRequired = (qAbs(*delta) >= minUniversalIdDelta); } if (preloadRequired) { _idsLimit = preloadIdsLimit; _aroundId = aroundId; refreshViewer(); } }; if (preloadTop && !topLoaded) { preloadAroundItem(topLayout); } else if (preloadBottom && !bottomLoaded) { preloadAroundItem(bottomLayout); } } rpl::producer Provider::source( Type type, Data::MessagePosition aroundId, QString query, int limitBefore, int limitAfter) { Expects(_type == type); _totalListQuery = query; return [=](auto consumer) { auto lifetime = rpl::lifetime(); const auto session = &_controller->session(); struct State : base::has_weak_ptr { State(not_null session) : session(session) { } ~State() { session->api().request(requestId).cancel(); } const not_null session; Fn pushAndLoadMore; mtpRequestId requestId = 0; }; const auto state = lifetime.make_state(session); const auto guard = base::make_weak(state); state->pushAndLoadMore = [=] { auto result = fillRequest(aroundId, limitBefore, limitAfter); // May destroy 'state' by calling source() with different args. consumer.put_next(std::move(result.slice)); if (guard && !currentList()->loaded && result.notEnough) { state->requestId = requestMore(state->pushAndLoadMore); } }; state->pushAndLoadMore(); return lifetime; }; } mtpRequestId Provider::requestMore(Fn loaded) { const auto done = [=](const Api::GlobalMediaResult &result) { const auto list = currentList(); if (result.messageIds.empty()) { list->loaded = true; list->fullCount = list->list.size(); } else { list->list.reserve(list->list.size() + result.messageIds.size()); list->fullCount = result.fullCount; for (const auto &position : result.messageIds) { _seenIds.emplace(position.fullId); list->offsetPosition = position; list->list.push_back(position); } } if (!result.offsetRate) { list->loaded = true; } else { list->offsetRate = result.offsetRate; } loaded(); }; const auto list = currentList(); return _controller->session().api().requestGlobalMedia( _type, _totalListQuery, list->offsetRate, list->offsetPosition, done); } Provider::FillResult Provider::fillRequest( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { const auto list = currentList(); const auto i = ranges::lower_bound( list->list, aroundId, std::greater<>()); const auto hasAfter = int(i - begin(list->list)); const auto hasBefore = int(end(list->list) - i); const auto takeAfter = std::min(limitAfter, hasAfter); const auto takeBefore = std::min(limitBefore, hasBefore); auto messages = std::vector{ i - takeAfter, i + takeBefore, }; return FillResult{ .slice = GlobalMediaSlice( GlobalMediaKey{ aroundId }, std::move(messages), ((!list->list.empty() || list->loaded) ? list->fullCount : std::optional()), hasAfter - takeAfter), .notEnough = (takeBefore < limitBefore), }; } void Provider::refreshViewer() { _viewerLifetime.destroy(); _controller->searchQueryValue( ) | rpl::map([=](QString query) { return source( _type, sliceKey(_aroundId).aroundId, query, _idsLimit, _idsLimit); }) | rpl::flatten_latest( ) | rpl::start_with_next([=](GlobalMediaSlice &&slice) { if (!slice.fullCount()) { // Don't display anything while full count is unknown. return; } _slice = std::move(slice); if (auto nearest = _slice.nearest(_aroundId)) { _aroundId = *nearest; } _refreshed.fire({}); }, _viewerLifetime); } rpl::producer<> Provider::refreshed() { return _refreshed.events(); } std::vector Provider::fillSections( not_null delegate) { markLayoutsStale(); const auto guard = gsl::finally([&] { clearStaleLayouts(); }); auto result = std::vector(); result.emplace_back(_type, sectionDelegate()); auto §ion = result.back(); for (auto i = 0, count = int(_slice.size()); i != count; ++i) { auto position = _slice[i]; if (auto layout = getLayout(position.fullId, delegate)) { section.addItem(layout); } } if (section.empty()) { result.pop_back(); } return result; } void Provider::markLayoutsStale() { for (auto &layout : _layouts) { layout.second.stale = true; } } void Provider::clearStaleLayouts() { for (auto i = _layouts.begin(); i != _layouts.end();) { if (i->second.stale) { _layoutRemoved.fire(i->second.item.get()); i = _layouts.erase(i); } else { ++i; } } } Provider::List *Provider::currentList() { return &_totalLists[_totalListQuery]; } rpl::producer> Provider::layoutRemoved() { return _layoutRemoved.events(); } Media::BaseLayout *Provider::lookupLayout( const HistoryItem *item) { const auto i = _layouts.find(item ? item->fullId() : FullMsgId()); return (i != _layouts.end()) ? i->second.item.get() : nullptr; } bool Provider::isMyItem(not_null item) { return _seenIds.contains(item->fullId()); } bool Provider::isAfter( not_null a, not_null b) { return (a->fullId() < b->fullId()); } void Provider::setSearchQuery(QString query) { Unexpected("Media::Provider::setSearchQuery."); } GlobalMediaKey Provider::sliceKey(Data::MessagePosition aroundId) const { return GlobalMediaKey{ aroundId }; } void Provider::itemRemoved(not_null item) { const auto id = item->fullId(); if (const auto i = _layouts.find(id); i != end(_layouts)) { _layoutRemoved.fire(i->second.item.get()); _layouts.erase(i); } } Media::BaseLayout *Provider::getLayout( FullMsgId itemId, not_null delegate) { auto it = _layouts.find(itemId); if (it == _layouts.end()) { if (auto layout = createLayout(itemId, delegate, _type)) { layout->initDimensions(); it = _layouts.emplace( itemId, std::move(layout)).first; } else { return nullptr; } } it->second.stale = false; return it->second.item.get(); } std::unique_ptr Provider::createLayout( FullMsgId itemId, not_null delegate, Type type) { const auto item = _controller->session().data().message(itemId); if (!item) { return nullptr; } const auto getPhoto = [&]() -> PhotoData* { if (const auto media = item->media()) { return media->photo(); } return nullptr; }; const auto getFile = [&]() -> DocumentData* { if (const auto media = item->media()) { return media->document(); } return nullptr; }; const auto &songSt = st::overviewFileLayout; using namespace Overview::Layout; const auto options = [&] { const auto media = item->media(); return MediaOptions{ .spoiler = media && media->hasSpoiler() }; }; switch (type) { case Type::Photo: if (const auto photo = getPhoto()) { return std::make_unique( delegate, item, photo, options()); } return nullptr; case Type::GIF: if (const auto file = getFile()) { return std::make_unique(delegate, item, file); } return nullptr; case Type::Video: if (const auto file = getFile()) { return std::make_unique