/* 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/media/info_media_provider.h" #include "info/media/info_media_widget.h" #include "info/media/info_media_list_section.h" #include "info/info_controller.h" #include "layout/layout_selection.h" #include "main/main_session.h" #include "lang/lang_keys.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "data/data_session.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" #include "data/data_user.h" #include "data/data_peer_values.h" #include "data/data_document.h" #include "styles/style_info.h" #include "styles/style_overview.h" namespace Info::Media { namespace { constexpr auto kPreloadedScreensCount = 4; constexpr auto kPreloadedScreensCountFull = kPreloadedScreensCount + 1 + kPreloadedScreensCount; [[nodiscard]] int MinItemHeight(Type type, int width) { auto &songSt = st::overviewFileLayout; switch (type) { case Type::Photo: case Type::GIF: case Type::Video: case Type::RoundFile: { auto itemsLeft = st::infoMediaSkip; auto itemsInRow = (width - itemsLeft) / (st::infoMediaMinGridSize + st::infoMediaSkip); return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; } break; case Type::RoundVoiceFile: return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom() + st::lineWidth; case Type::File: return songSt.filePadding.top() + songSt.fileThumbSize + songSt.filePadding.bottom() + st::lineWidth; case Type::MusicFile: return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom(); case Type::Link: return st::linksPhotoSize + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder; } Unexpected("Type in MinItemHeight()"); } } // namespace Provider::Provider(not_null controller) : _controller(controller) , _peer(_controller->key().peer()) , _topicRootId(_controller->key().topic() ? _controller->key().topic()->rootId() : 0) , _migrated(_controller->migrated()) , _type(_controller->section().mediaType()) , _slice(sliceKey(_universalAroundId)) { _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); } Type Provider::type() { return _type; } bool Provider::hasSelectRestriction() { if (_peer->allowsForwarding()) { return false; } else if (const auto chat = _peer->asChat()) { return !chat->canDeleteMessages(); } else if (const auto channel = _peer->asChannel()) { return !channel->canDeleteMessages(); } return true; } rpl::producer Provider::hasSelectRestrictionChanges() { if (_peer->isUser()) { return rpl::never(); } const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); auto noForwards = chat ? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards) : Data::PeerFlagValue( channel, ChannelDataFlag::NoForwards ) | rpl::type_erased(); auto rights = chat ? chat->adminRightsValue() : channel->adminRightsValue(); auto canDelete = std::move( rights ) | rpl::map([=] { return chat ? chat->canDeleteMessages() : channel->canDeleteMessages(); }); return rpl::combine( std::move(noForwards), std::move(canDelete) ) | rpl::map([=] { return hasSelectRestriction(); }) | rpl::distinct_until_changed() | rpl::skip(1); } 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) { switch (_type) { case Type::Photo: case Type::GIF: case Type::Video: case Type::RoundFile: case Type::RoundVoiceFile: case Type::File: return langMonthFull(item->dateTime().date()); case Type::Link: return langDayOfMonthFull(item->dateTime().date()); case Type::MusicFile: return QString(); } Unexpected("Type in ListSection::setHeader()"); } bool Provider::sectionItemBelongsHere( not_null item, not_null previous) { const auto date = item->dateTime().date(); const auto sectionDate = previous->dateTime().date(); switch (_type) { case Type::Photo: case Type::GIF: case Type::Video: case Type::RoundFile: case Type::RoundVoiceFile: case Type::File: return date.year() == sectionDate.year() && date.month() == sectionDate.month(); case Type::Link: return date == sectionDate; case Type::MusicFile: return true; } Unexpected("Type in ListSection::belongsHere()"); } bool Provider::isPossiblyMyItem(not_null item) { return isPossiblyMyPeerId(item->history()->peer->id); } bool Provider::isPossiblyMyPeerId(PeerId peerId) const { return (peerId == _peer->id) || (_migrated && peerId == _migrated->id); } std::optional Provider::fullCount() { return _slice.fullCount(); } void Provider::restart() { _layouts.clear(); _universalAroundId = kDefaultAroundId; _idsLimit = kMinimalIdsLimit; _slice = SparseIdsMergedSlice(sliceKey(_universalAroundId)); 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 = 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 - kPreloadIfLessThanScreens; const auto minUniversalIdDelta = (minScreenDelta * visibleHeight) / minItemHeight; const auto preloadAroundItem = [&](not_null layout) { auto preloadRequired = false; auto universalId = GetUniversalId(layout); if (!preloadRequired) { preloadRequired = (_idsLimit < preloadIdsLimitMin); } if (!preloadRequired) { auto delta = _slice.distance( sliceKey(_universalAroundId), sliceKey(universalId)); Assert(delta != std::nullopt); preloadRequired = (qAbs(*delta) >= minUniversalIdDelta); } if (preloadRequired) { _idsLimit = preloadIdsLimit; _universalAroundId = universalId; refreshViewer(); } }; if (preloadTop && !topLoaded) { preloadAroundItem(topLayout); } else if (preloadBottom && !bottomLoaded) { preloadAroundItem(bottomLayout); } } void Provider::refreshViewer() { _viewerLifetime.destroy(); const auto idForViewer = sliceKey(_universalAroundId).universalId; _controller->mediaSource( idForViewer, _idsLimit, _idsLimit ) | rpl::start_with_next([=](SparseIdsMergedSlice &&slice) { if (!slice.fullCount()) { // Don't display anything while full count is unknown. return; } _slice = std::move(slice); if (auto nearest = _slice.nearest(idForViewer)) { _universalAroundId = GetUniversalId(*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, sectionDelegate()); auto count = _slice.size(); for (auto i = count; i != 0;) { auto universalId = GetUniversalId(_slice[--i]); if (auto layout = getLayout(universalId, delegate)) { if (!section.addItem(layout)) { section.finishSection(); result.push_back(std::move(section)); section = ListSection(_type, 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) { _layoutRemoved.fire(i->second.item.get()); i = _layouts.erase(i); } else { ++i; } } } rpl::producer> Provider::layoutRemoved() { return _layoutRemoved.events(); } BaseLayout *Provider::lookupLayout( const HistoryItem *item) { const auto i = _layouts.find(GetUniversalId(item)); return (i != _layouts.end()) ? i->second.item.get() : nullptr; } bool Provider::isMyItem(not_null item) { const auto peer = item->history()->peer; return (_peer == peer) || (_migrated == peer); } bool Provider::isAfter( not_null a, not_null b) { return (GetUniversalId(a) < GetUniversalId(b)); } void Provider::setSearchQuery(QString query) { Unexpected("Media::Provider::setSearchQuery."); } SparseIdsMergedSlice::Key Provider::sliceKey( UniversalMsgId universalId) const { using Key = SparseIdsMergedSlice::Key; if (!_topicRootId && _migrated) { return Key(_peer->id, _topicRootId, _migrated->id, universalId); } if (universalId < 0) { // Convert back to plain id for non-migrated histories. universalId = universalId + ServerMaxMsgId; } return Key(_peer->id, _topicRootId, 0, universalId); } void Provider::itemRemoved(not_null item) { const auto id = GetUniversalId(item); if (const auto i = _layouts.find(id); i != end(_layouts)) { _layoutRemoved.fire(i->second.item.get()); _layouts.erase(i); } } FullMsgId Provider::computeFullId( UniversalMsgId universalId) const { Expects(universalId != 0); return (universalId > 0) ? FullMsgId(_peer->id, universalId) : FullMsgId( (_migrated ? _migrated : _peer.get())->id, ServerMaxMsgId + universalId); } BaseLayout *Provider::getLayout( UniversalMsgId universalId, not_null delegate) { auto it = _layouts.find(universalId); if (it == _layouts.end()) { if (auto layout = createLayout(universalId, delegate, _type)) { layout->initDimensions(); it = _layouts.emplace( universalId, std::move(layout)).first; } else { return nullptr; } } it->second.stale = false; return it->second.item.get(); } std::unique_ptr Provider::createLayout( UniversalMsgId universalId, not_null delegate, Type type) { const auto item = _controller->session().data().message( computeFullId(universalId)); 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