"Fossies" - the Fresh Open Source Software Archive

Member "tdesktop-4.8.3/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp" (1 Jun 2023, 25509 Bytes) of package /linux/misc/tdesktop-4.8.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "gifs_list_widget.cpp" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 4.8.1_vs_4.8.3.

    1 /*
    2 This file is part of Telegram Desktop,
    3 the official desktop application for the Telegram messaging service.
    4 
    5 For license and copyright information please follow this link:
    6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
    7 */
    8 #include "chat_helpers/gifs_list_widget.h"
    9 
   10 #include "api/api_toggling_media.h" // Api::ToggleSavedGif
   11 #include "base/const_string.h"
   12 #include "base/qt/qt_key_modifiers.h"
   13 #include "chat_helpers/stickers_list_footer.h"
   14 #include "data/data_photo.h"
   15 #include "data/data_document.h"
   16 #include "data/stickers/data_custom_emoji.h"
   17 #include "data/data_session.h"
   18 #include "data/data_user.h"
   19 #include "data/data_file_origin.h"
   20 #include "data/data_photo_media.h"
   21 #include "data/data_document_media.h"
   22 #include "data/stickers/data_stickers.h"
   23 #include "menu/menu_send.h" // SendMenu::FillSendMenu
   24 #include "core/click_handler_types.h"
   25 #include "ui/controls/tabbed_search.h"
   26 #include "ui/widgets/buttons.h"
   27 #include "ui/widgets/input_fields.h"
   28 #include "ui/widgets/popup_menu.h"
   29 #include "ui/effects/ripple_animation.h"
   30 #include "ui/image/image.h"
   31 #include "ui/painter.h"
   32 #include "boxes/stickers_box.h"
   33 #include "inline_bots/inline_bot_result.h"
   34 #include "storage/localstorage.h"
   35 #include "lang/lang_keys.h"
   36 #include "layout/layout_position.h"
   37 #include "mainwindow.h"
   38 #include "main/main_session.h"
   39 #include "window/window_session_controller.h"
   40 #include "history/view/history_view_cursor_state.h"
   41 #include "storage/storage_account.h" // Account::writeSavedGifs
   42 #include "styles/style_chat_helpers.h"
   43 #include "styles/style_menu_icons.h"
   44 
   45 #include <QtWidgets/QApplication>
   46 
   47 namespace ChatHelpers {
   48 namespace {
   49 
   50 constexpr auto kSearchRequestDelay = 400;
   51 constexpr auto kSearchBotUsername = "gif"_cs;
   52 constexpr auto kMinRepaintDelay = crl::time(33);
   53 constexpr auto kMinAfterScrollDelay = crl::time(33);
   54 
   55 } // namespace
   56 
   57 void AddGifAction(
   58         Fn<void(QString, Fn<void()> &&, const style::icon*)> callback,
   59         std::shared_ptr<Show> show,
   60         not_null<DocumentData*> document) {
   61     if (!document->isGifv()) {
   62         return;
   63     }
   64     auto &data = document->owner();
   65     const auto index = data.stickers().savedGifs().indexOf(document);
   66     const auto saved = (index >= 0);
   67     const auto text = (saved
   68         ? tr::lng_context_delete_gif
   69         : tr::lng_context_save_gif)(tr::now);
   70     callback(text, [=] {
   71         Api::ToggleSavedGif(
   72             show,
   73             document,
   74             Data::FileOriginSavedGifs(),
   75             !saved);
   76 
   77         auto &data = document->owner();
   78         if (saved) {
   79             data.stickers().savedGifsRef().remove(index);
   80             document->session().local().writeSavedGifs();
   81         }
   82         data.stickers().notifySavedGifsUpdated();
   83     }, saved ? &st::menuIconDelete : &st::menuIconGif);
   84 }
   85 
   86 GifsListWidget::GifsListWidget(
   87     QWidget *parent,
   88     not_null<Window::SessionController*> controller,
   89     PauseReason level)
   90 : GifsListWidget(parent, {
   91     .show = controller->uiShow(),
   92     .paused = Window::PausedIn(controller, level),
   93 }) {
   94 }
   95 
   96 GifsListWidget::GifsListWidget(
   97     QWidget *parent,
   98     GifsListDescriptor &&descriptor)
   99 : Inner(
  100     parent,
  101     st::defaultEmojiPan,
  102     descriptor.show,
  103     descriptor.paused)
  104 , _show(std::move(descriptor.show))
  105 , _api(&session().mtp())
  106 , _section(Section::Gifs)
  107 , _updateInlineItems([=] { updateInlineItems(); })
  108 , _mosaic(st::emojiPanWidth - st::inlineResultsLeft)
  109 , _previewTimer([=] { showPreview(); }) {
  110     setMouseTracking(true);
  111     setAttribute(Qt::WA_OpaquePaintEvent);
  112 
  113     setupSearch();
  114 
  115     _inlineRequestTimer.setSingleShot(true);
  116     connect(
  117         &_inlineRequestTimer,
  118         &QTimer::timeout,
  119         this,
  120         [=] { sendInlineRequest(); });
  121 
  122     session().data().stickers().savedGifsUpdated(
  123     ) | rpl::start_with_next([=] {
  124         refreshSavedGifs();
  125     }, lifetime());
  126 
  127     session().downloaderTaskFinished(
  128     ) | rpl::start_with_next([=] {
  129         updateInlineItems();
  130     }, lifetime());
  131 
  132     _show->pauseChanged(
  133     ) | rpl::start_with_next([=] {
  134         if (!paused()) {
  135             updateInlineItems();
  136         }
  137     }, lifetime());
  138 
  139     sizeValue(
  140     ) | rpl::start_with_next([=](const QSize &s) {
  141         _mosaic.setFullWidth(s.width());
  142     }, lifetime());
  143 
  144     _mosaic.setPadding(st::gifsPadding
  145         + QMargins(-st::emojiPanRadius, _search->height(), 0, 0));
  146     _mosaic.setRightSkip(st::inlineResultsSkip);
  147 }
  148 
  149 rpl::producer<FileChosen> GifsListWidget::fileChosen() const {
  150     return _fileChosen.events();
  151 }
  152 
  153 rpl::producer<PhotoChosen> GifsListWidget::photoChosen() const {
  154     return _photoChosen.events();
  155 }
  156 
  157 auto GifsListWidget::inlineResultChosen() const
  158 -> rpl::producer<InlineChosen> {
  159     return _inlineResultChosen.events();
  160 }
  161 
  162 object_ptr<TabbedSelector::InnerFooter> GifsListWidget::createFooter() {
  163     Expects(_footer == nullptr);
  164 
  165     using FooterDescriptor = StickersListFooter::Descriptor;
  166     auto result = object_ptr<StickersListFooter>(FooterDescriptor{
  167         .session = &session(),
  168         .paused = pausedMethod(),
  169         .parent = this,
  170         .st = &st(),
  171     });
  172     _footer = result;
  173     _chosenSetId = Data::Stickers::RecentSetId;
  174 
  175     GifSectionsValue(
  176         &session()
  177     ) | rpl::start_with_next([=](std::vector<GifSection> &&list) {
  178         _sections = std::move(list);
  179         refreshIcons();
  180     }, _footer->lifetime());
  181 
  182     _footer->setChosen(
  183     ) | rpl::start_with_next([=](uint64 setId) {
  184         if (_search) {
  185             _search->cancel();
  186         }
  187         _chosenSetId = setId;
  188         refreshIcons();
  189         const auto i = ranges::find(_sections, setId, [](GifSection value) {
  190             return value.document->id;
  191         });
  192         searchForGifs((i != end(_sections)) ? i->emoji->text() : QString());
  193     }, _footer->lifetime());
  194 
  195     return result;
  196 }
  197 
  198 void GifsListWidget::refreshIcons() {
  199     if (_footer) {
  200         _footer->refreshIcons(
  201             fillIcons(),
  202             _chosenSetId,
  203             nullptr,
  204             ValidateIconAnimations::None);
  205     }
  206 }
  207 
  208 std::vector<StickerIcon> GifsListWidget::fillIcons() {
  209     auto result = std::vector<StickerIcon>();
  210     result.reserve(_sections.size() + 1);
  211     result.emplace_back(Data::Stickers::RecentSetId);
  212     const auto side = StickersListFooter::IconFrameSize();
  213     for (const auto &section : _sections) {
  214         const auto s = section.document;
  215         const auto id = s->id;
  216         const auto size = s->hasThumbnail()
  217             ? QSize(
  218                 s->thumbnailLocation().width(),
  219                 s->thumbnailLocation().height())
  220             : QSize();
  221         const auto pix = size.scaled(side, side, Qt::KeepAspectRatio);
  222         const auto owner = &s->owner();
  223         const auto already = _fakeSets.find(id);
  224         const auto set = (already != end(_fakeSets))
  225             ? already
  226             : _fakeSets.emplace(
  227                 id,
  228                 std::make_unique<Data::StickersSet>(
  229                     owner,
  230                     id,
  231                     0,
  232                     0,
  233                     QString(),
  234                     QString(),
  235                     0,
  236                     Data::StickersSetFlag::Special,
  237                     0)).first;
  238         result.emplace_back(set->second.get(), s, pix.width(), pix.height());
  239     }
  240     return result;
  241 }
  242 
  243 void GifsListWidget::visibleTopBottomUpdated(
  244         int visibleTop,
  245         int visibleBottom) {
  246     const auto top = getVisibleTop();
  247     Inner::visibleTopBottomUpdated(visibleTop, visibleBottom);
  248     if (top != getVisibleTop()) {
  249         _lastScrolledAt = crl::now();
  250         update();
  251     }
  252     checkLoadMore();
  253 }
  254 
  255 void GifsListWidget::checkLoadMore() {
  256     auto visibleHeight = (getVisibleBottom() - getVisibleTop());
  257     if (getVisibleBottom() + visibleHeight > height()) {
  258         sendInlineRequest();
  259     }
  260 }
  261 
  262 int GifsListWidget::countDesiredHeight(int newWidth) {
  263     return _mosaic.countDesiredHeight(newWidth);
  264 }
  265 
  266 GifsListWidget::~GifsListWidget() {
  267     clearInlineRows(true);
  268     deleteUnusedGifLayouts();
  269     deleteUnusedInlineLayouts();
  270 }
  271 
  272 void GifsListWidget::cancelGifsSearch() {
  273     _search->setLoading(false);
  274     if (_inlineRequestId) {
  275         _api.request(_inlineRequestId).cancel();
  276         _inlineRequestId = 0;
  277     }
  278     _inlineRequestTimer.stop();
  279     _inlineQuery = _inlineNextQuery = _inlineNextOffset = QString();
  280     _inlineCache.clear();
  281     refreshInlineRows(nullptr, true);
  282 }
  283 
  284 void GifsListWidget::inlineResultsDone(const MTPmessages_BotResults &result) {
  285     _search->setLoading(false);
  286     _inlineRequestId = 0;
  287 
  288     auto it = _inlineCache.find(_inlineQuery);
  289     auto adding = (it != _inlineCache.cend());
  290     if (result.type() == mtpc_messages_botResults) {
  291         auto &d = result.c_messages_botResults();
  292         session().data().processUsers(d.vusers());
  293 
  294         auto &v = d.vresults().v;
  295         auto queryId = d.vquery_id().v;
  296 
  297         if (it == _inlineCache.cend()) {
  298             it = _inlineCache.emplace(
  299                 _inlineQuery,
  300                 std::make_unique<InlineCacheEntry>()).first;
  301         }
  302         const auto entry = it->second.get();
  303         entry->nextOffset = qs(d.vnext_offset().value_or_empty());
  304         if (const auto count = v.size()) {
  305             entry->results.reserve(entry->results.size() + count);
  306         }
  307         auto added = 0;
  308         for (const auto &res : v) {
  309             auto result = InlineBots::Result::Create(
  310                 &session(),
  311                 queryId,
  312                 res);
  313             if (result) {
  314                 ++added;
  315                 entry->results.push_back(std::move(result));
  316             }
  317         }
  318 
  319         if (!added) {
  320             entry->nextOffset = QString();
  321         }
  322     } else if (adding) {
  323         it->second->nextOffset = QString();
  324     }
  325 
  326     if (!showInlineRows(!adding)) {
  327         it->second->nextOffset = QString();
  328     }
  329     checkLoadMore();
  330 }
  331 
  332 void GifsListWidget::paintEvent(QPaintEvent *e) {
  333     Painter p(this);
  334     auto clip = e->rect();
  335     p.fillRect(clip, st::emojiPanBg);
  336 
  337     paintInlineItems(p, clip);
  338 }
  339 
  340 void GifsListWidget::paintInlineItems(Painter &p, QRect clip) {
  341     if (_mosaic.empty()) {
  342         p.setFont(st::normalFont);
  343         p.setPen(st::noContactsColor);
  344         auto text = _inlineQuery.isEmpty()
  345             ? tr::lng_gifs_no_saved(tr::now)
  346             : tr::lng_inline_bot_no_results(tr::now);
  347         p.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), text, style::al_center);
  348         return;
  349     }
  350     const auto gifPaused = paused();
  351     using namespace InlineBots::Layout;
  352     PaintContext context(crl::now(), false, gifPaused, false);
  353 
  354     auto paintItem = [&](not_null<const ItemBase*> item, QPoint point) {
  355         p.translate(point.x(), point.y());
  356         item->paint(
  357             p,
  358             clip.translated(-point),
  359             &context);
  360         p.translate(-point.x(), -point.y());
  361     };
  362     _mosaic.paint(std::move(paintItem), clip);
  363 }
  364 
  365 void GifsListWidget::mousePressEvent(QMouseEvent *e) {
  366     if (e->button() != Qt::LeftButton) {
  367         return;
  368     }
  369     _lastMousePos = e->globalPos();
  370     updateSelected();
  371 
  372     _pressed = _selected;
  373     ClickHandler::pressed();
  374     _previewTimer.callOnce(QApplication::startDragTime());
  375 }
  376 
  377 base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
  378         SendMenu::Type type) {
  379     if (_selected < 0 || _pressed >= 0) {
  380         return nullptr;
  381     }
  382 
  383     auto menu = base::make_unique_q<Ui::PopupMenu>(
  384         this,
  385         st::popupMenuWithIcons);
  386     const auto send = [=, selected = _selected](Api::SendOptions options) {
  387         selectInlineResult(selected, options, true);
  388     };
  389     SendMenu::FillSendMenu(
  390         menu,
  391         type,
  392         SendMenu::DefaultSilentCallback(send),
  393         SendMenu::DefaultScheduleCallback(this, type, send),
  394         SendMenu::DefaultWhenOnlineCallback(send));
  395 
  396     if (const auto item = _mosaic.maybeItemAt(_selected)) {
  397         const auto document = item->getDocument()
  398             ? item->getDocument() // Saved GIF.
  399             : item->getPreviewDocument(); // Searched GIF.
  400         if (document) {
  401             auto callback = [&](
  402                     const QString &text,
  403                     Fn<void()> &&done,
  404                     const style::icon *icon) {
  405                 menu->addAction(text, std::move(done), icon);
  406             };
  407             AddGifAction(std::move(callback), _show, document);
  408         }
  409     }
  410     return menu;
  411 }
  412 
  413 void GifsListWidget::mouseReleaseEvent(QMouseEvent *e) {
  414     _previewTimer.cancel();
  415 
  416     auto pressed = std::exchange(_pressed, -1);
  417     auto activated = ClickHandler::unpressed();
  418 
  419     if (_previewShown) {
  420         _previewShown = false;
  421         return;
  422     }
  423 
  424     _lastMousePos = e->globalPos();
  425     updateSelected();
  426 
  427     if (_selected < 0 || _selected != pressed || !activated) {
  428         return;
  429     }
  430 
  431     if (dynamic_cast<InlineBots::Layout::SendClickHandler*>(activated.get())) {
  432         selectInlineResult(_selected, {});
  433     } else {
  434         ActivateClickHandler(window(), activated, {
  435             e->button(),
  436             QVariant::fromValue(ClickHandlerContext{
  437                 .show = _show,
  438             })
  439         });
  440     }
  441 }
  442 
  443 void GifsListWidget::selectInlineResult(
  444         int index,
  445         Api::SendOptions options,
  446         bool forceSend) {
  447     const auto item = _mosaic.maybeItemAt(index);
  448     if (!item) {
  449         return;
  450     }
  451 
  452     const auto messageSendingFrom = [&] {
  453         if (options.scheduled) {
  454             return Ui::MessageSendingAnimationFrom();
  455         }
  456         const auto rect = item->innerContentRect().translated(
  457             _mosaic.findRect(index).topLeft());
  458         return Ui::MessageSendingAnimationFrom{
  459             .type = Ui::MessageSendingAnimationFrom::Type::Gif,
  460             .localId = session().data().nextLocalMessageId(),
  461             .globalStartGeometry = mapToGlobal(rect),
  462             .crop = true,
  463         };
  464     };
  465 
  466     forceSend |= base::IsCtrlPressed();
  467     if (const auto photo = item->getPhoto()) {
  468         using Data::PhotoSize;
  469         const auto media = photo->activeMediaView();
  470         if (forceSend
  471             || (media && media->image(PhotoSize::Thumbnail))
  472             || (media && media->image(PhotoSize::Large))) {
  473             _photoChosen.fire({
  474                 .photo = photo,
  475                 .options = options });
  476         } else if (!photo->loading(PhotoSize::Thumbnail)) {
  477             photo->load(PhotoSize::Thumbnail, Data::FileOrigin());
  478         }
  479     } else if (const auto document = item->getDocument()) {
  480         const auto media = document->activeMediaView();
  481         const auto preview = Data::VideoPreviewState(media.get());
  482         if (forceSend || (media && preview.loaded())) {
  483             _fileChosen.fire({
  484                 .document = document,
  485                 .options = options,
  486                 .messageSendingFrom = messageSendingFrom(),
  487             });
  488         } else if (!preview.usingThumbnail()) {
  489             if (preview.loading()) {
  490                 document->cancel();
  491             } else {
  492                 document->save(
  493                     document->stickerOrGifOrigin(),
  494                     QString());
  495             }
  496         }
  497     } else if (const auto inlineResult = item->getResult()) {
  498         if (inlineResult->onChoose(item)) {
  499             options.hideViaBot = true;
  500             _inlineResultChosen.fire({
  501                 .result = inlineResult,
  502                 .bot = _searchBot,
  503                 .options = options,
  504                 .messageSendingFrom = messageSendingFrom(),
  505             });
  506         }
  507     }
  508 }
  509 
  510 void GifsListWidget::mouseMoveEvent(QMouseEvent *e) {
  511     _lastMousePos = e->globalPos();
  512     updateSelected();
  513 }
  514 
  515 void GifsListWidget::leaveEventHook(QEvent *e) {
  516     clearSelection();
  517 }
  518 
  519 void GifsListWidget::leaveToChildEvent(QEvent *e, QWidget *child) {
  520     clearSelection();
  521 }
  522 
  523 void GifsListWidget::enterFromChildEvent(QEvent *e, QWidget *child) {
  524     _lastMousePos = QCursor::pos();
  525     updateSelected();
  526 }
  527 
  528 void GifsListWidget::clearSelection() {
  529     if (_selected >= 0) {
  530         ClickHandler::clearActive(_mosaic.itemAt(_selected));
  531         setCursor(style::cur_default);
  532     }
  533     _selected = _pressed = -1;
  534     repaintItems();
  535 }
  536 
  537 TabbedSelector::InnerFooter *GifsListWidget::getFooter() const {
  538     return _footer;
  539 }
  540 
  541 void GifsListWidget::processHideFinished() {
  542     clearSelection();
  543     clearHeavyData();
  544     if (_footer) {
  545         _footer->clearHeavyData();
  546     }
  547 }
  548 
  549 void GifsListWidget::processPanelHideFinished() {
  550     clearHeavyData();
  551     if (_footer) {
  552         _footer->clearHeavyData();
  553     }
  554 }
  555 
  556 void GifsListWidget::clearHeavyData() {
  557     // Preserve panel state through visibility toggles.
  558     //clearInlineRows(false);
  559     for (const auto &[document, layout] : _gifLayouts) {
  560         layout->unloadHeavyPart();
  561     }
  562     for (const auto &[document, layout] : _inlineLayouts) {
  563         layout->unloadHeavyPart();
  564     }
  565 }
  566 
  567 void GifsListWidget::refreshSavedGifs() {
  568     if (_section == Section::Gifs) {
  569         clearInlineRows(false);
  570 
  571         const auto &saved = session().data().stickers().savedGifs();
  572         if (!saved.isEmpty()) {
  573             const auto layouts = ranges::views::all(
  574                 saved
  575             ) | ranges::views::transform([&](not_null<DocumentData*> gif) {
  576                 return layoutPrepareSavedGif(gif);
  577             }) | ranges::views::filter([](const LayoutItem *item) {
  578                 return item != nullptr;
  579             }) | ranges::to<std::vector<not_null<LayoutItem*>>>;
  580 
  581             _mosaic.addItems(layouts);
  582         }
  583         deleteUnusedGifLayouts();
  584 
  585         resizeToWidth(width());
  586         repaintItems();
  587     }
  588 
  589     if (isVisible()) {
  590         updateSelected();
  591     } else {
  592         preloadImages();
  593     }
  594 }
  595 
  596 void GifsListWidget::clearInlineRows(bool resultsDeleted) {
  597     if (resultsDeleted) {
  598         _selected = _pressed = -1;
  599     } else {
  600         clearSelection();
  601     }
  602     _mosaic.clearRows(resultsDeleted);
  603 }
  604 
  605 GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareSavedGif(
  606         not_null<DocumentData*> document) {
  607     auto it = _gifLayouts.find(document);
  608     if (it == _gifLayouts.cend()) {
  609         if (auto layout = LayoutItem::createLayoutGif(this, document)) {
  610             it = _gifLayouts.emplace(document, std::move(layout)).first;
  611             it->second->initDimensions();
  612         } else {
  613             return nullptr;
  614         }
  615     }
  616     if (!it->second->maxWidth()) return nullptr;
  617 
  618     return it->second.get();
  619 }
  620 
  621 GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareInlineResult(
  622         not_null<InlineResult*> result) {
  623     auto it = _inlineLayouts.find(result);
  624     if (it == _inlineLayouts.cend()) {
  625         if (auto layout = LayoutItem::createLayout(
  626                 this,
  627                 result,
  628                 _inlineWithThumb)) {
  629             it = _inlineLayouts.emplace(result, std::move(layout)).first;
  630             it->second->initDimensions();
  631         } else {
  632             return nullptr;
  633         }
  634     }
  635     if (!it->second->maxWidth()) return nullptr;
  636 
  637     return it->second.get();
  638 }
  639 
  640 void GifsListWidget::deleteUnusedGifLayouts() {
  641     if (_mosaic.empty() || _section != Section::Gifs) { // delete all
  642         _gifLayouts.clear();
  643     } else {
  644         for (auto i = _gifLayouts.begin(); i != _gifLayouts.cend();) {
  645             if (i->second->position() < 0) {
  646                 i = _gifLayouts.erase(i);
  647             } else {
  648                 ++i;
  649             }
  650         }
  651     }
  652 }
  653 
  654 void GifsListWidget::deleteUnusedInlineLayouts() {
  655     if (_mosaic.empty() || _section == Section::Gifs) { // delete all
  656         _inlineLayouts.clear();
  657     } else {
  658         for (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {
  659             if (i->second->position() < 0) {
  660                 i = _inlineLayouts.erase(i);
  661             } else {
  662                 ++i;
  663             }
  664         }
  665     }
  666 }
  667 
  668 void GifsListWidget::preloadImages() {
  669     _mosaic.forEach([](not_null<const LayoutItem*> item) {
  670         item->preload();
  671     });
  672 }
  673 
  674 void GifsListWidget::switchToSavedGifs() {
  675     clearInlineRows(false);
  676     _section = Section::Gifs;
  677     refreshSavedGifs();
  678     scrollTo(0);
  679 }
  680 
  681 int GifsListWidget::refreshInlineRows(const InlineCacheEntry *entry, bool resultsDeleted) {
  682     if (!entry) {
  683         if (resultsDeleted) {
  684             clearInlineRows(true);
  685             deleteUnusedInlineLayouts();
  686         }
  687         switchToSavedGifs();
  688         return 0;
  689     }
  690 
  691     clearSelection();
  692 
  693     _section = Section::Inlines;
  694     const auto count = int(entry->results.size());
  695     const auto from = validateExistingInlineRows(entry->results);
  696     auto added = 0;
  697     if (count) {
  698         const auto resultLayouts = entry->results | ranges::views::slice(
  699             from,
  700             count
  701         ) | ranges::views::transform([&](
  702                 const std::unique_ptr<InlineBots::Result> &r) {
  703             return layoutPrepareInlineResult(r.get());
  704         }) | ranges::views::filter([](const LayoutItem *item) {
  705             return item != nullptr;
  706         }) | ranges::to<std::vector<not_null<LayoutItem*>>>;
  707 
  708         _mosaic.addItems(resultLayouts);
  709         added = resultLayouts.size();
  710         preloadImages();
  711     }
  712 
  713     resizeToWidth(width());
  714     repaintItems();
  715 
  716     _lastMousePos = QCursor::pos();
  717     updateSelected();
  718 
  719     return added;
  720 }
  721 
  722 int GifsListWidget::validateExistingInlineRows(const InlineResults &results) {
  723     const auto until = _mosaic.validateExistingRows([&](
  724             not_null<const LayoutItem*> item,
  725             int untilIndex) {
  726         return item->getResult() != results[untilIndex].get();
  727     }, results.size());
  728 
  729     if (_mosaic.empty()) {
  730         _inlineWithThumb = false;
  731         for (int i = until; i < results.size(); ++i) {
  732             if (results.at(i)->hasThumbDisplay()) {
  733                 _inlineWithThumb = true;
  734                 break;
  735             }
  736         }
  737     }
  738     return until;
  739 }
  740 
  741 void GifsListWidget::inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) {
  742     if (_selected < 0 || !isVisible()) {
  743         return;
  744     }
  745 
  746     if (const auto item = _mosaic.maybeItemAt(_selected)) {
  747         if (layout == item) {
  748             updateSelected();
  749         }
  750     }
  751 }
  752 
  753 void GifsListWidget::inlineItemRepaint(
  754         const InlineBots::Layout::ItemBase *layout) {
  755     updateInlineItems();
  756 }
  757 
  758 bool GifsListWidget::inlineItemVisible(
  759         const InlineBots::Layout::ItemBase *layout) {
  760     auto position = layout->position();
  761     if (position < 0 || !isVisible()) {
  762         return false;
  763     }
  764 
  765     const auto &[row, column] = Layout::IndexToPosition(position);
  766     auto top = 0;
  767     for (auto i = 0; i != row; ++i) {
  768         top += _mosaic.rowHeightAt(i);
  769     }
  770 
  771     return (top < getVisibleBottom())
  772         && (top + _mosaic.itemAt(row, column)->height() > getVisibleTop());
  773 }
  774 
  775 Data::FileOrigin GifsListWidget::inlineItemFileOrigin() {
  776     return _inlineQuery.isEmpty()
  777         ? Data::FileOriginSavedGifs()
  778         : Data::FileOrigin();
  779 }
  780 
  781 void GifsListWidget::afterShown() {
  782     if (_search) {
  783         _search->stealFocus();
  784     }
  785 }
  786 
  787 void GifsListWidget::beforeHiding() {
  788     if (_search) {
  789         _search->returnFocus();
  790     }
  791 }
  792 
  793 bool GifsListWidget::refreshInlineRows(int32 *added) {
  794     auto it = _inlineCache.find(_inlineQuery);
  795     const InlineCacheEntry *entry = nullptr;
  796     if (it != _inlineCache.cend()) {
  797         entry = it->second.get();
  798         _inlineNextOffset = it->second->nextOffset;
  799     }
  800     auto result = refreshInlineRows(entry, false);
  801     if (added) *added = result;
  802     return (entry != nullptr);
  803 }
  804 
  805 void GifsListWidget::setupSearch() {
  806     const auto session = &_show->session();
  807     _search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {
  808         const auto accumulated = ranges::accumulate(query, QString(), [](
  809                 QString a,
  810                 QString b) {
  811             return a.isEmpty() ? b : (a + ' ' + b);
  812         });
  813         _chosenSetId = accumulated.isEmpty()
  814             ? Data::Stickers::RecentSetId
  815             : SearchEmojiSectionSetId();
  816         refreshIcons();
  817         searchForGifs(accumulated);
  818     }, session);
  819 }
  820 
  821 int32 GifsListWidget::showInlineRows(bool newResults) {
  822     auto added = 0;
  823     refreshInlineRows(&added);
  824     if (newResults) {
  825         scrollTo(0);
  826     }
  827     return added;
  828 }
  829 
  830 void GifsListWidget::searchForGifs(const QString &query) {
  831     if (query.isEmpty()) {
  832         cancelGifsSearch();
  833         return;
  834     }
  835 
  836     if (_inlineQuery != query) {
  837         _search->setLoading(false);
  838         if (_inlineRequestId) {
  839             _api.request(_inlineRequestId).cancel();
  840             _inlineRequestId = 0;
  841         }
  842         if (_inlineCache.find(query) != _inlineCache.cend()) {
  843             _inlineRequestTimer.stop();
  844             _inlineQuery = _inlineNextQuery = query;
  845             showInlineRows(true);
  846         } else {
  847             _inlineNextQuery = query;
  848             _inlineRequestTimer.start(kSearchRequestDelay);
  849         }
  850     }
  851 
  852     if (!_searchBot && !_searchBotRequestId) {
  853         auto username = kSearchBotUsername.utf16();
  854         _searchBotRequestId = _api.request(MTPcontacts_ResolveUsername(
  855             MTP_string(username)
  856         )).done([=](const MTPcontacts_ResolvedPeer &result) {
  857             Expects(result.type() == mtpc_contacts_resolvedPeer);
  858 
  859             auto &data = result.c_contacts_resolvedPeer();
  860             session().data().processUsers(data.vusers());
  861             session().data().processChats(data.vchats());
  862             const auto peer = session().data().peerLoaded(
  863                 peerFromMTP(data.vpeer()));
  864             if (const auto user = peer ? peer->asUser() : nullptr) {
  865                 _searchBot = user;
  866             }
  867         }).send();
  868     }
  869 }
  870 
  871 void GifsListWidget::cancelled() {
  872     _cancelled.fire({});
  873 }
  874 
  875 rpl::producer<> GifsListWidget::cancelRequests() const {
  876     return _cancelled.events();
  877 }
  878 
  879 void GifsListWidget::sendInlineRequest() {
  880     if (_inlineRequestId || !_inlineQueryPeer || _inlineNextQuery.isEmpty()) {
  881         return;
  882     }
  883 
  884     if (!_searchBot) {
  885         // Wait for the bot being resolved.
  886         _search->setLoading(true);
  887         _inlineRequestTimer.start(kSearchRequestDelay);
  888         return;
  889     }
  890     _inlineRequestTimer.stop();
  891     _inlineQuery = _inlineNextQuery;
  892 
  893     auto nextOffset = QString();
  894     auto it = _inlineCache.find(_inlineQuery);
  895     if (it != _inlineCache.cend()) {
  896         nextOffset = it->second->nextOffset;
  897         if (nextOffset.isEmpty()) {
  898             _search->setLoading(false);
  899             return;
  900         }
  901     }
  902 
  903     _search->setLoading(true);
  904     _inlineRequestId = _api.request(MTPmessages_GetInlineBotResults(
  905         MTP_flags(0),
  906         _searchBot->inputUser,
  907         _inlineQueryPeer->input,
  908         MTPInputGeoPoint(),
  909         MTP_string(_inlineQuery),
  910         MTP_string(nextOffset)
  911     )).done([this](const MTPmessages_BotResults &result) {
  912         inlineResultsDone(result);
  913     }).fail([this] {
  914         // show error?
  915         _search->setLoading(false);
  916         _inlineRequestId = 0;
  917     }).handleAllErrors().send();
  918 }
  919 
  920 void GifsListWidget::refreshRecent() {
  921     if (_section == Section::Gifs) {
  922         refreshSavedGifs();
  923     }
  924 }
  925 
  926 void GifsListWidget::updateSelected() {
  927     if (_pressed >= 0 && !_previewShown) {
  928         return;
  929     }
  930 
  931     const auto p = mapFromGlobal(_lastMousePos);
  932     const auto sx = rtl() ? (width() - p.x()) : p.x();
  933     const auto sy = p.y();
  934     const auto &[index, exact, relative] = _mosaic.findByPoint({ sx, sy });
  935     const auto selected = exact ? index : -1;
  936     const auto item = exact ? _mosaic.itemAt(selected).get() : nullptr;
  937     const auto link = exact ? item->getState(relative, {}).link : nullptr;
  938 
  939     if (_selected != selected) {
  940         if (const auto s = _mosaic.maybeItemAt(_selected)) {
  941             s->update();
  942         }
  943         _selected = selected;
  944         if (item) {
  945             item->update();
  946         }
  947         if (_previewShown && _selected >= 0 && _pressed != _selected) {
  948             _pressed = _selected;
  949             if (item) {
  950                 if (const auto preview = item->getPreviewDocument()) {
  951                     _show->showMediaPreview(
  952                         Data::FileOriginSavedGifs(),
  953                         preview);
  954                 } else if (const auto preview = item->getPreviewPhoto()) {
  955                     _show->showMediaPreview(Data::FileOrigin(), preview);
  956                 }
  957             }
  958         }
  959     }
  960     if (ClickHandler::setActive(link, item)) {
  961         setCursor(link ? style::cur_pointer : style::cur_default);
  962     }
  963 }
  964 
  965 void GifsListWidget::showPreview() {
  966     if (_pressed < 0) {
  967         return;
  968     }
  969     if (const auto layout = _mosaic.maybeItemAt(_pressed)) {
  970         if (const auto previewDocument = layout->getPreviewDocument()) {
  971             _previewShown = _show->showMediaPreview(
  972                 Data::FileOriginSavedGifs(),
  973                 previewDocument);
  974         } else if (const auto previewPhoto = layout->getPreviewPhoto()) {
  975             _previewShown = _show->showMediaPreview(
  976                 Data::FileOrigin(),
  977                 previewPhoto);
  978         }
  979     }
  980 }
  981 
  982 void GifsListWidget::updateInlineItems() {
  983     const auto now = crl::now();
  984 
  985     const auto delay = std::max(
  986         _lastScrolledAt + kMinAfterScrollDelay - now,
  987         _lastUpdatedAt + kMinRepaintDelay - now);
  988     if (delay <= 0) {
  989         repaintItems(now);
  990     } else if (!_updateInlineItems.isActive()
  991         || _updateInlineItems.remainingTime() > kMinRepaintDelay) {
  992         _updateInlineItems.callOnce(std::max(delay, kMinRepaintDelay));
  993     }
  994 }
  995 
  996 void GifsListWidget::repaintItems(crl::time now) {
  997     _lastUpdatedAt = now ? now : crl::now();
  998     update();
  999 }
 1000 
 1001 } // namespace ChatHelpers