Added initial support gallery flag for inline bot results layout mode.

This commit is contained in:
23rd
2026-03-31 15:19:24 +03:00
parent 6df0333ca2
commit 892de6154d
7 changed files with 321 additions and 31 deletions
@@ -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<DocumentData*> 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*> context,
std::shared_ptr<Result> 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*> context,
std::shared_ptr<Result> 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*> context, std::shared_ptr<Result> result)
: ItemBase(context, std::move(result))
, _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)
@@ -364,6 +364,45 @@ private:
};
class Thumbnail : public ItemBase {
public:
Thumbnail(not_null<Context*> context, std::shared_ptr<Result> 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<Data::PhotoMedia> _photoMedia;
mutable std::shared_ptr<Data::DocumentMedia> _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<Data::PhotoMedia> _photoMedia;
mutable std::shared_ptr<Data::DocumentMedia> _documentMedia;
};
@@ -93,9 +93,37 @@ void ItemBase::layoutChanged() {
std::unique_ptr<ItemBase> ItemBase::createLayout(
not_null<Context*> context,
std::shared_ptr<Result> result,
bool forceThumb) {
bool forceThumb,
std::optional<bool> gallery) {
using Type = Result::Type;
if (gallery.has_value()) {
if (!*gallery) {
// Force list mode: render all types as Article.
return std::make_unique<internal::Article>(
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<internal::Thumbnail>(
context,
std::move(result));
default:
break;
}
}
}
switch (result->_type) {
case Type::Photo:
return std::make_unique<internal::Photo>(context, std::move(result));
@@ -113,7 +113,8 @@ public:
static std::unique_ptr<ItemBase> createLayout(
not_null<Context*> context,
std::shared_ptr<Result> result,
bool forceThumb);
bool forceThumb,
std::optional<bool> gallery = std::nullopt);
static std::unique_ptr<ItemBase> createLayoutGif(
not_null<Context*> context,
not_null<DocumentData*> document);
@@ -455,7 +455,8 @@ ItemBase *Inner::layoutPrepareInlineResult(std::shared_ptr<Result> 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;
@@ -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<Ui::RoundButton> _switchPmButton = { nullptr };
QString _switchPmStartToken;
@@ -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);