diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 02567967b1..5694f2d091 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -712,13 +712,25 @@ PRIVATE info/common_groups/info_common_groups_inner_widget.h info/common_groups/info_common_groups_widget.cpp info/common_groups/info_common_groups_widget.h + info/downloads/info_downloads_inner_widget.cpp + info/downloads/info_downloads_inner_widget.h + info/downloads/info_downloads_provider.cpp + info/downloads/info_downloads_provider.h + info/downloads/info_downloads_widget.cpp + info/downloads/info_downloads_widget.h info/media/info_media_buttons.h + info/media/info_media_common.cpp + info/media/info_media_common.h info/media/info_media_empty_widget.cpp info/media/info_media_empty_widget.h info/media/info_media_inner_widget.cpp info/media/info_media_inner_widget.h + info/media/info_media_list_section.cpp + info/media/info_media_list_section.h info/media/info_media_list_widget.cpp info/media/info_media_list_widget.h + info/media/info_media_provider.cpp + info/media/info_media_provider.h info/media/info_media_widget.cpp info/media/info_media_widget.h info/members/info_members_widget.cpp diff --git a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h index f423bac357..d48542d190 100644 --- a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h @@ -24,31 +24,31 @@ public: , _skippedAfter(skippedAfter) { } - std::optional fullCount() const { + [[nodiscard]] std::optional fullCount() const { return _fullCount; } - std::optional skippedBefore() const { + [[nodiscard]] std::optional skippedBefore() const { return _skippedBefore; } - std::optional skippedAfter() const { + [[nodiscard]] std::optional skippedAfter() const { return _skippedAfter; } - std::optional indexOf(Id id) const { + [[nodiscard]] std::optional indexOf(Id id) const { const auto it = ranges::find(_ids, id); if (it != _ids.end()) { return (it - _ids.begin()); } return std::nullopt; } - int size() const { + [[nodiscard]] int size() const { return _ids.size(); } - Id operator[](int index) const { + [[nodiscard]] Id operator[](int index) const { Expects(index >= 0 && index < size()); return *(_ids.begin() + index); } - std::optional distance(Id a, Id b) const { + [[nodiscard]] std::optional distance(Id a, Id b) const { if (const auto i = indexOf(a)) { if (const auto j = indexOf(b)) { return *j - *i; @@ -56,7 +56,8 @@ public: } return std::nullopt; } - std::optional nearest(Id id) const { + [[nodiscard]] std::optional nearest(Id id) const { + static_assert(std::is_same_v>); if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) { return *it; } else if (_ids.empty()) { diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 675e125728..448bf10ad6 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -181,6 +181,60 @@ struct FullMsgId { Q_DECLARE_METATYPE(FullMsgId); +struct GlobalMsgId { + FullMsgId itemId; + uint64 sessionUniqueId = 0; + + constexpr explicit operator bool() const noexcept { + return itemId && sessionUniqueId; + } + constexpr bool operator!() const noexcept { + return !itemId || !sessionUniqueId; + } +}; + +[[nodiscard]] inline constexpr bool operator<( + const GlobalMsgId &a, + const GlobalMsgId &b) noexcept { + if (a.itemId < b.itemId) { + return true; + } else if (a.itemId > b.itemId) { + return false; + } + return a.sessionUniqueId < b.sessionUniqueId; +} + +[[nodiscard]] inline constexpr bool operator>( + const GlobalMsgId &a, + const GlobalMsgId &b) noexcept { + return b < a; +} + +[[nodiscard]] inline constexpr bool operator<=( + const GlobalMsgId &a, + const GlobalMsgId &b) noexcept { + return !(b < a); +} + +[[nodiscard]] inline constexpr bool operator>=( + const GlobalMsgId &a, + const GlobalMsgId &b) noexcept { + return !(a < b); +} + +[[nodiscard]] inline constexpr bool operator==( + const GlobalMsgId &a, + const GlobalMsgId &b) noexcept { + return (a.itemId == b.itemId) + && (a.sessionUniqueId == b.sessionUniqueId); +} + +[[nodiscard]] inline constexpr bool operator!=( + const GlobalMsgId &a, + const GlobalMsgId &b) noexcept { + return !(a == b); +} + namespace std { template <> diff --git a/Telegram/SourceFiles/data/data_sparse_ids.cpp b/Telegram/SourceFiles/data/data_sparse_ids.cpp index 84fe3a8310..0605db8c1b 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.cpp +++ b/Telegram/SourceFiles/data/data_sparse_ids.cpp @@ -28,22 +28,22 @@ SparseIdsMergedSlice::SparseIdsMergedSlice( SparseIdsMergedSlice::SparseIdsMergedSlice( Key key, - SparseUnsortedIdsSlice scheduled) + SparseUnsortedIdsSlice unsorted) : _key(key) -, _scheduled(std::move(scheduled)) { +, _unsorted(std::move(unsorted)) { } std::optional SparseIdsMergedSlice::fullCount() const { - return _scheduled - ? _scheduled->fullCount() + return _unsorted + ? _unsorted->fullCount() : Add( _part.fullCount(), _migrated ? _migrated->fullCount() : 0); } std::optional SparseIdsMergedSlice::skippedBefore() const { - return _scheduled - ? _scheduled->skippedBefore() + return _unsorted + ? _unsorted->skippedBefore() : Add( isolatedInMigrated() ? 0 : _part.skippedBefore(), _migrated @@ -55,8 +55,8 @@ std::optional SparseIdsMergedSlice::skippedBefore() const { } std::optional SparseIdsMergedSlice::skippedAfter() const { - return _scheduled - ? _scheduled->skippedAfter() + return _unsorted + ? _unsorted->skippedAfter() : Add( isolatedInMigrated() ? _part.fullCount() : _part.skippedAfter(), isolatedInPart() ? 0 : _migrated->skippedAfter() @@ -65,8 +65,8 @@ std::optional SparseIdsMergedSlice::skippedAfter() const { std::optional SparseIdsMergedSlice::indexOf( FullMsgId fullId) const { - return _scheduled - ? _scheduled->indexOf(fullId.msg) + return _unsorted + ? _unsorted->indexOf(fullId.msg) : isFromPart(fullId) ? (_part.indexOf(fullId.msg) | func::add(migratedSize())) : isolatedInPart() @@ -77,8 +77,8 @@ std::optional SparseIdsMergedSlice::indexOf( } int SparseIdsMergedSlice::size() const { - return _scheduled - ? _scheduled->size() + return _unsorted + ? _unsorted->size() : (isolatedInPart() ? 0 : migratedSize()) + (isolatedInMigrated() ? 0 : _part.size()); } @@ -86,8 +86,8 @@ int SparseIdsMergedSlice::size() const { FullMsgId SparseIdsMergedSlice::operator[](int index) const { Expects(index >= 0 && index < size()); - if (_scheduled) { - return ComputeId(_key.peerId, (*_scheduled)[index]); + if (_unsorted) { + return ComputeId(_key.peerId, (*_unsorted)[index]); } if (const auto size = migratedSize()) { @@ -112,10 +112,13 @@ std::optional SparseIdsMergedSlice::distance( auto SparseIdsMergedSlice::nearest( UniversalMsgId id) const -> std::optional { - if (_scheduled) { - if (const auto nearestId = _scheduled->nearest(id)) { - return ComputeId(_key.peerId, *nearestId); + if (_unsorted) { + if (_unsorted->indexOf(id).has_value()) { + return ComputeId(_key.peerId, id); + } else if (const auto count = _unsorted->size()) { + return ComputeId(_key.peerId, (*_unsorted)[count / 2]); } + return std::nullopt; } const auto convertFromPartNearest = [&](MsgId result) { return ComputeId(_key.peerId, result); diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index e949b80c14..a3f03e98da 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -62,7 +62,7 @@ public: std::optional migrated); SparseIdsMergedSlice( Key key, - SparseUnsortedIdsSlice scheduled); + SparseUnsortedIdsSlice unsorted); std::optional fullCount() const; std::optional skippedBefore() const; @@ -139,7 +139,7 @@ private: Key _key; SparseIdsSlice _part; std::optional _migrated; - std::optional _scheduled; + std::optional _unsorted; }; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 8fb09b7e0e..ad2276e3c3 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -27,6 +27,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/file_upload.h" #include "storage/storage_facade.h" #include "storage/storage_shared_media.h" +#include "main/main_account.h" +#include "main/main_domain.h" #include "main/main_session.h" #include "apiwrap.h" #include "media/audio/media_audio.h" @@ -1017,6 +1019,10 @@ FullMsgId HistoryItem::fullId() const { return FullMsgId(_history->peer->id, id); } +GlobalMsgId HistoryItem::globalId() const { + return { fullId(), _history->session().uniqueId() }; +} + Data::MessagePosition HistoryItem::position() const { return { .fullId = fullId(), .date = date() }; } @@ -1301,6 +1307,20 @@ HistoryItem::~HistoryItem() { applyTTL(0); } +HistoryItem *MessageByGlobalId(GlobalMsgId globalId) { + if (!globalId.sessionUniqueId || !globalId.itemId) { + return nullptr; + } + for (const auto &[index, account] : Core::App().domain().accounts()) { + if (const auto session = account->maybeSession()) { + if (session->uniqueId() == globalId.sessionUniqueId) { + return session->data().message(globalId.itemId); + } + } + } + return nullptr; +} + QDateTime ItemDateTime(not_null item) { return base::unixtime::parse(item->date()); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b8c5e44830..8dd89778d8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -380,6 +380,7 @@ public: [[nodiscard]] bool hasDirectLink() const; [[nodiscard]] FullMsgId fullId() const; + [[nodiscard]] GlobalMsgId globalId() const; [[nodiscard]] Data::MessagePosition position() const; [[nodiscard]] TimeId date() const; @@ -494,9 +495,14 @@ private: }; -QDateTime ItemDateTime(not_null item); -QString ItemDateText(not_null item, bool isUntilOnline); -bool IsItemScheduledUntilOnline(not_null item); +[[nodiscard]] HistoryItem *MessageByGlobalId(GlobalMsgId globalId); + +[[nodiscard]] QDateTime ItemDateTime(not_null item); +[[nodiscard]] QString ItemDateText( + not_null item, + bool isUntilOnline); +[[nodiscard]] bool IsItemScheduledUntilOnline( + not_null item); ClickHandlerPtr goToMessageClickHandler( not_null peer, diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp index 37c0cd4c39..6c3ab80d54 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp @@ -282,4 +282,3 @@ void InnerWidget::peerListSetDescription( } // namespace CommonGroups } // namespace Info - diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp new file mode 100644 index 0000000000..611a3a3733 --- /dev/null +++ b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp @@ -0,0 +1,195 @@ +/* +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/downloads/info_downloads_inner_widget.h" + +#include "info/downloads/info_downloads_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::Downloads { + +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; + +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; +} + +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(); +} + +void InnerWidget::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +bool InnerWidget::showInternal(not_null memento) { + if (memento->section().type() == Section::Type::Downloads) { + restoreState(memento); + return true; + } + return false; +} + +object_ptr InnerWidget::setupList() { + auto result = object_ptr( + this, + _controller); + 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()); + _selectedLists.fire(result->selectedListValue()); + _listTops.fire(result->topValue()); + _controller->mediaSourceQueryValue( + ) | rpl::start_with_next([this](const QString &query) { + _empty->setSearchQuery(query); + }, result->lifetime()); + return result; +} + +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::cancelSelection() { + _list->cancelSelection(); +} + +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::Downloads diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h new file mode 100644 index 0000000000..90d3a2b692 --- /dev/null +++ b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h @@ -0,0 +1,79 @@ +/* +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 SettingsSlider; +class VerticalLayout; +class SearchFieldController; +} // namespace Ui + +namespace Info { + +class Controller; +struct SelectedItems; + +namespace Media { +class ListWidget; +} // namespace Media + +namespace Downloads { + +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 cancelSelection(); + + ~InnerWidget(); + +protected: + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + int recountHeight(); + void refreshHeight(); + + 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 Downloads +} // namespace Info diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp new file mode 100644 index 0000000000..1cd2b9e10b --- /dev/null +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp @@ -0,0 +1,123 @@ +/* +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/downloads/info_downloads_provider.h" + +#include "info/media/info_media_widget.h" +#include "info/media/info_media_list_section.h" +#include "info/info_controller.h" +#include "storage/storage_shared_media.h" +#include "layout/layout_selection.h" + +namespace Info::Downloads { +namespace { + +using namespace Media; + +} // namespace + +Provider::Provider(not_null controller) +: _controller(controller) { + //_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::File; +} + +bool Provider::hasSelectRestriction() { + return false; +} + +rpl::producer Provider::hasSelectRestrictionChanges() { + return rpl::never(); +} + +bool Provider::isPossiblyMyItem(not_null item) { + return true; +} + +std::optional Provider::fullCount() { + return 0; +} + +void Provider::restart() { + +} + +void Provider::checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) { + +} + +void Provider::refreshViewer() { + +} + +rpl::producer<> Provider::refreshed() { + return _refreshed.events(); +} + +std::vector Provider::fillSections( + not_null delegate) { + return {}; +} + +rpl::producer> Provider::layoutRemoved() { + return _layoutRemoved.events(); +} + +BaseLayout *Provider::lookupLayout(const HistoryItem *item) { + return nullptr; +} + +bool Provider::isMyItem(not_null item) { + return false; +} + +bool Provider::isAfter( + not_null a, + not_null b) { + return a < b; +} + +void Provider::applyDragSelection( + ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) { + +} + +void Provider::saveState( + not_null memento, + ListScrollTopState scrollState) { + +} + +void Provider::restoreState( + not_null memento, + Fn restoreScrollState) { + +} + +} // namespace Info::Downloads diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h new file mode 100644 index 0000000000..d30ae486b0 --- /dev/null +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h @@ -0,0 +1,79 @@ +/* +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 "info/media/info_media_common.h" + +namespace Info { +class AbstractController; +} // namespace Info + +namespace Info::Downloads { + +class Provider final : public Media::ListProvider { +public: + explicit Provider(not_null controller); + + Media::Type type() override; + bool hasSelectRestriction() override; + rpl::producer hasSelectRestrictionChanges() override; + bool isPossiblyMyItem(not_null item) override; + + std::optional fullCount() override; + + void restart() override; + void checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) override; + void refreshViewer() override; + rpl::producer<> refreshed() override; + + std::vector fillSections( + not_null delegate) override; + rpl::producer> layoutRemoved() override; + Media::BaseLayout *lookupLayout(const HistoryItem *item) override; + bool isMyItem(not_null item) override; + bool isAfter( + not_null a, + not_null b) override; + + void applyDragSelection( + Media::ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) override; + + void saveState( + not_null memento, + Media::ListScrollTopState scrollState) override; + void restoreState( + not_null memento, + Fn restoreScrollState) override; + +private: + struct Element { + not_null item; + uint64 started = 0; + }; + const not_null _controller; + + std::vector _elements; + + std::unordered_map, Media::CachedItem> _layouts; + rpl::event_stream> _layoutRemoved; + rpl::event_stream<> _refreshed; + + rpl::lifetime _lifetime; + +}; + +} // namespace Info::Downloads diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp new file mode 100644 index 0000000000..4bedfa2660 --- /dev/null +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -0,0 +1,99 @@ +/* +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/downloads/info_downloads_widget.h" + +#include "info/downloads/info_downloads_inner_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "ui/search_field_controller.h" +#include "ui/widgets/scroll_area.h" +#include "data/data_download_manager.h" +#include "data/data_user.h" +#include "core/application.h" +#include "styles/style_info.h" + +namespace Info::Downloads { + +Memento::Memento(not_null controller) +: ContentMemento(Tag{}) +, _media(controller) { +} + +Memento::Memento(not_null self) +: ContentMemento(Downloads::Tag{}) +, _media(self, 0, Media::Type::File) { +} + +Memento::~Memento() = default; + +Section Memento::section() const { + return Section(Section::Type::Downloads); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +Widget::Widget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) { + _inner = setInnerWidget(object_ptr( + this, + controller)); +} + +bool Widget::showInternal(not_null memento) { + if (!controller()->validateMementoPeer(memento)) { + return false; + } + if (auto downloadsMemento = dynamic_cast(memento.get())) { + restoreState(downloadsMemento); + return true; + } + return false; +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared(controller()); + saveState(result.get()); + return result; +} + +void Widget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); + _inner->saveState(memento); +} + +void Widget::restoreState(not_null memento) { + _inner->restoreState(memento); + scrollTopRestore(memento->scrollTop()); +} + +std::shared_ptr Make(not_null self) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(self))); +} + +} // namespace Info::Downloads + diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.h b/Telegram/SourceFiles/info/downloads/info_downloads_widget.h new file mode 100644 index 0000000000..ce848cf4ef --- /dev/null +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.h @@ -0,0 +1,69 @@ +/* +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 "info/info_content_widget.h" +#include "info/media/info_media_widget.h" + +namespace Ui { +class SearchFieldController; +} // namespace Ui + +namespace Info::Downloads { + +class InnerWidget; + +class Memento final : public ContentMemento { +public: + Memento(not_null controller); + Memento(not_null self); + ~Memento(); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + [[nodiscard]] Media::Memento &media() { + return _media; + } + [[nodiscard]] const Media::Memento &media() const { + return _media; + } + +private: + Media::Memento _media; + +}; + +class Widget final : public ContentWidget { +public: + Widget(QWidget *parent, not_null controller); + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + InnerWidget *_inner = nullptr; + +}; + +[[nodiscard]] std::shared_ptr Make(not_null self); + +} // namespace Info::Downloads diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index c100da5009..ac2536ffe8 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -262,8 +262,10 @@ Key ContentMemento::key() const { return Key(peer); } else if (const auto poll = this->poll()) { return Key(poll, pollContextId()); + } else if (const auto self = settingsSelf()) { + return Settings::Tag{ self }; } else { - return Settings::Tag{ settingsSelf() }; + return Downloads::Tag(); } } @@ -271,4 +273,7 @@ ContentMemento::ContentMemento(Settings::Tag settings) : _settingsSelf(settings.self.get()) { } +ContentMemento::ContentMemento(Downloads::Tag downloads) { +} + } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index e70ff3db32..548df85fb8 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -28,6 +28,10 @@ namespace Settings { struct Tag; } // namespace Settings +namespace Downloads { +struct Tag; +} // namespace Downloads + class ContentMemento; class Controller; @@ -120,6 +124,7 @@ public: , _migratedPeerId(migratedPeerId) { } explicit ContentMemento(Settings::Tag settings); + explicit ContentMemento(Downloads::Tag downloads); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollContextId(contextId) { diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 3a3f89ce8e..582b0ecc27 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -14,13 +14,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_content_widget.h" #include "info/info_memento.h" #include "info/media/info_media_widget.h" +#include "core/application.h" #include "data/data_changes.h" #include "data/data_peer.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_session.h" #include "data/data_media_types.h" +#include "data/data_download_manager.h" #include "history/history_item.h" +#include "history/history.h" #include "main/main_session.h" #include "window/window_session_controller.h" @@ -32,6 +35,9 @@ Key::Key(not_null peer) : _value(peer) { Key::Key(Settings::Tag settings) : _value(settings) { } +Key::Key(Downloads::Tag downloads) : _value(downloads) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -50,6 +56,10 @@ UserData *Key::settingsSelf() const { return nullptr; } +bool Key::isDownloads() const { + return v::is(_value); +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; @@ -98,6 +108,28 @@ rpl::producer AbstractController::mediaSourceQueryValue() const { return rpl::single(QString()); } +rpl::producer AbstractController::downloadsSource() const { + const auto manager = &Core::App().downloadManager(); + return rpl::single( + rpl::empty_value() + ) | rpl::then( + manager->loadingListChanges() + ) | rpl::map([=] { + auto result = DownloadsSlice(); + for (const auto &id : manager->loadingList()) { + result.entries.push_back(DownloadsEntry{ + .item = id.object.item, + .started = id.started, + }); + } + ranges::sort( + result.entries, + ranges::less(), + &DownloadsEntry::started); + return result; + }); +} + AbstractController::AbstractController( not_null parent) : SessionNavigation(&parent->session()) @@ -204,17 +236,19 @@ void Controller::setSection(not_null memento) { void Controller::updateSearchControllers( not_null memento) { using Type = Section::Type; - auto type = _section.type(); - auto isMedia = (type == Type::Media); - auto mediaType = isMedia + const auto type = _section.type(); + const auto isMedia = (type == Type::Media); + const auto mediaType = isMedia ? _section.mediaType() : Section::MediaType::kCount; - auto hasMediaSearch = isMedia + const auto hasMediaSearch = isMedia && SharedMediaAllowSearch(mediaType); - auto hasCommonGroupsSearch + const auto hasCommonGroupsSearch = (type == Type::CommonGroups); - auto hasMembersSearch = (type == Type::Members || type == Type::Profile); - auto searchQuery = memento->searchFieldQuery(); + const auto hasMembersSearch = (type == Type::Members) + || (type == Type::Profile); + const auto searchQuery = memento->searchFieldQuery(); + const auto isDownloads = (type == Type::Downloads); if (isMedia) { _searchController = std::make_unique(&session()); @@ -289,7 +323,9 @@ rpl::producer Controller::searchEnabledByContent() const { } rpl::producer Controller::mediaSourceQueryValue() const { - return _searchController->currentQueryValue(); + return _searchController + ? _searchController->currentQueryValue() + : rpl::never(); AssertIsDebug() // #TODO downloads } rpl::producer Controller::mediaSource( diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index c16a99d434..82978e5d1c 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -28,14 +28,23 @@ struct Tag { } // namespace Settings +namespace Downloads { + +struct Tag { +}; + +} // namespace Downloads + class Key { public: Key(not_null peer); Key(Settings::Tag settings); + Key(Downloads::Tag downloads); Key(not_null poll, FullMsgId contextId); PeerData *peer() const; UserData *settingsSelf() const; + bool isDownloads() const; PollData *poll() const; FullMsgId pollContextId() const; @@ -47,6 +56,7 @@ private: std::variant< not_null, Settings::Tag, + Downloads::Tag, PollKey> _value; }; @@ -64,6 +74,7 @@ public: CommonGroups, Members, Settings, + Downloads, PollResults, }; using SettingsType = ::Settings::Type; @@ -102,6 +113,14 @@ private: }; +struct DownloadsEntry { + not_null item; + int64 started = 0; // unixtime * 1000. +}; +struct DownloadsSlice { + std::vector entries; +}; + class AbstractController : public Window::SessionNavigation { public: AbstractController(not_null parent); @@ -115,6 +134,9 @@ public: UserData *settingsSelf() const { return key().settingsSelf(); } + bool isDownloads() const { + return key().isDownloads(); + } PollData *poll() const; FullMsgId pollContextId() const { return key().pollContextId(); @@ -128,6 +150,8 @@ public: int limitAfter) const; virtual rpl::producer mediaSourceQueryValue() const; + [[nodiscard]] rpl::producer downloadsSource() const; + void showSection( std::shared_ptr memento, const Window::SectionShow ¶ms = Window::SectionShow()) override; diff --git a/Telegram/SourceFiles/info/info_memento.h b/Telegram/SourceFiles/info/info_memento.h index af23e30c09..b215fc6544 100644 --- a/Telegram/SourceFiles/info/info_memento.h +++ b/Telegram/SourceFiles/info/info_memento.h @@ -27,6 +27,10 @@ namespace Settings { struct Tag; } // namespace Settings +namespace Downloads { +struct Tag; +} // namespace Downloads + class ContentMemento; class WrapWidget; diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index acc150996b..7e040ba82e 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -545,9 +545,13 @@ MessageIdsList TopBar::collectItems() const { return ranges::views::all( _selectedItems.list ) | ranges::views::transform([](auto &&item) { - return item.msgId; - }) | ranges::views::filter([&](FullMsgId msgId) { - return _navigation->session().data().message(msgId) != nullptr; + return item.globalId; + }) | ranges::views::filter([&](const GlobalMsgId &globalId) { + const auto session = &_navigation->session(); + return (globalId.sessionUniqueId == session->uniqueId()) + && (session->data().message(globalId.itemId) != nullptr); + }) | ranges::views::transform([](const GlobalMsgId &globalId) { + return globalId.itemId; }) | ranges::to_vector; } @@ -568,6 +572,7 @@ void TopBar::performForward() { } void TopBar::performDelete() { + // #TODO downloads auto items = collectItems(); if (items.empty()) { _cancelSelectionClicks.fire({}); @@ -667,6 +672,10 @@ rpl::producer TitleValue( return key.poll()->quiz() ? tr::lng_polls_quiz_results_title() : tr::lng_polls_poll_results_title(); + + case Section::Type::Downloads: + return rpl::single(u"Downloads"_q); + } Unexpected("Bad section type in Info::TitleValue()"); } diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 5e83946cd2..1f4749edd9 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -871,13 +871,19 @@ void WrapWidget::showNewContent( _historyStack.clear(); } - _controller = std::move(newController); - if (newContent) { - setupTop(); - showContent(std::move(newContent)); - } else { - showNewContent(memento); + { + // Let old controller outlive old content widget. + const auto oldController = std::exchange( + _controller, + std::move(newController)); + if (newContent) { + setupTop(); + showContent(std::move(newContent)); + } else { + showNewContent(memento); + } } + if (animationParams) { if (Ui::InFocusChain(this)) { setFocus(); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 67d366c694..63ab1e9488 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -51,10 +51,10 @@ enum class Wrap { }; struct SelectedItem { - explicit SelectedItem(FullMsgId msgId) : msgId(msgId) { + explicit SelectedItem(GlobalMsgId globalId) : globalId(globalId) { } - FullMsgId msgId; + GlobalMsgId globalId; bool canDelete = false; bool canForward = false; }; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 1df72876b6..4d69e0775e 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -23,8 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "styles/style_info.h" -namespace Info { -namespace Media { +namespace Info::Media { using Type = Storage::SharedMediaType; @@ -118,5 +117,4 @@ inline auto AddCommonGroupsButton( return result; }; -} // namespace Media -} // namespace Info +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_common.cpp b/Telegram/SourceFiles/info/media/info_media_common.cpp new file mode 100644 index 0000000000..bf7849a9d5 --- /dev/null +++ b/Telegram/SourceFiles/info/media/info_media_common.cpp @@ -0,0 +1,54 @@ +/* +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_common.h" + +#include "history/history_item.h" + +namespace Info::Media { + +UniversalMsgId GetUniversalId(FullMsgId itemId) { + return peerIsChannel(itemId.peer) + ? UniversalMsgId(itemId.msg) + : UniversalMsgId(itemId.msg - ServerMaxMsgId); +} + +UniversalMsgId GetUniversalId(not_null item) { + return GetUniversalId(item->fullId()); +} + +UniversalMsgId GetUniversalId(not_null layout) { + return GetUniversalId(layout->getItem()->fullId()); +} + +bool ChangeItemSelection( + ListSelectedMap &selected, + not_null item, + TextSelection selection) { + const auto changeExisting = [&](auto it) { + if (it == selected.cend()) { + return false; + } else if (it->second.text != selection) { + it->second.text = selection; + return true; + } + return false; + }; + if (selected.size() < MaxSelectedItems) { + const auto [i, ok] = selected.try_emplace(item, selection); + if (ok) { + // #TODO downloads + i->second.canDelete = item->canDelete(); + i->second.canForward = item->allowsForward(); + return true; + } + return changeExisting(i); + } + return changeExisting(selected.find(item)); +} + +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h new file mode 100644 index 0000000000..a1edf33060 --- /dev/null +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -0,0 +1,139 @@ +/* +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 "overview/overview_layout.h" + +namespace Storage { +enum class SharedMediaType : signed char; +} // namespace Storage + +namespace Info::Media { + +using Type = Storage::SharedMediaType; +using BaseLayout = Overview::Layout::ItemBase; + +class Memento; +class ListSection; + +inline constexpr auto kPreloadIfLessThanScreens = 2; + +struct ListItemSelectionData { + explicit ListItemSelectionData(TextSelection text) : text(text) { + } + + TextSelection text; + bool canDelete = false; + bool canForward = false; +}; + +using ListSelectedMap = base::flat_map< + not_null, + ListItemSelectionData, + std::less<>>; + +enum class ListDragSelectAction { + None, + Selecting, + Deselecting, +}; + +struct ListContext { + Overview::Layout::PaintContext layoutContext; + not_null selected; + not_null dragSelected; + ListDragSelectAction dragSelectAction = ListDragSelectAction::None; +}; + +struct ListScrollTopState { + HistoryItem *item = nullptr; + int shift = 0; +}; + +struct ListFoundItem { + not_null layout; + QRect geometry; + bool exact = false; +}; + +struct CachedItem { + CachedItem(std::unique_ptr item) : item(std::move(item)) { + }; + CachedItem(CachedItem &&other) = default; + CachedItem &operator=(CachedItem &&other) = default; + ~CachedItem() = default; + + std::unique_ptr item; + bool stale = false; +}; + +using UniversalMsgId = MsgId; + +[[nodiscard]] UniversalMsgId GetUniversalId(FullMsgId itemId); +[[nodiscard]] UniversalMsgId GetUniversalId( + not_null item); +[[nodiscard]] UniversalMsgId GetUniversalId( + not_null layout); + +bool ChangeItemSelection( + ListSelectedMap &selected, + not_null item, + TextSelection selection); + +class ListProvider { +public: + [[nodiscard]] virtual Type type() = 0; + [[nodiscard]] virtual bool hasSelectRestriction() = 0; + [[nodiscard]] virtual auto hasSelectRestrictionChanges() + ->rpl::producer = 0; + [[nodiscard]] virtual bool isPossiblyMyItem( + not_null item) = 0; + + [[nodiscard]] virtual std::optional fullCount() = 0; + + virtual void restart() = 0; + virtual void checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) = 0; + virtual void refreshViewer() = 0; + [[nodiscard]] virtual rpl::producer<> refreshed() = 0; + + [[nodiscard]] virtual std::vector fillSections( + not_null delegate) = 0; + [[nodiscard]] virtual auto layoutRemoved() + -> rpl::producer> = 0; + [[nodiscard]] virtual BaseLayout *lookupLayout( + const HistoryItem *item) = 0; + [[nodiscard]] virtual bool isMyItem( + not_null item) = 0; + [[nodiscard]] virtual bool isAfter( + not_null a, + not_null b) = 0; + + virtual void applyDragSelection( + ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) = 0; + + virtual void saveState( + not_null memento, + ListScrollTopState scrollState) = 0; + virtual void restoreState( + not_null memento, + Fn restoreScrollState) = 0; + + virtual ~ListProvider() = default; + +}; + +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp new file mode 100644 index 0000000000..280e48561e --- /dev/null +++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp @@ -0,0 +1,486 @@ +/* +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_list_section.h" + +#include "storage/storage_shared_media.h" +#include "lang/lang_keys.h" +#include "layout/layout_selection.h" +#include "styles/style_info.h" + +namespace Info::Media { +namespace { + +constexpr auto kFloatingHeaderAlpha = 0.9; + +[[nodiscard]] bool HasFloatingHeader(Type type) { + 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()"); +} + +} // namespace + +ListSection::ListSection(Type type) +: _type(type) +, _hasFloatingHeader(HasFloatingHeader(type)) +, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) { +} + +bool ListSection::empty() const { + return _items.empty(); +} + +UniversalMsgId ListSection::minId() const { + Expects(!empty()); + + return GetUniversalId(_items.back()->getItem()); +} + +void ListSection::setTop(int top) { + _top = top; +} + +int ListSection::top() const { + return _top; +} + +int ListSection::height() const { + return _height; +} + +int ListSection::bottom() const { + return top() + height(); +} + +bool ListSection::addItem(not_null item) { + if (_items.empty() || belongsHere(item)) { + if (_items.empty()) { + setHeader(item); + } + appendItem(item); + return true; + } + return false; +} + +void ListSection::finishSection() { + if (_type == Type::GIF) { + _mosaic.setOffset(st::infoMediaSkip, headerHeight()); + _mosaic.setRightSkip(st::infoMediaSkip); + _mosaic.addItems(_items); + } +} + +void ListSection::setHeader(not_null item) { + auto text = [&] { + auto date = item->dateTime().date(); + switch (_type) { + case Type::Photo: + case Type::GIF: + case Type::Video: + case Type::RoundFile: + case Type::RoundVoiceFile: + case Type::File: + return langMonthFull(date); + + case Type::Link: + return langDayOfMonthFull(date); + + case Type::MusicFile: + return QString(); + } + Unexpected("Type in ListSection::setHeader()"); + }(); + _header.setText(st::infoMediaHeaderStyle, text); +} + +bool ListSection::belongsHere( + not_null item) const { + Expects(!_items.empty()); + + auto date = item->dateTime().date(); + auto myDate = _items.back()->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() == myDate.year() + && date.month() == myDate.month(); + + case Type::Link: + return date.year() == myDate.year() + && date.month() == myDate.month() + && date.day() == myDate.day(); + + case Type::MusicFile: + return true; + } + Unexpected("Type in ListSection::belongsHere()"); +} + +void ListSection::appendItem(not_null item) { + _items.push_back(item); + _byItem.emplace(item->getItem(), item); +} + +bool ListSection::removeItem(not_null item) { + if (const auto i = _byItem.find(item); i != end(_byItem)) { + _items.erase(ranges::remove(_items, i->second), end(_items)); + _byItem.erase(i); + refreshHeight(); + return true; + } + return false; +} + +QRect ListSection::findItemRect( + not_null item) const { + auto position = item->position(); + if (!_mosaic.empty()) { + return _mosaic.findRect(position); + } + auto top = position / _itemsInRow; + auto indexInRow = position % _itemsInRow; + auto left = _itemsLeft + + indexInRow * (_itemWidth + st::infoMediaSkip); + return QRect(left, top, _itemWidth, item->height()); +} + +ListFoundItem ListSection::completeResult( + not_null item, + bool exact) const { + return { item, findItemRect(item), exact }; +} + +ListFoundItem ListSection::findItemByPoint(QPoint point) const { + Expects(!_items.empty()); + + if (!_mosaic.empty()) { + const auto found = _mosaic.findByPoint(point); + Assert(found.index != -1); + const auto item = _mosaic.itemAt(found.index); + const auto rect = findItemRect(item); + return { item, rect, found.exact }; + } + auto itemIt = findItemAfterTop(point.y()); + if (itemIt == end(_items)) { + --itemIt; + } + auto item = *itemIt; + auto rect = findItemRect(item); + if (point.y() >= rect.top()) { + auto shift = floorclamp( + point.x(), + (_itemWidth + st::infoMediaSkip), + 0, + _itemsInRow); + while (shift-- && itemIt != _items.end()) { + ++itemIt; + } + if (itemIt == _items.end()) { + --itemIt; + } + item = *itemIt; + rect = findItemRect(item); + } + return { item, rect, rect.contains(point) }; +} + +std::optional ListSection::findItemByItem( + not_null item) const { + const auto i = _byItem.find(item); + if (i != end(_byItem)) { + return ListFoundItem{ i->second, findItemRect(i->second), true }; + } + return std::nullopt; +} + +ListFoundItem ListSection::findItemNearId(UniversalMsgId universalId) const { + Expects(!_items.empty()); + + // #TODO downloads + auto itemIt = ranges::lower_bound( + _items, + universalId, + std::greater<>(), + [](const auto &item) { return GetUniversalId(item); }); + if (itemIt == _items.end()) { + --itemIt; + } + const auto item = *itemIt; + const auto exact = (GetUniversalId(item) == universalId); + return { item, findItemRect(item), exact }; +} + +ListFoundItem ListSection::findItemDetails( + not_null item) const { + return { item, findItemRect(item), true }; +} + +auto ListSection::findItemAfterTop( + int top) -> Items::iterator { + Expects(_mosaic.empty()); + + return ranges::lower_bound( + _items, + top, + std::less_equal<>(), + [this](const auto &item) { + const auto itemTop = item->position() / _itemsInRow; + return itemTop + item->height(); + }); +} + +auto ListSection::findItemAfterTop( + int top) const -> Items::const_iterator { + Expects(_mosaic.empty()); + + return ranges::lower_bound( + _items, + top, + std::less_equal<>(), + [this](const auto &item) { + const auto itemTop = item->position() / _itemsInRow; + return itemTop + item->height(); + }); +} + +auto ListSection::findItemAfterBottom( + Items::const_iterator from, + int bottom) const -> Items::const_iterator { + Expects(_mosaic.empty()); + return ranges::lower_bound( + from, + _items.end(), + bottom, + std::less<>(), + [this](const auto &item) { + const auto itemTop = item->position() / _itemsInRow; + return itemTop; + }); +} + +void ListSection::paint( + Painter &p, + const ListContext &context, + QRect clip, + int outerWidth) const { + auto header = headerHeight(); + if (QRect(0, 0, outerWidth, header).intersects(clip)) { + p.setPen(st::infoMediaHeaderFg); + _header.drawLeftElided( + p, + st::infoMediaHeaderPosition.x(), + st::infoMediaHeaderPosition.y(), + outerWidth - 2 * st::infoMediaHeaderPosition.x(), + outerWidth); + } + auto localContext = context.layoutContext; + localContext.isAfterDate = (header > 0); + + if (!_mosaic.empty()) { + auto paintItem = [&](not_null item, QPoint point) { + p.translate(point.x(), point.y()); + item->paint( + p, + clip.translated(-point), + itemSelection(item, context), + &localContext); + p.translate(-point.x(), -point.y()); + }; + _mosaic.paint(std::move(paintItem), clip); + return; + } + + auto fromIt = findItemAfterTop(clip.y()); + auto tillIt = findItemAfterBottom( + fromIt, + clip.y() + clip.height()); + for (auto it = fromIt; it != tillIt; ++it) { + auto item = *it; + auto rect = findItemRect(item); + localContext.isAfterDate = (header > 0) + && (rect.y() <= header + _itemsTop); + if (rect.intersects(clip)) { + p.translate(rect.topLeft()); + item->paint( + p, + clip.translated(-rect.topLeft()), + itemSelection(item, context), + &localContext); + p.translate(-rect.topLeft()); + } + } +} + +void ListSection::paintFloatingHeader( + Painter &p, + int visibleTop, + int outerWidth) { + if (!_hasFloatingHeader) { + return; + } + const auto headerTop = st::infoMediaHeaderPosition.y() / 2; + if (visibleTop <= (_top + headerTop)) { + return; + } + const auto header = headerHeight(); + const auto headerLeft = st::infoMediaHeaderPosition.x(); + const auto floatingTop = std::min( + visibleTop, + bottom() - header + headerTop); + p.save(); + p.resetTransform(); + p.setOpacity(kFloatingHeaderAlpha); + p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg); + p.setOpacity(1.0); + p.setPen(st::infoMediaHeaderFg); + _header.drawLeftElided( + p, + headerLeft, + floatingTop + headerTop, + outerWidth - 2 * headerLeft, + outerWidth); + p.restore(); +} + +TextSelection ListSection::itemSelection( + not_null item, + const ListContext &context) const { + const auto parent = item->getItem(); + auto dragSelectAction = context.dragSelectAction; + if (dragSelectAction != ListDragSelectAction::None) { + auto i = context.dragSelected->find(parent); + if (i != context.dragSelected->end()) { + return (dragSelectAction == ListDragSelectAction::Selecting) + ? FullSelection + : TextSelection(); + } + } + auto i = context.selected->find(parent); + return (i == context.selected->cend()) + ? TextSelection() + : i->second.text; +} + +int ListSection::headerHeight() const { + return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight; +} + +void ListSection::resizeToWidth(int newWidth) { + auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2; + if (newWidth < minWidth) { + return; + } + + auto resizeOneColumn = [&](int itemsLeft, int itemWidth) { + _itemsLeft = itemsLeft; + _itemsTop = 0; + _itemsInRow = 1; + _itemWidth = itemWidth; + for (auto &item : _items) { + item->resizeGetHeight(_itemWidth); + } + }; + switch (_type) { + case Type::Photo: + case Type::Video: + case Type::RoundFile: { + _itemsLeft = st::infoMediaSkip; + _itemsTop = st::infoMediaSkip; + _itemsInRow = (newWidth - _itemsLeft) + / (st::infoMediaMinGridSize + st::infoMediaSkip); + _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) + - st::infoMediaSkip; + for (auto &item : _items) { + item->resizeGetHeight(_itemWidth); + } + } break; + + case Type::GIF: { + _mosaic.setFullWidth(newWidth - st::infoMediaSkip); + } break; + + case Type::RoundVoiceFile: + case Type::MusicFile: + resizeOneColumn(0, newWidth); + break; + case Type::File: + case Type::Link: { + auto itemsLeft = st::infoMediaHeaderPosition.x(); + auto itemWidth = newWidth - 2 * itemsLeft; + resizeOneColumn(itemsLeft, itemWidth); + } break; + } + + refreshHeight(); +} + +int ListSection::recountHeight() { + auto result = headerHeight(); + + switch (_type) { + case Type::Photo: + case Type::Video: + case Type::RoundFile: { + auto itemHeight = _itemWidth + st::infoMediaSkip; + auto index = 0; + result += _itemsTop; + for (auto &item : _items) { + item->setPosition(_itemsInRow * result + index); + if (++index == _itemsInRow) { + result += itemHeight; + index = 0; + } + } + if (_items.size() % _itemsInRow) { + _rowsCount = int(_items.size()) / _itemsInRow + 1; + result += itemHeight; + } else { + _rowsCount = int(_items.size()) / _itemsInRow; + } + } break; + + case Type::GIF: { + return _mosaic.countDesiredHeight(0) + result; + } break; + + case Type::RoundVoiceFile: + case Type::File: + case Type::MusicFile: + case Type::Link: + for (auto &item : _items) { + item->setPosition(result); + result += item->height(); + } + _rowsCount = _items.size(); + break; + } + + return result; +} + +void ListSection::refreshHeight() { + _height = recountHeight(); +} + +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.h b/Telegram/SourceFiles/info/media/info_media_list_section.h new file mode 100644 index 0000000000..7a394d2e97 --- /dev/null +++ b/Telegram/SourceFiles/info/media/info_media_list_section.h @@ -0,0 +1,93 @@ +/* +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 "info/media/info_media_common.h" +#include "layout/layout_mosaic.h" +#include "ui/text/text.h" + +namespace Info::Media { + +class ListSection { +public: + ListSection(Type type); + + bool addItem(not_null item); + void finishSection(); + + [[nodiscard]] bool empty() const; + + [[nodiscard]] UniversalMsgId minId() const; + + void setTop(int top); + [[nodiscard]] int top() const; + void resizeToWidth(int newWidth); + [[nodiscard]] int height() const; + + [[nodiscard]] int bottom() const; + + bool removeItem(not_null item); + [[nodiscard]] std::optional findItemByItem( + not_null item) const; + [[nodiscard]] ListFoundItem findItemNearId( + UniversalMsgId universalId) const; + [[nodiscard]] ListFoundItem findItemDetails( + not_null item) const; + [[nodiscard]] ListFoundItem findItemByPoint(QPoint point) const; + + void paint( + Painter &p, + const ListContext &context, + QRect clip, + int outerWidth) const; + + void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth); + +private: + using Items = std::vector>; + + [[nodiscard]] int headerHeight() const; + void appendItem(not_null item); + void setHeader(not_null item); + [[nodiscard]] bool belongsHere(not_null item) const; + [[nodiscard]] Items::iterator findItemAfterTop(int top); + [[nodiscard]] Items::const_iterator findItemAfterTop(int top) const; + [[nodiscard]] Items::const_iterator findItemAfterBottom( + Items::const_iterator from, + int bottom) const; + [[nodiscard]] QRect findItemRect(not_null item) const; + [[nodiscard]] ListFoundItem completeResult( + not_null item, + bool exact) const; + [[nodiscard]] TextSelection itemSelection( + not_null item, + const ListContext &context) const; + + int recountHeight(); + void refreshHeight(); + + Type _type = Type{}; + bool _hasFloatingHeader = false; + Ui::Text::String _header; + Items _items; + base::flat_map< + not_null, + not_null> _byItem; + int _itemsLeft = 0; + int _itemsTop = 0; + int _itemWidth = 0; + int _itemsInRow = 1; + mutable int _rowsCount = 0; + int _top = 0; + int _height = 0; + + Mosaic::Layout::MosaicLayout _mosaic; + +}; + +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index b9217b741f..0ee0c2007f 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -7,8 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/media/info_media_list_widget.h" +#include "info/media/info_media_common.h" +#include "info/media/info_media_provider.h" +#include "info/media/info_media_list_section.h" +#include "info/downloads/info_downloads_provider.h" #include "info/info_controller.h" -#include "overview/overview_layout.h" #include "layout/layout_mosaic.h" #include "layout/layout_selection.h" #include "data/data_media_types.h" @@ -54,52 +57,10 @@ namespace Info { namespace Media { namespace { -constexpr auto kFloatingHeaderAlpha = 0.9; -constexpr auto kPreloadedScreensCount = 4; -constexpr auto kPreloadIfLessThanScreens = 2; -constexpr auto kPreloadedScreensCountFull - = kPreloadedScreensCount + 1 + kPreloadedScreensCount; constexpr auto kMediaCountForSearch = 10; -UniversalMsgId GetUniversalId(FullMsgId itemId) { - return peerIsChannel(itemId.peer) - ? UniversalMsgId(itemId.msg) - : UniversalMsgId(itemId.msg - ServerMaxMsgId); -} - -UniversalMsgId GetUniversalId(not_null item) { - return GetUniversalId(item->fullId()); -} - -UniversalMsgId GetUniversalId(not_null layout) { - return GetUniversalId(layout->getItem()->fullId()); -} - -bool HasFloatingHeader(Type type) { - 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()"); -} - } // namespace -struct ListWidget::Context { - Overview::Layout::PaintContext layoutContext; - not_null selected; - not_null dragSelected; - DragSelectAction dragSelectAction; -}; - struct ListWidget::DateBadge { DateBadge(Type type, Fn checkCallback, Fn hideCallback); @@ -114,111 +75,22 @@ struct ListWidget::DateBadge { QRect rect; }; -class ListWidget::Section { -public: - Section(Type type) - : _type(type) - , _hasFloatingHeader(HasFloatingHeader(type)) - , _mosaic(st::emojiPanWidth - st::inlineResultsLeft) { +[[nodiscard]] std::unique_ptr MakeProvider( + not_null controller) { + if (controller->isDownloads()) { + return std::make_unique(controller); } + return std::make_unique(controller); +} - bool addItem(not_null item); - void finishSection(); - - bool empty() const { - return _items.empty(); - } - - UniversalMsgId minId() const { - Expects(!empty()); - - return _items.back().first; - } - UniversalMsgId maxId() const { - Expects(!empty()); - - return _items.front().first; - } - - void setTop(int top) { - _top = top; - } - int top() const { - return _top; - } - void resizeToWidth(int newWidth); - int height() const { - return _height; - } - - int bottom() const { - return top() + height(); - } - - bool removeItem(UniversalMsgId universalId); - FoundItem findItemNearId(UniversalMsgId universalId) const; - FoundItem findItemDetails(not_null item) const; - FoundItem findItemByPoint(QPoint point) const; - - void paint( - Painter &p, - const Context &context, - QRect clip, - int outerWidth) const; - - void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth); - - static int MinItemHeight(Type type, int width); - -private: - using Items = base::flat_map< - UniversalMsgId, - not_null, - std::greater<>>; - int headerHeight() const; - void appendItem(not_null item); - void setHeader(not_null item); - bool belongsHere(not_null item) const; - Items::iterator findItemAfterTop(int top); - Items::const_iterator findItemAfterTop(int top) const; - Items::const_iterator findItemAfterBottom( - Items::const_iterator from, - int bottom) const; - QRect findItemRect(not_null item) const; - FoundItem completeResult( - not_null item, - bool exact) const; - TextSelection itemSelection( - not_null item, - const Context &context) const; - - int recountHeight(); - void refreshHeight(); - - Type _type = Type::Photo; - bool _hasFloatingHeader = false; - Ui::Text::String _header; - Items _items; - int _itemsLeft = 0; - int _itemsTop = 0; - int _itemWidth = 0; - int _itemsInRow = 1; - mutable int _rowsCount = 0; - int _top = 0; - int _height = 0; - - Mosaic::Layout::MosaicLayout _mosaic; - -}; - -bool ListWidget::IsAfter( +bool ListWidget::isAfter( const MouseState &a, - const MouseState &b) { - if (a.itemId != b.itemId) { - return (a.itemId < b.itemId); + const MouseState &b) const { + if (a.item != b.item) { + return _provider->isAfter(a.item, b.item); } - auto xAfter = a.cursor.x() - b.cursor.x(); - auto yAfter = a.cursor.y() - b.cursor.y(); + const auto xAfter = a.cursor.x() - b.cursor.x(); + const auto yAfter = a.cursor.y() - b.cursor.y(); return (xAfter + yAfter >= 0); } @@ -237,17 +109,6 @@ bool ListWidget::SkipSelectTillItem(const MouseState &state) { return false; } -ListWidget::CachedItem::CachedItem(std::unique_ptr item) -: item(std::move(item)) { -} - -ListWidget::CachedItem::CachedItem(CachedItem &&other) = default; - -ListWidget::CachedItem &ListWidget::CachedItem::operator=( - CachedItem && other) = default; - -ListWidget::CachedItem::~CachedItem() = default; - ListWidget::DateBadge::DateBadge( Type type, Fn checkCallback, @@ -259,447 +120,16 @@ ListWidget::DateBadge::DateBadge( || type == Type::GIF) { } -bool ListWidget::Section::addItem(not_null item) { - if (_items.empty() || belongsHere(item)) { - if (_items.empty()) setHeader(item); - appendItem(item); - return true; - } - return false; -} - -void ListWidget::Section::finishSection() { - if (_type == Type::GIF) { - _mosaic.setOffset(st::infoMediaSkip, headerHeight()); - _mosaic.setRightSkip(st::infoMediaSkip); - const auto items = ranges::views::values(_items) | ranges::to_vector; - _mosaic.addItems(items); - } -} - -void ListWidget::Section::setHeader(not_null item) { - auto text = [&] { - auto date = item->dateTime().date(); - switch (_type) { - case Type::Photo: - case Type::GIF: - case Type::Video: - case Type::RoundFile: - case Type::RoundVoiceFile: - case Type::File: - return langMonthFull(date); - - case Type::Link: - return langDayOfMonthFull(date); - - case Type::MusicFile: - return QString(); - } - Unexpected("Type in ListWidget::Section::setHeader()"); - }(); - _header.setText(st::infoMediaHeaderStyle, text); -} - -bool ListWidget::Section::belongsHere( - not_null item) const { - Expects(!_items.empty()); - - auto date = item->dateTime().date(); - auto myDate = _items.back().second->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() == myDate.year() - && date.month() == myDate.month(); - - case Type::Link: - return date.year() == myDate.year() - && date.month() == myDate.month() - && date.day() == myDate.day(); - - case Type::MusicFile: - return true; - } - Unexpected("Type in ListWidget::Section::belongsHere()"); -} - -void ListWidget::Section::appendItem(not_null item) { - _items.emplace(GetUniversalId(item), item); -} - -bool ListWidget::Section::removeItem(UniversalMsgId universalId) { - if (auto it = _items.find(universalId); it != _items.end()) { - it = _items.erase(it); - refreshHeight(); - return true; - } - return false; -} - -QRect ListWidget::Section::findItemRect( - not_null item) const { - auto position = item->position(); - if (!_mosaic.empty()) { - return _mosaic.findRect(position); - } - auto top = position / _itemsInRow; - auto indexInRow = position % _itemsInRow; - auto left = _itemsLeft - + indexInRow * (_itemWidth + st::infoMediaSkip); - return QRect(left, top, _itemWidth, item->height()); -} - -auto ListWidget::Section::completeResult( - not_null item, - bool exact) const -> FoundItem { - return { item, findItemRect(item), exact }; -} - -auto ListWidget::Section::findItemByPoint( - QPoint point) const -> FoundItem { - Expects(!_items.empty()); - if (!_mosaic.empty()) { - const auto found = _mosaic.findByPoint(point); - Assert(found.index != -1); - const auto item = _mosaic.itemAt(found.index); - const auto rect = findItemRect(item); - return { item, rect, found.exact }; - } - auto itemIt = findItemAfterTop(point.y()); - if (itemIt == _items.end()) { - --itemIt; - } - auto item = itemIt->second; - auto rect = findItemRect(item); - if (point.y() >= rect.top()) { - auto shift = floorclamp( - point.x(), - (_itemWidth + st::infoMediaSkip), - 0, - _itemsInRow); - while (shift-- && itemIt != _items.end()) { - ++itemIt; - } - if (itemIt == _items.end()) { - --itemIt; - } - item = itemIt->second; - rect = findItemRect(item); - } - return { item, rect, rect.contains(point) }; -} - -auto ListWidget::Section::findItemNearId(UniversalMsgId universalId) const --> FoundItem { - Expects(!_items.empty()); - - auto itemIt = ranges::lower_bound( - _items, - universalId, - std::greater<>(), - [](const auto &item) -> UniversalMsgId { return item.first; }); - if (itemIt == _items.end()) { - --itemIt; - } - auto item = itemIt->second; - auto exact = (GetUniversalId(item) == universalId); - return { item, findItemRect(item), exact }; -} - -auto ListWidget::Section::findItemDetails(not_null item) const --> FoundItem { - return { item, findItemRect(item), true }; -} - -auto ListWidget::Section::findItemAfterTop( - int top) -> Items::iterator { - Expects(_mosaic.empty()); - return ranges::lower_bound( - _items, - top, - std::less_equal<>(), - [this](const auto &item) { - auto itemTop = item.second->position() / _itemsInRow; - return itemTop + item.second->height(); - }); -} - -auto ListWidget::Section::findItemAfterTop( - int top) const -> Items::const_iterator { - Expects(_mosaic.empty()); - return ranges::lower_bound( - _items, - top, - std::less_equal<>(), - [this](const auto &item) { - auto itemTop = item.second->position() / _itemsInRow; - return itemTop + item.second->height(); - }); -} - -auto ListWidget::Section::findItemAfterBottom( - Items::const_iterator from, - int bottom) const -> Items::const_iterator { - Expects(_mosaic.empty()); - return ranges::lower_bound( - from, - _items.end(), - bottom, - std::less<>(), - [this](const auto &item) { - auto itemTop = item.second->position() / _itemsInRow; - return itemTop; - }); -} - -void ListWidget::Section::paint( - Painter &p, - const Context &context, - QRect clip, - int outerWidth) const { - auto header = headerHeight(); - if (QRect(0, 0, outerWidth, header).intersects(clip)) { - p.setPen(st::infoMediaHeaderFg); - _header.drawLeftElided( - p, - st::infoMediaHeaderPosition.x(), - st::infoMediaHeaderPosition.y(), - outerWidth - 2 * st::infoMediaHeaderPosition.x(), - outerWidth); - } - auto localContext = context.layoutContext; - localContext.isAfterDate = (header > 0); - - if (!_mosaic.empty()) { - auto paintItem = [&](not_null item, QPoint point) { - p.translate(point.x(), point.y()); - item->paint( - p, - clip.translated(-point), - itemSelection(item, context), - &localContext); - p.translate(-point.x(), -point.y()); - }; - _mosaic.paint(std::move(paintItem), clip); - return; - } - - auto fromIt = findItemAfterTop(clip.y()); - auto tillIt = findItemAfterBottom( - fromIt, - clip.y() + clip.height()); - for (auto it = fromIt; it != tillIt; ++it) { - auto item = it->second; - auto rect = findItemRect(item); - localContext.isAfterDate = (header > 0) - && (rect.y() <= header + _itemsTop); - if (rect.intersects(clip)) { - p.translate(rect.topLeft()); - item->paint( - p, - clip.translated(-rect.topLeft()), - itemSelection(item, context), - &localContext); - p.translate(-rect.topLeft()); - } - } -} - -void ListWidget::Section::paintFloatingHeader( - Painter &p, - int visibleTop, - int outerWidth) { - if (!_hasFloatingHeader) { - return; - } - const auto headerTop = st::infoMediaHeaderPosition.y() / 2; - if (visibleTop <= (_top + headerTop)) { - return; - } - const auto header = headerHeight(); - const auto headerLeft = st::infoMediaHeaderPosition.x(); - const auto floatingTop = std::min( - visibleTop, - bottom() - header + headerTop); - p.save(); - p.resetTransform(); - p.setOpacity(kFloatingHeaderAlpha); - p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg); - p.setOpacity(1.0); - p.setPen(st::infoMediaHeaderFg); - _header.drawLeftElided( - p, - headerLeft, - floatingTop + headerTop, - outerWidth - 2 * headerLeft, - outerWidth); - p.restore(); -} - -TextSelection ListWidget::Section::itemSelection( - not_null item, - const Context &context) const { - auto universalId = GetUniversalId(item); - auto dragSelectAction = context.dragSelectAction; - if (dragSelectAction != DragSelectAction::None) { - auto i = context.dragSelected->find(universalId); - if (i != context.dragSelected->end()) { - return (dragSelectAction == DragSelectAction::Selecting) - ? FullSelection - : TextSelection(); - } - } - auto i = context.selected->find(universalId); - return (i == context.selected->cend()) - ? TextSelection() - : i->second.text; -} - -int ListWidget::Section::headerHeight() const { - return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight; -} - -void ListWidget::Section::resizeToWidth(int newWidth) { - auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2; - if (newWidth < minWidth) { - return; - } - - auto resizeOneColumn = [&](int itemsLeft, int itemWidth) { - _itemsLeft = itemsLeft; - _itemsTop = 0; - _itemsInRow = 1; - _itemWidth = itemWidth; - for (auto &item : _items) { - item.second->resizeGetHeight(_itemWidth); - } - }; - switch (_type) { - case Type::Photo: - case Type::Video: - case Type::RoundFile: { - _itemsLeft = st::infoMediaSkip; - _itemsTop = st::infoMediaSkip; - _itemsInRow = (newWidth - _itemsLeft) - / (st::infoMediaMinGridSize + st::infoMediaSkip); - _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) - - st::infoMediaSkip; - for (auto &item : _items) { - item.second->resizeGetHeight(_itemWidth); - } - } break; - - case Type::GIF: { - _mosaic.setFullWidth(newWidth - st::infoMediaSkip); - } break; - - case Type::RoundVoiceFile: - case Type::MusicFile: - resizeOneColumn(0, newWidth); - break; - case Type::File: - case Type::Link: { - auto itemsLeft = st::infoMediaHeaderPosition.x(); - auto itemWidth = newWidth - 2 * itemsLeft; - resizeOneColumn(itemsLeft, itemWidth); - } break; - } - - refreshHeight(); -} - -int ListWidget::Section::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 ListWidget::Section::MinItemHeight()"); -} - -int ListWidget::Section::recountHeight() { - auto result = headerHeight(); - - switch (_type) { - case Type::Photo: - case Type::Video: - case Type::RoundFile: { - auto itemHeight = _itemWidth + st::infoMediaSkip; - auto index = 0; - result += _itemsTop; - for (auto &item : _items) { - item.second->setPosition(_itemsInRow * result + index); - if (++index == _itemsInRow) { - result += itemHeight; - index = 0; - } - } - if (_items.size() % _itemsInRow) { - _rowsCount = int(_items.size()) / _itemsInRow + 1; - result += itemHeight; - } else { - _rowsCount = int(_items.size()) / _itemsInRow; - } - } break; - - case Type::GIF: { - return _mosaic.countDesiredHeight(0) + result; - } break; - - case Type::RoundVoiceFile: - case Type::File: - case Type::MusicFile: - case Type::Link: - for (auto &item : _items) { - item.second->setPosition(result); - result += item.second->height(); - } - _rowsCount = _items.size(); - break; - } - - return result; -} - -void ListWidget::Section::refreshHeight() { - _height = recountHeight(); -} - ListWidget::ListWidget( QWidget *parent, not_null controller) : RpWidget(parent) , _controller(controller) -, _peer(_controller->key().peer()) -, _migrated(_controller->migrated()) -, _type(_controller->section().mediaType()) -, _slice(sliceKey(_universalAroundId)) +, _provider(MakeProvider(_controller)) , _dateBadge(std::make_unique( - _type, + _provider->type(), [=] { scrollDateCheck(); }, [=] { scrollDateHide(); })) { - setMouseTracking(true); start(); } @@ -708,68 +138,63 @@ Main::Session &ListWidget::session() const { } void ListWidget::start() { + setMouseTracking(true); + _controller->setSearchEnabledByContent(false); - style::PaletteChanged( + + _provider->layoutRemoved( + ) | rpl::start_with_next([=](not_null layout) { + if (_overLayout == layout) { + _overLayout = nullptr; + } + _heavyLayouts.remove(layout); + }, lifetime()); + + _provider->refreshed( ) | rpl::start_with_next([=] { - invalidatePaletteCache(); + refreshRows(); }, lifetime()); - session().downloaderTaskFinished( - ) | rpl::start_with_next([=] { - update(); - }, lifetime()); + if (_controller->isDownloads()) { + _provider->refreshViewer(); + } else { + subscribeToSession(&session()); - session().data().itemLayoutChanged( - ) | rpl::start_with_next([this](auto item) { - itemLayoutChanged(item); - }, lifetime()); - - session().data().itemRemoved( - ) | rpl::start_with_next([this](auto item) { - itemRemoved(item); - }, lifetime()); - - session().data().itemRepaintRequest( - ) | rpl::start_with_next([this](auto item) { - repaintItem(item); - }, lifetime()); - - _controller->mediaSourceQueryValue( - ) | rpl::start_with_next([this] { - restart(); - }, lifetime()); + _controller->mediaSourceQueryValue( + ) | rpl::start_with_next([this] { + restart(); + }, lifetime()); + } setupSelectRestriction(); } -void ListWidget::setupSelectRestriction() { - if (_peer->isUser()) { - return; - } - 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(); +void ListWidget::subscribeToSession(not_null session) { + session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + update(); + }, lifetime()); - auto rights = chat - ? chat->adminRightsValue() - : channel->adminRightsValue(); - auto canDelete = std::move( - rights - ) | rpl::map([=] { - return chat - ? chat->canDeleteMessages() - : channel->canDeleteMessages(); - }); - rpl::combine( - std::move(noForwards), - std::move(canDelete) + session->data().itemLayoutChanged( + ) | rpl::start_with_next([this](auto item) { + itemLayoutChanged(item); + }, lifetime()); + + session->data().itemRemoved( + ) | rpl::start_with_next([this](auto item) { + itemRemoved(item); + }, lifetime()); + + session->data().itemRepaintRequest( + ) | rpl::start_with_next([this](auto item) { + repaintItem(item); + }, lifetime()); +} + +void ListWidget::setupSelectRestriction() { + _provider->hasSelectRestrictionChanges( ) | rpl::filter([=] { - return hasSelectRestriction() && hasSelectedItems(); + return _provider->hasSelectRestriction() && hasSelectedItems(); }) | rpl::start_with_next([=] { clearSelected(); if (_mouseAction == MouseAction::PrepareSelect) { @@ -790,10 +215,12 @@ rpl::producer ListWidget::selectedListValue() const { QRect ListWidget::getCurrentSongGeometry() { const auto type = AudioMsgId::Type::Song; const auto current = ::Media::Player::instance()->current(type); - const auto fullMsgId = current.contextId(); - if (fullMsgId && isPossiblyMyId(fullMsgId)) { - if (const auto item = findItemById(GetUniversalId(fullMsgId))) { - return item->geometry; + if (const auto document = current.audio()) { + const auto contextId = current.contextId(); + if (const auto item = document->owner().message(contextId)) { + if (const auto found = findItemByItem(item)) { + return found->geometry; + } } } return QRect(0, 0, width(), 0); @@ -804,26 +231,24 @@ void ListWidget::restart() { _overLayout = nullptr; _sections.clear(); - _layouts.clear(); _heavyLayouts.clear(); - _universalAroundId = kDefaultAroundId; - _idsLimit = kMinimalIdsLimit; - _slice = SparseIdsMergedSlice(sliceKey(_universalAroundId)); - - refreshViewer(); + _provider->restart(); } void ListWidget::itemRemoved(not_null item) { - if (!isMyItem(item)) { + if (!_provider->isMyItem(item)) { return; } - auto id = GetUniversalId(item); + + if (_contextItem == item) { + _contextItem = nullptr; + } auto needHeightRefresh = false; - auto sectionIt = findSectionByItem(id); + auto sectionIt = findSectionByItem(item); if (sectionIt != _sections.end()) { - if (sectionIt->removeItem(id)) { + if (sectionIt->removeItem(item)) { if (sectionIt->empty()) { _sections.erase(sectionIt); } @@ -834,14 +259,17 @@ void ListWidget::itemRemoved(not_null item) { if (isItemLayout(item, _overLayout)) { _overLayout = nullptr; } + _dragSelected.remove(item); - if (const auto i = _layouts.find(id); i != _layouts.end()) { - _heavyLayouts.remove(i->second.item.get()); - _layouts.erase(i); + if (_pressState.item == item) { + mouseActionCancel(); + } + if (_overState.item == item) { + _mouseAction = MouseAction::None; + _overState = {}; } - _dragSelected.remove(id); - if (const auto i = _selected.find(id); i != _selected.cend()) { + if (const auto i = _selected.find(item); i != _selected.cend()) { removeItemSelection(i); } @@ -851,22 +279,11 @@ void ListWidget::itemRemoved(not_null item) { mouseActionUpdate(_mousePosition); } -FullMsgId ListWidget::computeFullId( - UniversalMsgId universalId) const { - Expects(universalId != 0); - - return (universalId > 0) - ? FullMsgId(_peer->id, universalId) - : FullMsgId( - (_migrated ? _migrated : _peer.get())->id, - ServerMaxMsgId + universalId); -} - auto ListWidget::collectSelectedItems() const -> SelectedItems { auto convert = [&]( - UniversalMsgId universalId, + not_null item, const SelectionData &selection) { - auto result = SelectedItem(computeFullId(universalId)); + auto result = SelectedItem(item->globalId()); result.canDelete = selection.canDelete; result.canForward = selection.canForward; return result; @@ -874,7 +291,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { auto transformation = [&](const auto &item) { return convert(item.first, item.second); }; - auto items = SelectedItems(_type); + auto items = SelectedItems(_provider->type()); if (hasSelectedItems()) { items.list.reserve(_selected.size()); std::transform( @@ -891,7 +308,7 @@ MessageIdsList ListWidget::collectSelectedIds() const { return ranges::views::all( selected.list ) | ranges::views::transform([](const SelectedItem &item) { - return item.msgId; + return item.globalId.itemId; // #TODO downloads }) | ranges::to_vector; } @@ -936,53 +353,31 @@ void ListWidget::itemLayoutChanged( } void ListWidget::repaintItem(const HistoryItem *item) { - if (item && isMyItem(item)) { - repaintItem(GetUniversalId(item)); - } -} - -void ListWidget::repaintItem(UniversalMsgId universalId) { - if (auto item = findItemById(universalId)) { - repaintItem(item->geometry); + if (const auto found = findItemByItem(item)) { + repaintItem(found->geometry); } } void ListWidget::repaintItem(const BaseLayout *item) { if (item) { - repaintItem(GetUniversalId(item)); + repaintItem(item->getItem()); } } void ListWidget::repaintItem(not_null item) { - repaintItem(GetUniversalId(item)); + repaintItem(item->getItem()); } void ListWidget::repaintItem(QRect itemGeometry) { rtlupdate(itemGeometry); } -bool ListWidget::isMyItem(not_null item) const { - const auto peer = item->history()->peer; - return (_peer == peer) || (_migrated == peer); -} - -bool ListWidget::isPossiblyMyId(FullMsgId fullId) const { - return (fullId.peer == _peer->id) - || (_migrated && fullId.peer == _migrated->id); -} - bool ListWidget::isItemLayout( not_null item, BaseLayout *layout) const { return layout && (layout->getItem() == item); } -void ListWidget::invalidatePaletteCache() { - for (auto &layout : _layouts) { - layout.second.item->invalidateCache(); - } -} - void ListWidget::registerHeavyItem(not_null item) { if (!_heavyLayouts.contains(item)) { _heavyLayouts.emplace(item); @@ -999,7 +394,7 @@ void ListWidget::unregisterHeavyItem(not_null item) { } bool ListWidget::itemVisible(not_null item) { - if (const auto &found = findItemById(GetUniversalId(item))) { + if (const auto &found = findItemByItem(item->getItem())) { const auto geometry = found->geometry; return (geometry.top() < _visibleBottom) && (geometry.top() + geometry.height() > _visibleTop); @@ -1021,195 +416,35 @@ void ListWidget::openDocument( showInMediaView); } -SparseIdsMergedSlice::Key ListWidget::sliceKey( - UniversalMsgId universalId) const { - using Key = SparseIdsMergedSlice::Key; - if (_migrated) { - return Key(_peer->id, _migrated->id, universalId); - } - if (universalId < 0) { - // Convert back to plain id for non-migrated histories. - universalId = universalId + ServerMaxMsgId; - } - return Key(_peer->id, 0, universalId); -} - -void ListWidget::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); - } - refreshRows(); - }, _viewerLifetime); -} - -BaseLayout *ListWidget::getLayout(UniversalMsgId universalId) { - auto it = _layouts.find(universalId); - if (it == _layouts.end()) { - if (auto layout = createLayout(universalId, _type)) { - layout->initDimensions(); - it = _layouts.emplace( - universalId, - std::move(layout)).first; - } else { - return nullptr; - } - } - it->second.stale = false; - return it->second.item.get(); -} - -BaseLayout *ListWidget::getExistingLayout( - UniversalMsgId universalId) const { - auto it = _layouts.find(universalId); - return (it != _layouts.end()) - ? it->second.item.get() - : nullptr; -} - -std::unique_ptr ListWidget::createLayout( - UniversalMsgId universalId, - Type type) { - auto item = session().data().message(computeFullId(universalId)); - if (!item) { - return nullptr; - } - auto getPhoto = [&]() -> PhotoData* { - if (const auto media = item->media()) { - return media->photo(); - } - return nullptr; - }; - auto getFile = [&]() -> DocumentData* { - if (auto media = item->media()) { - return media->document(); - } - return nullptr; - }; - - auto &songSt = st::overviewFileLayout; - using namespace Overview::Layout; - switch (type) { - case Type::Photo: - if (const auto photo = getPhoto()) { - return std::make_unique(this, item, photo); - } - return nullptr; - case Type::GIF: - if (const auto file = getFile()) { - return std::make_unique(this, item, file); - } - return nullptr; - case Type::Video: - if (const auto file = getFile()) { - return std::make_unique