diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index 01d4c2e861..aaa660ef05 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -44,6 +44,21 @@ using TextState = HistoryView::TextState; constexpr auto kMaxInlineArea = 1280 * 720; +[[nodiscard]] QSize ScaleDown(int w, int h, int maxW, int maxH) { + if (w * maxH > h * maxW) { + if (maxH < h) { + w = w * maxH / h; + h = maxH; + } + } else { + if (maxW < w) { + h = h * maxW / w; + w = maxW; + } + } + return { w, h }; +} + [[nodiscard]] bool CanPlayInline(not_null document) { const auto dimensions = document->dimensions; return dimensions.width() * dimensions.height() <= kMaxInlineArea; @@ -1244,27 +1259,172 @@ void Contact::prepareThumbnail(int width, int height) const { && (_thumb.height() == height * style::DevicePixelRatio()))) { return; } - auto w = qMax(style::ConvertScale(thumb->width()), 1); - auto h = qMax(style::ConvertScale(thumb->height()), 1); - if (w * height > h * width) { - if (height < h) { - w = w * height / h; - h = height; - } - } else { - if (width < w) { - h = h * width / w; - w = width; - } - } + const auto scaled = ScaleDown( + qMax(style::ConvertScale(thumb->width()), 1), + qMax(style::ConvertScale(thumb->height()), 1), + width, + height); _thumb = Image(base::duplicate(*thumb)).pixNoCache( - QSize(w, h) * style::DevicePixelRatio(), + scaled * style::DevicePixelRatio(), { .options = Images::Option::TransparentBackground, .outer = { width, height }, }); } +Thumbnail::Thumbnail( + not_null context, + std::shared_ptr result) +: ItemBase(context, std::move(result)) { +} + +void Thumbnail::initDimensions() { + int w = 0, h = 0; + if (const auto photo = getResultPhoto()) { + w = photo->width(); + h = photo->height(); + } else if (const auto document = getResultDocument()) { + w = document->dimensions.width(); + h = document->dimensions.height(); + } + if (w <= 0 || h <= 0) { + w = h = 1; + } + w = w * st::inlineMediaHeight / h; + _maxw = qMax(w, int32(st::inlineResultsMinWidth)); + _minh = st::inlineMediaHeight + st::inlineResultsSkip; +} + +QSize Thumbnail::countFrameSize() const { + int w = 0, h = 0; + if (const auto photo = getResultPhoto()) { + w = photo->width(); + h = photo->height(); + } else if (const auto document = getResultDocument()) { + w = document->dimensions.width(); + h = document->dimensions.height(); + } + if (w <= 0 || h <= 0) { + return { _width, st::inlineMediaHeight }; + } + // Aspect-fill: scale so the smaller dimension covers the cell. + const auto targetHeight = st::inlineMediaHeight; + if (w * targetHeight > h * _width) { + w = w * targetHeight / h; + h = targetHeight; + } else { + h = h * _width / w; + w = _width; + } + return { qMax(w, 1), qMax(h, 1) }; +} + +void Thumbnail::validateThumbnail( + Image *image, + QSize size, + QSize frame, + bool good) const { + if (!image || (_thumbGood && !good)) { + return; + } else if ((_thumb.size() == size * style::DevicePixelRatio()) + && (_thumbGood || !good)) { + return; + } + _thumb = image->pixNoCache( + frame * style::DevicePixelRatio(), + { + .options = (Images::Option::TransparentBackground + | (good ? Images::Option() : Images::Option::Blur)), + .outer = size, + }); + _thumbGood = good; +} + +void Thumbnail::prepareThumbnail(QSize size, QSize frame) const { + if (const auto photo = getResultPhoto()) { + if (!_photoMedia) { + _photoMedia = photo->createMediaView(); + _photoMedia->wanted(Data::PhotoSize::Thumbnail, fileOrigin()); + } + using PhotoSize = Data::PhotoSize; + validateThumbnail( + _photoMedia->image(PhotoSize::Thumbnail), + size, + frame, + true); + validateThumbnail( + _photoMedia->image(PhotoSize::Small), + size, + frame, + false); + validateThumbnail( + _photoMedia->thumbnailInline(), + size, + frame, + false); + } else if (const auto document = getResultDocument()) { + if (!_documentMedia) { + _documentMedia = document->createMediaView(); + document->loadThumbnail(fileOrigin()); + } + validateThumbnail( + _documentMedia->thumbnail(), + size, + frame, + true); + validateThumbnail( + _documentMedia->thumbnailInline(), + size, + frame, + false); + } else if (const auto thumb = getResultThumb(fileOrigin())) { + if (_thumb.isNull()) { + const auto scaled = ScaleDown( + qMax(style::ConvertScale(thumb->width()), 1), + qMax(style::ConvertScale(thumb->height()), 1), + frame.width(), + frame.height()); + _thumb = Image(base::duplicate(*thumb)).pixNoCache( + scaled * style::DevicePixelRatio(), + { + .options = Images::Option::TransparentBackground, + .outer = size, + }); + } + } +} + +void Thumbnail::paint( + Painter &p, + const QRect &clip, + const PaintContext *context) const { + const auto height = st::inlineMediaHeight; + const auto frame = countFrameSize(); + + QRect r(0, 0, _width, height); + prepareThumbnail({ _width, height }, frame); + if (_thumb.isNull()) { + p.fillRect(r, st::overviewPhotoBg); + } else { + p.drawPixmap(r.topLeft(), _thumb); + } +} + +TextState Thumbnail::getState( + QPoint point, + StateRequest request) const { + if (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) { + return { nullptr, _send }; + } + return {}; +} + +void Thumbnail::unloadHeavyPart() { + _photoMedia = nullptr; + _documentMedia = nullptr; + ItemBase::unloadHeavyPart(); +} + Article::Article( not_null context, std::shared_ptr result, @@ -1389,6 +1549,10 @@ TextState Article::getState( void Article::prepareThumbnail(int width, int height) const { if (!hasResultThumb()) { + prepareMediaThumbnail(width, height); + if (!_thumb.isNull()) { + return; + } if ((_thumb.width() != width * style::DevicePixelRatio()) || (_thumb.height() != height * style::DevicePixelRatio())) { _thumb = getResultContactAvatar(width, height); @@ -1403,27 +1567,73 @@ void Article::prepareThumbnail(int width, int height) const { && (_thumb.height() == height * style::DevicePixelRatio()))) { return; } - auto w = qMax(style::ConvertScale(thumb->width()), 1); - auto h = qMax(style::ConvertScale(thumb->height()), 1); - if (w * height > h * width) { - if (height < h) { - w = w * height / h; - h = height; - } - } else { - if (width < w) { - h = h * width / w; - w = width; - } - } + const auto scaled = ScaleDown( + qMax(style::ConvertScale(thumb->width()), 1), + qMax(style::ConvertScale(thumb->height()), 1), + width, + height); _thumb = Image(base::duplicate(*thumb)).pixNoCache( - QSize(w, h) * style::DevicePixelRatio(), + scaled * style::DevicePixelRatio(), { .options = Images::Option::TransparentBackground, .outer = { width, height }, }); } +void Article::prepareMediaThumbnail(int width, int height) const { + auto thumbGood = false; + const auto make = [&](Image *image, bool good) { + if (!image || (thumbGood && !good)) { + return; + } + if (!_thumb.isNull() && !good) { + return; + } + const auto scaled = ScaleDown( + qMax(style::ConvertScale(image->width()), 1), + qMax(style::ConvertScale(image->height()), 1), + width, + height); + _thumb = image->pixNoCache( + scaled * style::DevicePixelRatio(), + { + .options = (Images::Option::TransparentBackground + | (good + ? Images::Option() + : Images::Option::Blur)), + .outer = { width, height }, + }); + if (good) { + thumbGood = true; + } + }; + if (const auto photo = getResultPhoto()) { + if (!_photoMedia) { + _photoMedia = photo->createMediaView(); + _photoMedia->wanted(Data::PhotoSize::Thumbnail, fileOrigin()); + } + using PhotoSize = Data::PhotoSize; + make(_photoMedia->image(PhotoSize::Thumbnail), true); + make(_photoMedia->image(PhotoSize::Small), false); + make(_photoMedia->thumbnailInline(), false); + } else if (const auto document = getResultDocument()) { + if (document->hasThumbnail()) { + if (!_documentMedia) { + _documentMedia = document->createMediaView(); + document->loadThumbnail(fileOrigin()); + } + make(_documentMedia->thumbnail(), true); + make(_documentMedia->thumbnailInline(), false); + } + } +} + +void Article::unloadHeavyPart() { + _photoMedia = nullptr; + _documentMedia = nullptr; + ItemBase::unloadHeavyPart(); +} + Game::Game(not_null context, std::shared_ptr result) : ItemBase(context, std::move(result)) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h index e483f858a9..25ecd5f62b 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h @@ -364,6 +364,45 @@ private: }; +class Thumbnail : public ItemBase { +public: + Thumbnail(not_null context, std::shared_ptr result); + + void initDimensions() override; + + bool isFullLine() const override { + return false; + } + bool hasRightSkip() const override { + return true; + } + + void paint( + Painter &p, + const QRect &clip, + const PaintContext *context) const override; + TextState getState( + QPoint point, + StateRequest request) const override; + + void unloadHeavyPart() override; + +private: + QSize countFrameSize() const; + void prepareThumbnail(QSize size, QSize frame) const; + void validateThumbnail( + Image *image, + QSize size, + QSize frame, + bool good) const; + + mutable QPixmap _thumb; + mutable bool _thumbGood = false; + mutable std::shared_ptr _photoMedia; + mutable std::shared_ptr _documentMedia; + +}; + class Article : public ItemBase { public: Article( @@ -379,6 +418,8 @@ public: QPoint point, StateRequest request) const override; + void unloadHeavyPart() override; + private: ClickHandlerPtr _url, _link; @@ -389,6 +430,10 @@ private: int32 _urlWidth; void prepareThumbnail(int width, int height) const; + void prepareMediaThumbnail(int width, int height) const; + + mutable std::shared_ptr _photoMedia; + mutable std::shared_ptr _documentMedia; }; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp index 146730aa72..37ea6ce3ab 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp @@ -93,9 +93,37 @@ void ItemBase::layoutChanged() { std::unique_ptr ItemBase::createLayout( not_null context, std::shared_ptr result, - bool forceThumb) { + bool forceThumb, + std::optional gallery) { using Type = Result::Type; + if (gallery.has_value()) { + if (!*gallery) { + // Force list mode: render all types as Article. + return std::make_unique( + context, + std::move(result), + forceThumb); + } else { + // Force gallery mode: render list types as Thumbnail. + switch (result->_type) { + case Type::Article: + case Type::Geo: + case Type::Venue: + case Type::Video: + case Type::Audio: + case Type::File: + case Type::Contact: + case Type::Game: + return std::make_unique( + context, + std::move(result)); + default: + break; + } + } + } + switch (result->_type) { case Type::Photo: return std::make_unique(context, std::move(result)); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h index e370b34774..11d72e8a92 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h @@ -113,7 +113,8 @@ public: static std::unique_ptr createLayout( not_null context, std::shared_ptr result, - bool forceThumb); + bool forceThumb, + std::optional gallery = std::nullopt); static std::unique_ptr createLayoutGif( not_null context, not_null document); diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp index 3a2d170df2..c96ee1631d 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp @@ -455,7 +455,8 @@ ItemBase *Inner::layoutPrepareInlineResult(std::shared_ptr result) { if (auto layout = ItemBase::createLayout( this, std::move(result), - _inlineWithThumb)) { + _inlineWithThumb, + _gallery)) { it = _inlineLayouts.emplace(raw, std::move(layout)).first; it->second->initDimensions(); } else { @@ -556,6 +557,8 @@ int Inner::refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntr Assert(_inlineBot != 0); + _gallery = entry->gallery; + const auto count = int(entry->results.size()); const auto from = validateExistingInlineRows(entry->results); auto added = 0; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.h b/Telegram/SourceFiles/inline_bots/inline_results_inner.h index e7d47a30a4..fd3dab2df3 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.h +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.h @@ -58,6 +58,7 @@ struct CacheEntry { QString switchPmStartToken; QByteArray switchPmUrl; Results results; + bool gallery = false; }; class Inner @@ -158,6 +159,7 @@ private: crl::time _lastUpdatedAt = 0; base::Timer _updateInlineItems; bool _inlineWithThumb = false; + bool _gallery = false; object_ptr _switchPmButton = { nullptr }; QString _switchPmStartToken; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp index 39fb2cb755..62372aad0c 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp @@ -397,6 +397,7 @@ void Widget::inlineResultsDone(const MTPmessages_BotResults &result) { entry->switchPmStartToken = QString(); entry->switchPmUrl = switchWebView->data().vurl().v; } + entry->gallery = d.is_gallery(); if (const auto count = v.size()) { entry->results.reserve(entry->results.size() + count);