/* 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/stories/info_stories_provider.h" #include "info/media/info_media_widget.h" #include "info/media/info_media_list_section.h" #include "info/info_controller.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_stories_ids.h" #include "main/main_account.h" #include "main/main_session.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_info.h" namespace Info::Stories { namespace { using namespace Media; constexpr auto kPreloadedScreensCount = 4; constexpr auto kPreloadedScreensCountFull = kPreloadedScreensCount + 1 + kPreloadedScreensCount; [[nodiscard]] int MinStoryHeight(int width) { auto itemsLeft = st::infoMediaSkip; auto itemsInRow = (width - itemsLeft) / (st::infoMediaMinGridSize + st::infoMediaSkip); return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; } } // namespace Provider::Provider(not_null controller) : _controller(controller) , _peer(controller->key().storiesPeer()) , _history(_peer->owner().history(_peer)) , _tab(controller->key().storiesTab()) { style::PaletteChanged( ) | rpl::start_with_next([=] { for (auto &layout : _layouts) { layout.second.item->invalidateCache(); } }, _lifetime); _peer->session().changes().storyUpdates( Data::StoryUpdate::Flag::Destroyed ) | rpl::filter([=](const Data::StoryUpdate &update) { return update.story->peer() == _peer; }) | rpl::start_with_next([=](const Data::StoryUpdate &update) { storyRemoved(update.story); }, _lifetime); } Provider::~Provider() { clear(); } Type Provider::type() { return Type::PhotoVideo; } bool Provider::hasSelectRestriction() { if (const auto channel = _peer->asChannel()) { return !channel->canEditStories() && !channel->canDeleteStories(); } return !_peer->isSelf(); } rpl::producer Provider::hasSelectRestrictionChanges() { return rpl::never(); } bool Provider::sectionHasFloatingHeader() { return false; } 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 true; } std::optional Provider::fullCount() { return _slice.fullCount(); } void Provider::clear() { for (const auto &[storyId, _] : _layouts) { _peer->owner().stories().unregisterPolling( { _peer->id, storyId }, Data::Stories::Polling::Chat); } _layouts.clear(); _aroundId = kDefaultAroundId; _idsLimit = kMinimalIdsLimit; _slice = Data::StoriesIdsSlice(); } void Provider::restart() { clear(); 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 = MinStoryHeight(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 - kPreloadIfLessThanScreens; const auto minIdDelta = (minScreenDelta * visibleHeight) / minItemHeight; const auto preloadAroundItem = [&](not_null layout) { auto preloadRequired = false; const auto id = StoryIdFromMsgId(layout->getItem()->id); if (!preloadRequired) { preloadRequired = (_idsLimit < preloadIdsLimitMin); } if (!preloadRequired) { auto delta = _slice.distance(_aroundId, id); Assert(delta != std::nullopt); preloadRequired = (qAbs(*delta) >= minIdDelta); } if (preloadRequired) { _idsLimit = preloadIdsLimit; _aroundId = id; refreshViewer(); } }; if (preloadTop && !topLoaded) { preloadAroundItem(topLayout); } else if (preloadBottom && !bottomLoaded) { preloadAroundItem(bottomLayout); } } void Provider::setSearchQuery(QString query) { } void Provider::refreshViewer() { _viewerLifetime.destroy(); const auto idForViewer = _aroundId; auto ids = (_tab == Tab::Saved) ? Data::SavedStoriesIds(_peer, idForViewer, _idsLimit) : Data::ArchiveStoriesIds(_peer, idForViewer, _idsLimit); std::move( ids ) | rpl::start_with_next([=](Data::StoriesIdsSlice &&slice) { if (!slice.fullCount()) { // Don't display anything while full count is unknown. return; } _slice = std::move(slice); auto nearestId = std::optional(); for (auto i = 0; i != _slice.size(); ++i) { if (!nearestId || std::abs(*nearestId - idForViewer) > std::abs(_slice[i] - idForViewer)) { nearestId = _slice[i]; } } if (nearestId) { _aroundId = *nearestId; } //if (const auto nearest = _slice.nearest(idForViewer)) { // _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(); auto section = ListSection(Type::PhotoVideo, sectionDelegate()); auto count = _slice.size(); for (auto i = 0; i != count; ++i) { const auto storyId = _slice[i]; if (const auto layout = getLayout(storyId, delegate)) { if (!section.addItem(layout)) { section.finishSection(); result.push_back(std::move(section)); section = ListSection(Type::PhotoVideo, sectionDelegate()); section.addItem(layout); } } } if (!section.empty()) { section.finishSection(); result.push_back(std::move(section)); } 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) { _peer->owner().stories().unregisterPolling( { _peer->id, i->first }, Data::Stories::Polling::Chat); _layoutRemoved.fire(i->second.item.get()); const auto taken = _items.take(i->first); i = _layouts.erase(i); } else { ++i; } } } rpl::producer> Provider::layoutRemoved() { return _layoutRemoved.events(); } BaseLayout *Provider::lookupLayout(const HistoryItem *item) { return nullptr; } bool Provider::isMyItem(not_null item) { return IsStoryMsgId(item->id) && (item->history()->peer == _peer); } bool Provider::isAfter( not_null a, not_null b) { return (a->id < b->id); } void Provider::storyRemoved(not_null story) { Expects(story->peer() == _peer); if (const auto i = _layouts.find(story->id()); i != end(_layouts)) { _peer->owner().stories().unregisterPolling( story, Data::Stories::Polling::Chat); _layoutRemoved.fire(i->second.item.get()); _layouts.erase(i); } _items.remove(story->id()); } BaseLayout *Provider::getLayout( StoryId id, not_null delegate) { auto it = _layouts.find(id); if (it == _layouts.end()) { if (auto layout = createLayout(id, delegate)) { layout->initDimensions(); it = _layouts.emplace(id, std::move(layout)).first; const auto ok = _peer->owner().stories().registerPolling( { _peer->id, id }, Data::Stories::Polling::Chat); Assert(ok); } else { return nullptr; } } it->second.stale = false; return it->second.item.get(); } HistoryItem *Provider::ensureItem(StoryId id) { const auto i = _items.find(id); if (i != end(_items)) { return i->second.get(); } auto item = _peer->owner().stories().resolveItem({ _peer->id, id }); if (!item) { return nullptr; } return _items.emplace(id, std::move(item)).first->second.get(); } std::unique_ptr Provider::createLayout( StoryId id, not_null delegate) { const auto item = ensureItem(id); 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; }; using namespace Overview::Layout; const auto options = MediaOptions{ .pinned = item->isPinned(), .story = true, }; if (const auto photo = getPhoto()) { return std::make_unique(delegate, item, photo, options); } else if (const auto file = getFile()) { return std::make_unique