diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index af21be839..2e5178a2e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -946,6 +946,12 @@ PRIVATE info/downloads/info_downloads_provider.h info/downloads/info_downloads_widget.cpp info/downloads/info_downloads_widget.h + info/global_media/info_global_media_widget.cpp + info/global_media/info_global_media_widget.h + info/global_media/info_global_media_inner_widget.cpp + info/global_media/info_global_media_inner_widget.h + info/global_media/info_global_media_provider.cpp + info/global_media/info_global_media_provider.h info/media/info_media_buttons.h info/media/info_media_common.cpp info/media/info_media_common.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2a2b05514..1bc6933bf 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5832,6 +5832,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recent_chats" = "Chats"; "lng_recent_channels" = "Channels"; "lng_recent_apps" = "Apps"; +"lng_all_photos" = "Photos"; +"lng_all_videos" = "Videos"; +"lng_all_downloads" = "Downloads"; +"lng_all_links" = "Links"; +"lng_all_files" = "Files"; +"lng_all_music" = "Music"; +"lng_all_voice" = "Voice"; "lng_channels_none_title" = "No channels yet..."; "lng_channels_none_about" = "You are not currently subscribed to any channels."; "lng_channels_your_title" = "Channels you joined"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 316aed080..12f048f76 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3219,6 +3219,31 @@ void ApiWrap::sharedMediaDone( } } +mtpRequestId ApiWrap::requestGlobalMedia( + Storage::SharedMediaType type, + const QString &query, + int32 offsetRate, + Data::MessagePosition offsetPosition, + Fn done) { + auto prepared = Api::PrepareGlobalMediaRequest( + _session, + offsetRate, + offsetPosition, + type, + query); + if (!prepared) { + done({}); + return 0; + } + return request( + std::move(*prepared) + ).done([=](const Api::SearchRequestResult &result) { + done(Api::ParseGlobalMediaResult(_session, result)); + }).fail([=] { + done({}); + }).send(); +} + void ApiWrap::sendAction(const SendAction &action) { if (!action.options.scheduled && !action.options.shortcutId diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index f25368293..cfbb71256 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -59,6 +59,7 @@ class Show; namespace Api { struct SearchResult; +struct GlobalMediaResult; class Updates; class Authorizations; @@ -288,6 +289,12 @@ public: Storage::SharedMediaType type, MsgId messageId, SliceType slice); + mtpRequestId requestGlobalMedia( + Storage::SharedMediaType type, + const QString &query, + int32 offsetRate, + Data::MessagePosition offsetPosition, + Fn done); void readFeaturedSetDelayed(uint64 setId); @@ -509,6 +516,10 @@ private: MsgId topicRootId, SharedMediaType type, Api::SearchResult &&parsed); + void globalMediaDone( + SharedMediaType type, + FullMsgId messageId, + Api::GlobalMediaResult &&parsed); void sendSharedContact( const QString &phone, @@ -672,6 +683,17 @@ private: }; base::flat_set _historyRequests; + struct GlobalMediaRequest { + SharedMediaType mediaType = {}; + FullMsgId aroundId; + SliceType sliceType = {}; + + friend inline auto operator<=>( + const GlobalMediaRequest&, + const GlobalMediaRequest&) = default; + }; + base::flat_set _globalMediaRequests; + std::unique_ptr _dialogsLoadState; TimeId _dialogsLoadTill = 0; rpl::variable _dialogsLoadMayBlockByDate = false; diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 575627d71..48c57091b 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -223,4 +223,13 @@ struct hash { } }; +template <> +struct hash { + size_t operator()(FullMsgId value) const { + return QtPrivate::QHashCombine().operator()( + std::hash()(value.peer.value), + value.msg.bare); + } +}; + } // namespace std diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 4e33a9eaa..4a2418eb9 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -26,6 +26,109 @@ constexpr auto kDefaultSearchTimeoutMs = crl::time(200); } // namespace +MTPMessagesFilter PrepareSearchFilter(Storage::SharedMediaType type) { + using Type = Storage::SharedMediaType; + switch (type) { + case Type::Photo: + return MTP_inputMessagesFilterPhotos(); + case Type::Video: + return MTP_inputMessagesFilterVideo(); + case Type::PhotoVideo: + return MTP_inputMessagesFilterPhotoVideo(); + case Type::MusicFile: + return MTP_inputMessagesFilterMusic(); + case Type::File: + return MTP_inputMessagesFilterDocument(); + case Type::VoiceFile: + return MTP_inputMessagesFilterVoice(); + case Type::RoundVoiceFile: + return MTP_inputMessagesFilterRoundVoice(); + case Type::RoundFile: + return MTP_inputMessagesFilterRoundVideo(); + case Type::GIF: + return MTP_inputMessagesFilterGif(); + case Type::Link: + return MTP_inputMessagesFilterUrl(); + case Type::ChatPhoto: + return MTP_inputMessagesFilterChatPhotos(); + case Type::Pinned: + return MTP_inputMessagesFilterPinned(); + } + return MTP_inputMessagesFilterEmpty(); +} + +std::optional PrepareGlobalMediaRequest( + not_null session, + int32 offsetRate, + Data::MessagePosition offsetPosition, + Storage::SharedMediaType type, + const QString &query) { + const auto filter = PrepareSearchFilter(type); + if (query.isEmpty() && filter.type() == mtpc_inputMessagesFilterEmpty) { + return std::nullopt; + } + + const auto minDate = 0; + const auto maxDate = 0; + const auto folderId = 0; + const auto limit = offsetPosition.fullId.peer + ? kSharedMediaLimit + : kFirstSharedMediaLimit; + return MTPmessages_SearchGlobal( + MTP_flags(MTPmessages_SearchGlobal::Flag::f_folder_id), // No archive + MTP_int(folderId), + MTP_string(query), + filter, + MTP_int(minDate), + MTP_int(maxDate), + MTP_int(offsetRate), + (offsetPosition.fullId.peer + ? session->data().peer(PeerId(offsetPosition.fullId.peer))->input + : MTP_inputPeerEmpty()), + MTP_int(offsetPosition.fullId.msg), + MTP_int(limit)); +} + +GlobalMediaResult ParseGlobalMediaResult( + not_null session, + const MTPmessages_Messages &data) { + auto result = GlobalMediaResult(); + + auto messages = (const QVector*)nullptr; + data.match([&](const MTPDmessages_messagesNotModified &) { + }, [&](const auto &data) { + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + messages = &data.vmessages().v; + }); + data.match([&](const MTPDmessages_messagesNotModified &) { + }, [&](const MTPDmessages_messages &data) { + result.fullCount = data.vmessages().v.size(); + }, [&](const MTPDmessages_messagesSlice &data) { + result.fullCount = data.vcount().v; + result.offsetRate = data.vnext_rate().value_or_empty(); + }, [&](const MTPDmessages_channelMessages &data) { + result.fullCount = data.vcount().v; + }); + data.match([&](const MTPDmessages_channelMessages &data) { + LOG(("API Error: received messages.channelMessages when " + "no channel was passed! (ParseSearchResult)")); + }, [](const auto &) {}); + + const auto addType = NewMessageType::Existing; + result.messageIds.reserve(messages->size()); + for (const auto &message : *messages) { + const auto item = session->data().addNewMessage( + message, + MessageFlags(), + addType); + if (item) { + result.messageIds.push_back(item->position()); + } + } + return result; +} + std::optional PrepareSearchRequest( not_null peer, MsgId topicRootId, @@ -33,36 +136,7 @@ std::optional PrepareSearchRequest( const QString &query, MsgId messageId, Data::LoadDirection direction) { - const auto filter = [&] { - using Type = Storage::SharedMediaType; - switch (type) { - case Type::Photo: - return MTP_inputMessagesFilterPhotos(); - case Type::Video: - return MTP_inputMessagesFilterVideo(); - case Type::PhotoVideo: - return MTP_inputMessagesFilterPhotoVideo(); - case Type::MusicFile: - return MTP_inputMessagesFilterMusic(); - case Type::File: - return MTP_inputMessagesFilterDocument(); - case Type::VoiceFile: - return MTP_inputMessagesFilterVoice(); - case Type::RoundVoiceFile: - return MTP_inputMessagesFilterRoundVoice(); - case Type::RoundFile: - return MTP_inputMessagesFilterRoundVideo(); - case Type::GIF: - return MTP_inputMessagesFilterGif(); - case Type::Link: - return MTP_inputMessagesFilterUrl(); - case Type::ChatPhoto: - return MTP_inputMessagesFilterChatPhotos(); - case Type::Pinned: - return MTP_inputMessagesFilterPinned(); - } - return MTP_inputMessagesFilterEmpty(); - }(); + const auto filter = PrepareSearchFilter(type); if (query.isEmpty() && filter.type() == mtpc_inputMessagesFilterEmpty) { return std::nullopt; } diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index 26b930b9b..c73fb7b39 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -19,6 +19,7 @@ class Session; namespace Data { enum class LoadDirection : char; +struct MessagePosition; } // namespace Data namespace Api { @@ -36,6 +37,27 @@ using HistoryResult = SearchResult; using HistoryRequest = MTPmessages_GetHistory; using HistoryRequestResult = MTPmessages_Messages; +using GlobalMediaRequest = MTPmessages_SearchGlobal; +struct GlobalMediaResult { + std::vector messageIds; + int32 offsetRate = 0; + int fullCount = 0; +}; + +[[nodiscard]] MTPMessagesFilter PrepareSearchFilter( + Storage::SharedMediaType type); + +[[nodiscard]] std::optional PrepareGlobalMediaRequest( + not_null session, + int32 offsetRate, + Data::MessagePosition offsetPosition, + Storage::SharedMediaType type, + const QString &query); + +[[nodiscard]] GlobalMediaResult ParseGlobalMediaResult( + not_null session, + const MTPmessages_Messages &data); + [[nodiscard]] std::optional PrepareSearchRequest( not_null peer, MsgId topicRootId, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 6b4edc0fe..387daa83c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -23,10 +23,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "dialogs/ui/chat_search_empty.h" #include "history/history.h" +#include "info/downloads/info_downloads_widget.h" +#include "info/media/info_media_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "info/info_wrap_widget.h" #include "inline_bots/bot_attach_web_view.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" +#include "storage/storage_shared_media.h" #include "ui/boxes/confirm_box.h" #include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" @@ -1297,6 +1303,18 @@ Suggestions::Suggestions( , _tabs( _tabsScroll->setOwnedWidget( object_ptr(this, st::dialogsSearchTabs))) +, _tabKeys{ + { Tab::Chats }, + { Tab::Channels }, + { Tab::Apps }, + { Tab::Media, MediaType::Photo }, + { Tab::Media, MediaType::Video }, + { Tab::Downloads }, + { Tab::Media, MediaType::Link }, + { Tab::Media, MediaType::File }, + { Tab::Media, MediaType::MusicFile }, + { Tab::Media, MediaType::RoundVoiceFile }, +} , _chatsScroll(std::make_unique(this)) , _chatsContent( _chatsScroll->setOwnedWidget(object_ptr(this))) @@ -1375,21 +1393,37 @@ void Suggestions::setupTabs() { shadow->setGeometry(0, height - line, width, line); }, shadow->lifetime()); - shadow->showOn(_tabs->shownValue()); + shadow->showOn(_tabsScroll->shownValue()); - auto sections = std::vector{ - tr::lng_recent_chats(tr::now), - tr::lng_recent_channels(tr::now), - tr::lng_recent_apps(tr::now), + const auto labels = base::flat_map{ + { Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) }, + { Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) }, + { Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) }, + { Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) }, + { Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) }, + { Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) }, + { Key{ Tab::Media, MediaType::Link }, tr::lng_all_links(tr::now) }, + { Key{ Tab::Media, MediaType::File }, tr::lng_all_files(tr::now) }, + { + Key{ Tab::Media, MediaType::MusicFile }, + tr::lng_all_music(tr::now), + }, + { + Key{ Tab::Media, MediaType::RoundVoiceFile }, + tr::lng_all_voice(tr::now), + }, }; + auto sections = std::vector(); + for (const auto key : _tabKeys) { + const auto i = labels.find(key); + Assert(i != end(labels)); + sections.push_back(i->second); + } _tabs->setSections(sections); _tabs->sectionActivated( ) | rpl::start_with_next([=](int section) { - switchTab(section == 2 - ? Tab::Apps - : section - ? Tab::Channels - : Tab::Chats); + Assert(section >= 0 && section < _tabKeys.size()); + switchTab(_tabKeys[section]); }, _tabs->lifetime()); } @@ -1459,7 +1493,7 @@ void Suggestions::setupChats() { _chatsScroll->viewportEvent(e); }, _topPeers->lifetime()); - _chatsScroll->setVisible(_tab.current() == Tab::Chats); + _chatsScroll->setVisible(_key.current().tab == Tab::Chats); _chatsScroll->setCustomTouchProcess(_recent->processTouch); } @@ -1493,7 +1527,7 @@ void Suggestions::setupChannels() { rpl::mappers::_1 + rpl::mappers::_2 == 0), anim::type::instant); - _channelsScroll->setVisible(_tab.current() == Tab::Channels); + _channelsScroll->setVisible(_key.current().tab == Tab::Channels); _channelsScroll->setCustomTouchProcess([=](not_null e) { const auto myChannels = _myChannels->processTouch(e); const auto recommendations = _recommendations->processTouch(e); @@ -1510,7 +1544,7 @@ void Suggestions::setupApps() { _popularApps->wrap->toggle(count > 0, anim::type::instant); }, _popularApps->wrap->lifetime()); - _appsScroll->setVisible(_tab.current() == Tab::Apps); + _appsScroll->setVisible(_key.current().tab == Tab::Apps); _appsScroll->setCustomTouchProcess([=](not_null e) { const auto recentApps = _recentApps->processTouch(e); const auto popularApps = _popularApps->processTouch(e); @@ -1519,12 +1553,11 @@ void Suggestions::setupApps() { } void Suggestions::selectJump(Qt::Key direction, int pageSize) { - switch (_tab.current()) { + switch (_key.current().tab) { case Tab::Chats: selectJumpChats(direction, pageSize); return; case Tab::Channels: selectJumpChannels(direction, pageSize); return; case Tab::Apps: selectJumpApps(direction, pageSize); return; } - Unexpected("Tab in Suggestions::selectJump."); } void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) { @@ -1702,7 +1735,7 @@ void Suggestions::selectJumpApps(Qt::Key direction, int pageSize) { } void Suggestions::chooseRow() { - switch (_tab.current()) { + switch (_key.current().tab) { case Tab::Chats: if (!_topPeers->chooseRow()) { _recent->choose(); @@ -1722,9 +1755,11 @@ void Suggestions::chooseRow() { } Data::Thread *Suggestions::updateFromParentDrag(QPoint globalPosition) { - return (_tab.current() == Tab::Chats) - ? updateFromChatsDrag(globalPosition) - : updateFromChannelsDrag(globalPosition); + switch (_key.current().tab) { + case Tab::Chats: return updateFromChatsDrag(globalPosition); + case Tab::Channels: return updateFromChannelsDrag(globalPosition); + } + return nullptr; } Data::Thread *Suggestions::updateFromChatsDrag(QPoint globalPosition) { @@ -1785,39 +1820,69 @@ void Suggestions::hide(anim::type animated, Fn finish) { } } -void Suggestions::switchTab(Tab tab) { - const auto was = _tab.current(); - if (was == tab) { +void Suggestions::switchTab(Key key) { + const auto was = _key.current(); + if (was == key) { return; } - _tab = tab; + _key = key; _persist = false; if (_tabs->isHidden()) { return; } - startSlideAnimation(was, tab); + startSlideAnimation(was, key); } -void Suggestions::startSlideAnimation(Tab was, Tab now) { - if (!_slideAnimation.animating()) { - _slideLeft = (was == Tab::Chats || now == Tab::Chats) - ? Ui::GrabWidget(_chatsScroll.get()) - : Ui::GrabWidget(_channelsScroll.get()); - _slideLeftTop = (was == Tab::Chats || now == Tab::Chats) - ? _chatsScroll->y() - : _channelsScroll->y(); - _slideRight = (was == Tab::Apps || now == Tab::Apps) - ? Ui::GrabWidget(_appsScroll.get()) - : Ui::GrabWidget(_channelsScroll.get()); - _slideRightTop = (was == Tab::Apps || now == Tab::Apps) - ? _appsScroll->y() - : _channelsScroll->y(); - _chatsScroll->hide(); - _channelsScroll->hide(); - _appsScroll->hide(); +void Suggestions::ensureContent(Key key) { + if (key.tab != Tab::Downloads && key.tab != Tab::Media) { + return; } - const auto from = (now > was) ? 0. : 1.; - const auto to = (now > was) ? 1. : 0.; + auto &list = _mediaLists[key]; + if (list.wrap) { + return; + } + const auto self = _controller->session().user(); + const auto memento = (key.tab == Tab::Downloads) + ? Info::Downloads::Make(self) + : std::make_shared( + self, + Info::Section(key.mediaType, Info::Section::Type::GlobalMedia)); + list.wrap = Ui::CreateChild( + this, + _controller, + Info::Wrap::Search, + memento.get()); + list.wrap->show(); + updateControlsGeometry(); +} + +void Suggestions::startSlideAnimation(Key was, Key now) { + ensureContent(now); + const auto wasIndex = ranges::find(_tabKeys, was); + const auto nowIndex = ranges::find(_tabKeys, now); + if (!_slideAnimation.animating()) { + const auto find = [&](Key key) -> not_null { + switch (key.tab) { + case Tab::Chats: return _chatsScroll.get(); + case Tab::Channels: return _channelsScroll.get(); + case Tab::Apps: return _appsScroll.get(); + } + return _mediaLists[key].wrap; + }; + auto left = find(was); + auto right = find(now); + if (wasIndex > nowIndex) { + std::swap(left, right); + } + _slideLeft = Ui::GrabWidget(left); + _slideLeftTop = left->y(); + _slideRight = Ui::GrabWidget(right); + _slideRightTop = right->y(); + left->hide(); + right->hide(); + } + const auto from = (nowIndex > wasIndex) ? 0. : 1.; + const auto to = (nowIndex > wasIndex) ? 1. : 0.; _slideAnimation.start([=] { update(); if (!_slideAnimation.animating() && !_shownAnimation.animating()) { @@ -1852,6 +1917,9 @@ void Suggestions::startShownAnimation(bool shown, Fn finish) { _chatsScroll->hide(); _channelsScroll->hide(); _appsScroll->hide(); + for (const auto &[key, list] : _mediaLists) { + list.wrap->hide(); + } _slideAnimation.stop(); } @@ -1864,10 +1932,13 @@ void Suggestions::finishShow() { _cache = QPixmap(); _tabsScroll->show(); - const auto tab = _tab.current(); - _chatsScroll->setVisible(tab == Tab::Chats); - _channelsScroll->setVisible(tab == Tab::Channels); - _appsScroll->setVisible(tab == Tab::Apps); + const auto key = _key.current(); + _chatsScroll->setVisible(key == Key{ Tab::Chats }); + _channelsScroll->setVisible(key == Key{ Tab::Channels }); + _appsScroll->setVisible(key == Key{ Tab::Apps }); + for (const auto &[mediaKey, list] : _mediaLists) { + list.wrap->setVisible(key == mediaKey); + } } float64 Suggestions::shownOpacity() const { @@ -1887,7 +1958,7 @@ void Suggestions::paintEvent(QPaintEvent *e) { p.drawPixmap(0, (opacity - 1.) * slide, _cache); } else if (!_slideLeft.isNull()) { const auto slide = st::topPeers.height + st::searchedBarHeight; - const auto right = (_tab.current() == Tab::Channels); + const auto right = (_key.current().tab == Tab::Channels); const auto progress = _slideAnimation.value(right ? 1. : 0.); p.setOpacity(1. - progress); p.drawPixmap( @@ -1903,20 +1974,39 @@ void Suggestions::paintEvent(QPaintEvent *e) { } void Suggestions::resizeEvent(QResizeEvent *e) { + updateControlsGeometry(); +} + +void Suggestions::updateControlsGeometry() { const auto w = std::max(width(), st::columnMinimalWidthLeft); _tabs->fitWidthToSections(); const auto tabs = _tabs->height(); _tabsScroll->setGeometry(0, 0, w, tabs); - _chatsScroll->setGeometry(0, tabs, w, height() - tabs); + const auto content = QRect(0, tabs, w, height() - tabs); + + _chatsScroll->setGeometry(content); _chatsContent->resizeToWidth(w); - _channelsScroll->setGeometry(0, tabs, w, height() - tabs); + _channelsScroll->setGeometry(content); _channelsContent->resizeToWidth(w); - _appsScroll->setGeometry(0, tabs, w, height() - tabs); + _appsScroll->setGeometry(content); _appsContent->resizeToWidth(w); + + const auto expanding = false; + for (const auto &[key, list] : _mediaLists) { + const auto full = !list.wrap->scrollBottomSkip(); + const auto additionalScroll = (full ? st::boxRadius : 0); + const auto height = content.height() - (full ? 0 : st::boxRadius); + const auto wrapGeometry = QRect{ 0, tabs, w, height}; + list.wrap->updateGeometry( + wrapGeometry, + expanding, + additionalScroll, + content.height()); + } } auto Suggestions::setupRecentPeers(RecentPeersList recentPeers) @@ -2070,8 +2160,8 @@ auto Suggestions::setupRecommendations() -> std::unique_ptr { _persist = true; }, list->lifetime()); - _tab.value() | rpl::filter( - rpl::mappers::_1 == Tab::Channels + _key.value() | rpl::filter( + rpl::mappers::_1 == Key{ Tab::Channels } ) | rpl::start_with_next([=] { controller->load(); }, list->lifetime()); @@ -2185,8 +2275,8 @@ auto Suggestions::setupPopularApps() -> std::unique_ptr { _persist = true; }, list->lifetime()); - _tab.value() | rpl::filter( - rpl::mappers::_1 == Tab::Apps + _key.value() | rpl::filter( + rpl::mappers::_1 == Key{ Tab::Apps } ) | rpl::start_with_next([=] { controller->load(); }, list->lifetime()); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index 036ad6972..efb42b2c6 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -18,10 +18,18 @@ namespace Data { class Thread; } // namespace Data +namespace Info { +class WrapWidget; +} // namespace Info + namespace Main { class Session; } // namespace Main +namespace Storage { +enum class SharedMediaType : signed char; +} // namespace Storage + namespace Ui { class BoxContent; class ScrollArea; @@ -97,10 +105,13 @@ public: class ObjectListController; private: + using MediaType = Storage::SharedMediaType; enum class Tab : uchar { Chats, Channels, Apps, + Media, + Downloads, }; enum class JumpResult : uchar { NotApplied, @@ -108,6 +119,14 @@ private: AppliedAndOut, }; + struct Key { + Tab tab = Tab::Chats; + MediaType mediaType = {}; + + friend inline auto operator<=>(Key, Key) = default; + friend inline bool operator==(Key, Key) = default; + }; + struct ObjectList { not_null*> wrap; rpl::variable count; @@ -119,6 +138,11 @@ private: rpl::event_stream> chosen; }; + struct MediaList { + Info::WrapWidget *wrap = nullptr; + rpl::variable count; + }; + void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; @@ -161,19 +185,22 @@ private: SearchEmptyIcon icon, rpl::producer text); - void switchTab(Tab tab); + void switchTab(Key key); void startShownAnimation(bool shown, Fn finish); - void startSlideAnimation(Tab was, Tab now); + void startSlideAnimation(Key was, Key now); + void ensureContent(Key key); void finishShow(); void handlePressForChatPreview(PeerId id, Fn callback); + void updateControlsGeometry(); const not_null _controller; const std::unique_ptr _tabsScroll; const not_null _tabs; Ui::Animations::Simple _tabsScrollAnimation; - rpl::variable _tab = Tab::Chats; + const std::vector _tabKeys; + rpl::variable _key; const std::unique_ptr _chatsScroll; const not_null _chatsContent; @@ -203,6 +230,8 @@ private: const std::unique_ptr _recentApps; const std::unique_ptr _popularApps; + base::flat_map _mediaLists; + Ui::Animations::Simple _shownAnimation; Fn _showFinished; bool _hidden = false; diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp index f2c116fdf..411dfafc8 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp @@ -108,9 +108,7 @@ bool InnerWidget::showInternal(not_null memento) { } object_ptr InnerWidget::setupList() { - auto result = object_ptr( - this, - _controller); + auto result = object_ptr(this, _controller); result->heightValue( ) | rpl::start_with_next( [this] { refreshHeight(); }, diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 7a68a81ab..92f7e1975 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -65,9 +65,6 @@ Widget::Widget( } bool Widget::showInternal(not_null memento) { - if (!controller()->validateMementoPeer(memento)) { - return false; - } if (auto downloadsMemento = dynamic_cast(memento.get())) { restoreState(downloadsMemento); return true; diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.cpp b/Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.cpp new file mode 100644 index 000000000..0e18c0d5a --- /dev/null +++ b/Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.cpp @@ -0,0 +1,212 @@ +/* +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_inner_widget.h" + +#include "info/global_media/info_global_media_provider.h" +#include "info/global_media/info_global_media_widget.h" +#include "info/media/info_media_list_widget.h" +#include "info/info_controller.h" +#include "ui/widgets/labels.h" +#include "ui/search_field_controller.h" +#include "lang/lang_keys.h" +#include "styles/style_info.h" + +namespace Info::GlobalMedia { + +class EmptyWidget : public Ui::RpWidget { +public: + EmptyWidget(QWidget *parent); + + void setFullHeight(rpl::producer fullHeightValue); + void setSearchQuery(const QString &query); + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + +private: + object_ptr _text; + int _height = 0; + +}; + +EmptyWidget::EmptyWidget(QWidget *parent) +: RpWidget(parent) +, _text(this, st::infoEmptyLabel) { +} + +void EmptyWidget::setFullHeight(rpl::producer fullHeightValue) { + std::move( + fullHeightValue + ) | rpl::start_with_next([this](int fullHeight) { + // Make icon center be on 1/3 height. + auto iconCenter = fullHeight / 3; + auto iconHeight = st::infoEmptyFile.height(); + auto iconTop = iconCenter - iconHeight / 2; + _height = iconTop + st::infoEmptyIconTop; + resizeToWidth(width()); + }, lifetime()); +} + +void EmptyWidget::setSearchQuery(const QString &query) { + _text->setText(query.isEmpty() + ? tr::lng_media_file_empty(tr::now) + : tr::lng_media_file_empty_search(tr::now)); + resizeToWidth(width()); +} + +int EmptyWidget::resizeGetHeight(int newWidth) { + auto labelTop = _height - st::infoEmptyLabelTop; + auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip; + _text->resizeToNaturalWidth(labelWidth); + + auto labelLeft = (newWidth - _text->width()) / 2; + _text->moveToLeft(labelLeft, labelTop, newWidth); + + update(); + return _height; +} + +void EmptyWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto iconLeft = (width() - st::infoEmptyFile.width()) / 2; + const auto iconTop = height() - st::infoEmptyIconTop; + st::infoEmptyFile.paint(p, iconLeft, iconTop, width()); +} + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller) +: RpWidget(parent) +, _controller(controller) +, _empty(this) { + _empty->heightValue( + ) | rpl::start_with_next( + [this] { refreshHeight(); }, + _empty->lifetime()); + _list = setupList(); +} + +object_ptr InnerWidget::setupList() { + auto result = object_ptr(this, _controller); + + // Setup list widget connections + result->heightValue( + ) | rpl::start_with_next([this] { + refreshHeight(); + }, result->lifetime()); + + using namespace rpl::mappers; + result->scrollToRequests( + ) | rpl::map([widget = result.data()](int to) { + return Ui::ScrollToRequest{ + widget->y() + to, + -1 + }; + }) | rpl::start_to_stream( + _scrollToRequests, + result->lifetime()); + + _controller->searchQueryValue( + ) | rpl::start_with_next([this](const QString &query) { + _empty->setSearchQuery(query); + }, result->lifetime()); + + return result; +} + +Storage::SharedMediaType InnerWidget::type() const { + return _controller->section().mediaType(); +} + +void InnerWidget::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +bool InnerWidget::showInternal(not_null memento) { + if (memento->section().type() == Section::Type::GlobalMedia + && memento->section().mediaType() == type()) { + restoreState(memento); + return true; + } + return false; +} + +void InnerWidget::saveState(not_null memento) { + _list->saveState(&memento->media()); +} + +void InnerWidget::restoreState(not_null memento) { + _list->restoreState(&memento->media()); +} + +rpl::producer InnerWidget::selectedListValue() const { + return _selectedLists.events_starting_with( + _list->selectedListValue() + ) | rpl::flatten_latest(); +} + +void InnerWidget::selectionAction(SelectionAction action) { + _list->selectionAction(action); +} + +InnerWidget::~InnerWidget() = default; + +int InnerWidget::resizeGetHeight(int newWidth) { + _inResize = true; + auto guard = gsl::finally([this] { _inResize = false; }); + + _list->resizeToWidth(newWidth); + _empty->resizeToWidth(newWidth); + return recountHeight(); +} + +void InnerWidget::refreshHeight() { + if (_inResize) { + return; + } + resize(width(), recountHeight()); +} + +int InnerWidget::recountHeight() { + auto top = 0; + auto listHeight = 0; + if (_list) { + _list->moveToLeft(0, top); + listHeight = _list->heightNoMargins(); + top += listHeight; + } + if (listHeight > 0) { + _empty->hide(); + } else { + _empty->show(); + _empty->moveToLeft(0, top); + top += _empty->heightNoMargins(); + } + return top; +} + +void InnerWidget::setScrollHeightValue(rpl::producer value) { + using namespace rpl::mappers; + _empty->setFullHeight(rpl::combine( + std::move(value), + _listTops.events_starting_with( + _list->topValue() + ) | rpl::flatten_latest(), + _1 - _2)); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +} // namespace Info::GlobalMedia \ No newline at end of file diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.h b/Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.h new file mode 100644 index 000000000..a827b1493 --- /dev/null +++ b/Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.h @@ -0,0 +1,85 @@ +/* +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 +*/ +#pragma once + +#include "ui/rp_widget.h" +#include "ui/widgets/scroll_area.h" +#include "base/unique_qptr.h" + +namespace Ui { +class VerticalLayout; +class SearchFieldController; +} // namespace Ui + +namespace Storage { +enum class SharedMediaType : signed char; +} // namespace Storage + +namespace Info { + +class Controller; +struct SelectedItems; +enum class SelectionAction; + +namespace Media { +class ListWidget; +} // namespace Media + +namespace GlobalMedia { + +class Memento; +class EmptyWidget; + +class InnerWidget final : public Ui::RpWidget { +public: + InnerWidget( + QWidget *parent, + not_null controller); + + bool showInternal(not_null memento); + + void saveState(not_null memento); + void restoreState(not_null memento); + + void setScrollHeightValue(rpl::producer value); + + rpl::producer scrollToRequests() const; + rpl::producer selectedListValue() const; + void selectionAction(SelectionAction action); + + ~InnerWidget(); + +protected: + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + int recountHeight(); + void refreshHeight(); + + Storage::SharedMediaType type() const; + + object_ptr setupList(); + + const not_null _controller; + + object_ptr _list = { nullptr }; + object_ptr _empty; + + bool _inResize = false; + + rpl::event_stream _scrollToRequests; + rpl::event_stream> _selectedLists; + rpl::event_stream> _listTops; + +}; + +} // namespace GlobalMedia +} // namespace Info \ No newline at end of file diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp b/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp new file mode 100644 index 000000000..3fe7b37ed --- /dev/null +++ b/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp @@ -0,0 +1,632 @@ +/* +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); + } +} + +void Provider::applyListQuery(const QString &query) { + if (_totalListQuery == query) { + return; + } + _totalListQuery = query; + _totalList.clear(); + _totalOffsetPosition = Data::MessagePosition(); + _totalOffsetRate = 0; + _totalFullCount = 0; + _totalLoaded = false; +} + +rpl::producer Provider::source( + Type type, + Data::MessagePosition aroundId, + QString query, + int limitBefore, + int limitAfter) { + Expects(_type == type); + + applyListQuery(query); + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto session = &_controller->session(); + + struct State { + 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); + + state->pushAndLoadMore = [=] { + auto result = fillRequest(aroundId, limitBefore, limitAfter); + consumer.put_next(std::move(result.slice)); + if (!_totalLoaded && result.notEnough) { + state->requestId = requestMore(state->pushAndLoadMore); + } + }; + state->pushAndLoadMore(); + + return lifetime; + }; +} + +mtpRequestId Provider::requestMore(Fn loaded) { + const auto done = [=](const Api::GlobalMediaResult &result) { + if (result.messageIds.empty()) { + _totalLoaded = true; + _totalFullCount = _totalList.size(); + } else { + _totalList.reserve(_totalList.size() + result.messageIds.size()); + _totalFullCount = result.fullCount; + for (const auto &position : result.messageIds) { + _seenIds.emplace(position.fullId); + _totalOffsetPosition = position; + _totalList.push_back(position); + } + } + if (!result.offsetRate) { + _totalLoaded = true; + } else { + _totalOffsetRate = result.offsetRate; + } + loaded(); + }; + return _controller->session().api().requestGlobalMedia( + _type, + _totalListQuery, + _totalOffsetRate, + _totalOffsetPosition, + done); +} + +Provider::FillResult Provider::fillRequest( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto i = ranges::lower_bound( + _totalList, + aroundId, + std::greater<>()); + const auto hasAfter = int(i - begin(_totalList)); + const auto hasBefore = int(end(_totalList) - i); + const auto takeAfter = std::min(limitAfter, hasAfter); + const auto takeBefore = std::min(limitBefore, hasBefore); + auto list = std::vector{ + i - takeAfter, + i + takeBefore, + }; + return FillResult{ + .slice = GlobalMediaSlice( + GlobalMediaKey{ aroundId }, + std::move(list), + ((!_totalList.empty() || _totalLoaded) + ? _totalFullCount + : 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; + } + } +} + +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