/* 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_widget.h" #include "info/info_controller.h" #include "overview/overview_layout.h" #include "data/data_media_types.h" #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_file_origin.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_service_message.h" #include "window/themes/window_theme.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "ui/widgets/popup_menu.h" #include "ui/controls/delete_message_context_action.h" #include "ui/ui_utility.h" #include "ui/inactive_press.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "mainwidget.h" #include "mainwindow.h" #include "styles/style_overview.h" #include "styles/style_info.h" #include "base/platform/base_platform_info.h" #include "media/player/media_player_instance.h" #include "boxes/peer_list_controllers.h" #include "boxes/confirm_box.h" #include "core/file_utilities.h" #include "facades.h" #include #include namespace Layout = Overview::Layout; 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 (itemId.channel != 0) ? 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::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 { Layout::PaintContext layoutContext; not_null selected; not_null dragSelected; DragSelectAction dragSelectAction; }; class ListWidget::Section { public: Section(Type type) : _type(type) , _hasFloatingHeader(HasFloatingHeader(type)) { } bool addItem(not_null item); 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() const; 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; }; bool ListWidget::IsAfter( const MouseState &a, const MouseState &b) { if (a.itemId != b.itemId) { return (a.itemId < b.itemId); } auto xAfter = a.cursor.x() - b.cursor.x(); auto yAfter = a.cursor.y() - b.cursor.y(); return (xAfter + yAfter >= 0); } bool ListWidget::SkipSelectFromItem(const MouseState &state) { if (state.cursor.y() >= state.size.height() || state.cursor.x() >= state.size.width()) { return true; } return false; } bool ListWidget::SkipSelectTillItem(const MouseState &state) { if (state.cursor.x() < 0 || state.cursor.y() < 0) { return true; } 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; 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::setHeader(not_null item) { auto text = [&] { auto date = item->dateTime().date(); switch (_type) { case Type::Photo: 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::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(); 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()); 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 { 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 { 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 { 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 baseIndex = 0; 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 top = header + _itemsTop; auto fromcol = floorclamp( clip.x() - _itemsLeft, _itemWidth, 0, _itemsInRow); auto tillcol = ceilclamp( clip.x() + clip.width() - _itemsLeft, _itemWidth, 0, _itemsInRow); auto localContext = context.layoutContext; localContext.isAfterDate = (header > 0); 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::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::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() const { 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::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)) , _dateBadge(DateBadge{ .check = SingleQueuedInvokation([=] { scrollDateCheck(); }), .hideTimer = base::Timer([=] { scrollDateHide(); }), .goodType = (_type == Type::Photo || _type == Type::Video), }) { setMouseTracking(true); start(); } Main::Session &ListWidget::session() const { return _controller->session(); } void ListWidget::start() { _controller->setSearchEnabledByContent(false); ObservableViewer( *Window::Theme::Background() ) | rpl::start_with_next([this](const auto &update) { if (update.paletteChanged()) { invalidatePaletteCache(); } }, lifetime()); session().downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); }, lifetime()); 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()); } rpl::producer ListWidget::scrollToRequests() const { return _scrollToRequests.events(); } rpl::producer ListWidget::selectedListValue() const { return _selectedListStream.events_starting_with( collectSelectedItems()); } 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; } } return QRect(0, 0, width(), 0); } void ListWidget::restart() { mouseActionCancel(); _overLayout = nullptr; _sections.clear(); _layouts.clear(); _heavyLayouts.clear(); _universalAroundId = kDefaultAroundId; _idsLimit = kMinimalIdsLimit; _slice = SparseIdsMergedSlice(sliceKey(_universalAroundId)); refreshViewer(); } void ListWidget::itemRemoved(not_null item) { if (!isMyItem(item)) { return; } auto id = GetUniversalId(item); auto needHeightRefresh = false; auto sectionIt = findSectionByItem(id); if (sectionIt != _sections.end()) { if (sectionIt->removeItem(id)) { auto top = sectionIt->top(); if (sectionIt->empty()) { _sections.erase(sectionIt); } needHeightRefresh = true; } } if (isItemLayout(item, _overLayout)) { _overLayout = nullptr; } if (const auto i = _layouts.find(id); i != _layouts.end()) { _heavyLayouts.remove(i->second.item.get()); _layouts.erase(i); } _dragSelected.remove(id); if (const auto i = _selected.find(id); i != _selected.cend()) { removeItemSelection(i); } if (needHeightRefresh) { refreshHeight(); } mouseActionUpdate(_mousePosition); } FullMsgId ListWidget::computeFullId( UniversalMsgId universalId) const { Expects(universalId != 0); return (universalId > 0) ? FullMsgId(peerToChannel(_peer->id), universalId) : FullMsgId(NoChannel, ServerMaxMsgId + universalId); } auto ListWidget::collectSelectedItems() const -> SelectedItems { auto convert = [&]( UniversalMsgId universalId, const SelectionData &selection) { auto result = SelectedItem(computeFullId(universalId)); result.canDelete = selection.canDelete; result.canForward = selection.canForward; return result; }; auto transformation = [&](const auto &item) { return convert(item.first, item.second); }; auto items = SelectedItems(_type); if (hasSelectedItems()) { items.list.reserve(_selected.size()); std::transform( _selected.begin(), _selected.end(), std::back_inserter(items.list), transformation); } return items; } MessageIdsList ListWidget::collectSelectedIds() const { const auto selected = collectSelectedItems(); return ranges::views::all( selected.list ) | ranges::views::transform([](const SelectedItem &item) { return item.msgId; }) | ranges::to_vector; } void ListWidget::pushSelectedItems() { _selectedListStream.fire(collectSelectedItems()); } bool ListWidget::hasSelected() const { return !_selected.empty(); } bool ListWidget::isSelectedItem( const SelectedMap::const_iterator &i) const { return (i != _selected.end()) && (i->second.text == FullSelection); } void ListWidget::removeItemSelection( const SelectedMap::const_iterator &i) { Expects(i != _selected.cend()); _selected.erase(i); if (_selected.empty()) { update(); } pushSelectedItems(); } bool ListWidget::hasSelectedText() const { return hasSelected() && !hasSelectedItems(); } bool ListWidget::hasSelectedItems() const { return isSelectedItem(_selected.cbegin()); } void ListWidget::itemLayoutChanged( not_null item) { if (isItemLayout(item, _overLayout)) { mouseActionUpdate(); } } 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); } } void ListWidget::repaintItem(const BaseLayout *item) { if (item) { repaintItem(GetUniversalId(item)); } } void ListWidget::repaintItem(QRect itemGeometry) { rtlupdate(itemGeometry); } bool ListWidget::isMyItem(not_null item) const { auto peer = item->history()->peer; return (_peer == peer) || (_migrated == peer); } bool ListWidget::isPossiblyMyId(FullMsgId fullId) const { return fullId.channel ? (_peer->isChannel() && peerToChannel(_peer->id) == fullId.channel) : (!_peer->isChannel() || _migrated); } 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); _heavyLayoutsInvalidated = true; } } void ListWidget::unregisterHeavyItem(not_null item) { const auto i = _heavyLayouts.find(item); if (i != _heavyLayouts.end()) { _heavyLayouts.erase(i); _heavyLayoutsInvalidated = true; } } 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 += ServerMaxMsgId; } return Key(_peer->id, 0, universalId); } void ListWidget::refreshViewer() { _viewerLifetime.destroy(); 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 Layout; switch (type) { case Type::Photo: if (const auto photo = getPhoto()) { return std::make_unique(this, item, photo); } return nullptr; case Type::Video: if (const auto file = getFile()) { return std::make_unique