init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,475 @@
/*
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 "api/api_common.h"
#include "base/flags.h"
#include "base/timer.h"
#include "base/weak_ptr.h"
#include "dialogs/dialogs_key.h"
#include "mtproto/sender.h"
#include "ui/chat/attach/attach_bot_webview.h"
#include "ui/rp_widget.h"
namespace Data {
class Thread;
} // namespace Data
namespace Ui {
class Show;
class GenericBox;
class DropdownMenu;
} // namespace Ui
namespace Ui::BotWebView {
class Panel;
struct DownloadsEntry;
} // namespace Ui::BotWebView
namespace Main {
class Session;
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Payments {
struct NonPanelPaymentForm;
enum class CheckoutResult;
} // namespace Payments
namespace InlineBots {
class WebViewInstance;
class Downloads;
class Storage;
enum class PeerType : uint8 {
SameBot = 0x01,
Bot = 0x02,
User = 0x04,
Group = 0x08,
Broadcast = 0x10,
};
using PeerTypes = base::flags<PeerType>;
[[nodiscard]] bool PeerMatchesTypes(
not_null<PeerData*> peer,
not_null<UserData*> bot,
PeerTypes types);
[[nodiscard]] PeerTypes ParseChooseTypes(QStringView choose);
struct AttachWebViewBot {
not_null<UserData*> user;
DocumentData *icon = nullptr;
std::shared_ptr<Data::DocumentMedia> media;
QString name;
PeerTypes types = 0;
bool inactive : 1 = false;
bool inMainMenu : 1 = false;
bool inAttachMenu : 1 = false;
bool disclaimerRequired : 1 = false;
bool requestWriteAccess : 1 = false;
};
struct WebViewSourceButton {
bool simple = false;
friend inline bool operator==(
WebViewSourceButton,
WebViewSourceButton) = default;
};
struct WebViewSourceSwitch {
friend inline bool operator==(
const WebViewSourceSwitch &,
const WebViewSourceSwitch &) = default;
};
struct WebViewSourceLinkApp { // t.me/botusername/appname
base::weak_ptr<WebViewInstance> from;
QString appname;
QString token;
friend inline bool operator==(
const WebViewSourceLinkApp &,
const WebViewSourceLinkApp &) = default;
};
struct WebViewSourceLinkAttachMenu { // ?startattach
base::weak_ptr<WebViewInstance> from;
base::weak_ptr<Data::Thread> thread;
PeerTypes choose;
QString token;
friend inline bool operator==(
const WebViewSourceLinkAttachMenu &,
const WebViewSourceLinkAttachMenu &) = default;
};
struct WebViewSourceLinkBotProfile { // t.me/botusername?startapp
base::weak_ptr<WebViewInstance> from;
QString token;
bool compact = false;
friend inline bool operator==(
const WebViewSourceLinkBotProfile &,
const WebViewSourceLinkBotProfile &) = default;
};
struct WebViewSourceMainMenu {
friend inline bool operator==(
WebViewSourceMainMenu,
WebViewSourceMainMenu) = default;
};
struct WebViewSourceAttachMenu {
base::weak_ptr<Data::Thread> thread;
friend inline bool operator==(
const WebViewSourceAttachMenu &,
const WebViewSourceAttachMenu &) = default;
};
struct WebViewSourceBotMenu {
friend inline bool operator==(
WebViewSourceBotMenu,
WebViewSourceBotMenu) = default;
};
struct WebViewSourceGame {
FullMsgId messageId;
QString title;
friend inline bool operator==(
WebViewSourceGame,
WebViewSourceGame) = default;
};
struct WebViewSourceBotProfile {
friend inline bool operator==(
WebViewSourceBotProfile,
WebViewSourceBotProfile) = default;
};
struct WebViewSourceAgeVerification {
Fn<void(int)> done;
friend inline bool operator==(
WebViewSourceAgeVerification,
WebViewSourceAgeVerification) {
return true;
}
};
struct WebViewSource : std::variant<
WebViewSourceButton,
WebViewSourceSwitch,
WebViewSourceLinkApp,
WebViewSourceLinkAttachMenu,
WebViewSourceLinkBotProfile,
WebViewSourceMainMenu,
WebViewSourceAttachMenu,
WebViewSourceBotMenu,
WebViewSourceGame,
WebViewSourceBotProfile,
WebViewSourceAgeVerification> {
using variant::variant;
};
struct WebViewButton {
QString text;
QString startCommand;
QByteArray url;
bool fromAttachMenu = false;
bool fromMainMenu = false;
bool fromSwitch = false;
};
struct WebViewContext {
base::weak_ptr<Window::SessionController> controller;
Dialogs::EntryState dialogsEntryState;
std::optional<Api::SendAction> action;
bool fullscreen = false;
bool maySkipConfirmation = false;
};
struct WebViewDescriptor {
not_null<UserData*> bot;
std::shared_ptr<Ui::Show> parentShow;
WebViewContext context;
WebViewButton button;
WebViewSource source;
};
class WebViewInstance final
: public base::has_weak_ptr
, public Ui::BotWebView::Delegate {
public:
explicit WebViewInstance(WebViewDescriptor &&descriptor);
~WebViewInstance();
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] not_null<UserData*> bot() const;
[[nodiscard]] WebViewSource source() const;
void activate();
void close();
[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
private:
void resolve();
void requestFullBot();
bool openAppFromBotMenuLink();
void requestButton();
void requestSimple();
void requestMain();
void requestApp(bool allowWrite);
void requestWithMainMenuDisclaimer();
void requestWithMenuAdd();
void maybeChooseAndRequestButton(PeerTypes supported);
enum class ConfirmType : uchar {
Always,
Once,
None,
};
void resolveApp(
const QString &appname,
const QString &startparam,
ConfirmType confirmType);
void confirmOpen(Fn<void()> done);
void confirmAppOpen(
bool writeAccess,
Fn<void(bool allowWrite)> done,
bool forceConfirmation);
struct ShowArgs {
QString url;
QString title;
uint64 queryId = 0;
bool fullscreen = false;
};
void show(ShowArgs &&args);
void showGame();
void started(uint64 queryId);
auto nonPanelPaymentFormFactory(
Fn<void(Payments::CheckoutResult)> reactivate)
-> Fn<void(Payments::NonPanelPaymentForm)>;
Webview::ThemeParams botThemeParams() override;
auto botDownloads(bool forceCheck = false)
-> const std::vector<Ui::BotWebView::DownloadsEntry> & override;
void botDownloadsAction(
uint32 id,
Ui::BotWebView::DownloadsAction type) override;
bool botHandleLocalUri(QString uri, bool keepOpen) override;
void botHandleInvoice(QString slug) override;
void botHandleMenuButton(Ui::BotWebView::MenuButton button) override;
bool botValidateExternalLink(QString uri) override;
void botOpenIvLink(QString uri) override;
void botSendData(QByteArray data) override;
void botSwitchInlineQuery(
std::vector<QString> chatTypes,
QString query) override;
void botCheckWriteAccess(Fn<void(bool allowed)> callback) override;
void botAllowWriteAccess(Fn<void(bool allowed)> callback) override;
bool botStorageWrite(QString key, std::optional<QString> value) override;
std::optional<QString> botStorageRead(QString key) override;
void botStorageClear() override;
void botRequestEmojiStatusAccess(
Fn<void(bool allowed)> callback) override;
void botSharePhone(Fn<void(bool shared)> callback) override;
void botInvokeCustomMethod(
Ui::BotWebView::CustomMethodRequest request) override;
void botSendPreparedMessage(
Ui::BotWebView::SendPreparedMessageRequest request) override;
void botSetEmojiStatus(
Ui::BotWebView::SetEmojiStatusRequest request) override;
void botDownloadFile(
Ui::BotWebView::DownloadFileRequest request) override;
void botVerifyAge(int age) override;
void botOpenPrivacyPolicy() override;
void botClose() override;
const std::shared_ptr<Ui::Show> _parentShow;
const not_null<Main::Session*> _session;
const not_null<UserData*> _bot;
const WebViewContext _context;
const WebViewButton _button;
const WebViewSource _source;
std::optional<ShowArgs> _botFullWaitingArgs;
BotAppData *_app = nullptr;
QString _appStartParam;
bool _dataSent = false;
bool _confirmingDownload = false;
mtpRequestId _requestId = 0;
mtpRequestId _prolongId = 0;
QString _panelUrl;
std::unique_ptr<Ui::BotWebView::Panel> _panel;
rpl::lifetime _lifetime;
static base::weak_ptr<WebViewInstance> PendingActivation;
};
class AttachWebView final : public base::has_weak_ptr {
public:
explicit AttachWebView(not_null<Main::Session*> session);
~AttachWebView();
[[nodiscard]] Downloads &downloads() const {
return *_downloads;
}
[[nodiscard]] Storage &storage() const {
return *_storage;
}
void open(WebViewDescriptor &&descriptor);
void openByUsername(
not_null<Window::SessionController*> controller,
const Api::SendAction &action,
const QString &botUsername,
const QString &startCommand,
bool fullscreen);
void cancel();
void requestBots(Fn<void()> callback = nullptr);
[[nodiscard]] const std::vector<AttachWebViewBot> &attachBots() const {
return _attachBots;
}
[[nodiscard]] rpl::producer<> attachBotsUpdates() const {
return _attachBotsUpdates.events();
}
void notifyBotIconLoaded() {
_attachBotsUpdates.fire({});
}
[[nodiscard]] bool disclaimerAccepted(
const AttachWebViewBot &bot) const;
[[nodiscard]] bool showMainMenuNewBadge(
const AttachWebViewBot &bot) const;
void removeFromMenu(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot);
enum class AddToMenuResult {
AlreadyInMenu,
Added,
Unsupported,
Cancelled,
};
void requestAddToMenu(
not_null<UserData*> bot,
Fn<void(AddToMenuResult, PeerTypes supported)> done);
void acceptMainMenuDisclaimer(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
Fn<void(AddToMenuResult, PeerTypes supported)> done);
void close(not_null<WebViewInstance*> instance);
void closeAll();
void loadPopularAppBots();
[[nodiscard]] auto popularAppBots() const
-> const std::vector<not_null<UserData*>> &;
[[nodiscard]] rpl::producer<> popularAppBotsLoaded() const;
private:
void resolveUsername(
std::shared_ptr<Ui::Show> show,
Fn<void(not_null<PeerData*>)> done);
enum class ToggledState {
Removed,
Added,
AllowedToWrite,
};
void toggleInMenu(
not_null<UserData*> bot,
ToggledState state,
Fn<void(bool success)> callback = nullptr);
void confirmAddToMenu(
AttachWebViewBot bot,
Fn<void(bool added)> callback = nullptr);
const not_null<Main::Session*> _session;
const std::unique_ptr<Downloads> _downloads;
const std::unique_ptr<Storage> _storage;
base::Timer _refreshTimer;
QString _botUsername;
QString _startCommand;
bool _fullScreenRequested = false;
mtpRequestId _requestId = 0;
uint64 _botsHash = 0;
mtpRequestId _botsRequestId = 0;
std::vector<Fn<void()>> _botsRequestCallbacks;
struct AddToMenuProcess {
mtpRequestId requestId = 0;
std::vector<Fn<void(AddToMenuResult, PeerTypes supported)>> done;
};
base::flat_map<not_null<UserData*>, AddToMenuProcess> _addToMenu;
std::vector<AttachWebViewBot> _attachBots;
rpl::event_stream<> _attachBotsUpdates;
base::flat_set<not_null<UserData*>> _disclaimerAccepted;
std::vector<std::unique_ptr<WebViewInstance>> _instances;
std::vector<not_null<UserData*>> _popularAppBots;
mtpRequestId _popularAppBotsRequestId = 0;
rpl::variable<bool> _popularAppBotsLoaded = false;
};
[[nodiscard]] std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
not_null<QWidget*> parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Fn<Api::SendAction()> actionFactory,
Fn<void(bool)> attach);
class MenuBotIcon final : public Ui::RpWidget {
public:
MenuBotIcon(
QWidget *parent,
std::shared_ptr<Data::DocumentMedia> media);
private:
void paintEvent(QPaintEvent *e) override;
void validate();
std::shared_ptr<Data::DocumentMedia> _media;
QImage _image;
QImage _mask;
};
} // namespace InlineBots

View File

@@ -0,0 +1,240 @@
/*
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 "inline_bots/inline_bot_confirm_prepared.h"
#include "boxes/peers/edit_peer_invite_link.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/admin_log/history_admin_log_item.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/layers/generic_box.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace InlineBots {
namespace {
using namespace HistoryView;
class PreviewDelegate final : public DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
};
class PreviewWrap final : public Ui::RpWidget {
public:
PreviewWrap(not_null<QWidget*> parent, not_null<HistoryItem*> item);
~PreviewWrap();
private:
void paintEvent(QPaintEvent *e) override;
void resizeTo(int width);
void prepare(not_null<HistoryItem*> item);
const not_null<History*> _history;
const std::unique_ptr<Ui::ChatTheme> _theme;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<PreviewDelegate> _delegate;
AdminLog::OwnedItem _item;
QPoint _position;
};
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, update)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto PreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context PreviewDelegate::elementContext() {
return Context::History;
}
PreviewWrap::PreviewWrap(
not_null<QWidget*> parent,
not_null<HistoryItem*> item)
: RpWidget(parent)
, _history(item->history())
, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
, _style(std::make_unique<Ui::ChatStyle>(
_history->session().colorIndicesValue()))
, _delegate(std::make_unique<PreviewDelegate>(
parent,
_style.get(),
[=] { update(); }))
, _position(0, st::msgMargin.bottom()) {
_style->apply(_theme.get());
using namespace HistoryView;
_history->owner().viewRepaintRequest(
) | rpl::on_next([=](not_null<const Element*> view) {
if (view == _item.get()) {
update();
}
}, lifetime());
_history->session().downloaderTaskFinished() | rpl::on_next([=] {
update();
}, lifetime());
prepare(item);
}
PreviewWrap::~PreviewWrap() {
_item = {};
}
void PreviewWrap::prepare(not_null<HistoryItem*> item) {
_item = AdminLog::OwnedItem(_delegate.get(), item);
if (width() >= st::msgMinWidth) {
resizeTo(width());
}
widthValue(
) | rpl::filter([=](int width) {
return width >= st::msgMinWidth;
}) | rpl::on_next([=](int width) {
resizeTo(width);
}, lifetime());
}
void PreviewWrap::resizeTo(int width) {
const auto height = _position.y()
+ _item->resizeGetHeight(width)
+ _position.y()
+ st::msgServiceMargin.top()
+ st::msgServiceGiftBoxTopSkip
- st::msgServiceMargin.bottom();
resize(width, height);
}
void PreviewWrap::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e->rect();
if (!clip.isEmpty()) {
p.setClipRect(clip);
Window::SectionWidget::PaintBackground(
p,
_theme.get(),
QSize(width(), window()->height()),
clip);
}
auto context = _theme->preparePaintContext(
_style.get(),
rect(),
e->rect(),
!window()->isActiveWindow());
p.translate(_position);
_item->draw(p, context);
}
} // namespace
void PreparedPreviewBox(
not_null<Ui::GenericBox*> box,
not_null<HistoryItem*> item,
rpl::producer<not_null<Data::Thread*>> recipient,
Fn<void()> choose,
Fn<void(not_null<Data::Thread*>)> send) {
box->setTitle(tr::lng_bot_share_prepared_title());
const auto container = box->verticalLayout();
container->add(object_ptr<PreviewWrap>(container, item));
const auto bot = item->viaBot();
const auto name = bot ? bot->name() : u"Bot"_q;
const auto info = container->add(
object_ptr<Ui::SlideWrap<Ui::DividerLabel>>(
container,
object_ptr<Ui::DividerLabel>(
container,
object_ptr<Ui::FlatLabel>(
container,
tr::lng_bot_share_prepared_about(lt_bot, rpl::single(name)),
st::boxDividerLabel),
st::defaultBoxDividerLabelPadding)));
const auto row = container->add(object_ptr<Ui::VerticalLayout>(
container));
const auto reset = [=] {
info->show(anim::type::instant);
while (row->count()) {
delete row->widgetAt(0);
}
box->addButton(tr::lng_bot_share_prepared_button(), choose);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
};
reset();
const auto lifetime = box->lifetime().make_state<rpl::lifetime>();
std::move(
recipient
) | rpl::on_next([=](not_null<Data::Thread*> thread) {
info->hide(anim::type::instant);
while (row->count()) {
delete row->widgetAt(0);
}
AddSkip(row);
AddSinglePeerRow(row, thread, nullptr, choose);
if (const auto topic = thread->asTopic()) {
*lifetime = topic->destroyed() | rpl::on_next(reset);
} else {
*lifetime = rpl::lifetime();
}
row->resizeToWidth(container->width());
box->clearButtons();
box->addButton(tr::lng_send_button(), [=] { send(thread); });
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}, info->lifetime());
item->history()->owner().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> removed) {
if (removed == item) {
box->closeBox();
}
}, box->lifetime());
}
} // namespace InlineBots

View File

@@ -0,0 +1,31 @@
/*
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
namespace Data {
class Thread;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class GenericBox;
} // namespace Ui
namespace InlineBots {
void PreparedPreviewBox(
not_null<Ui::GenericBox*> box,
not_null<HistoryItem*> item,
rpl::producer<not_null<Data::Thread*>> recipient,
Fn<void()> choose,
Fn<void(not_null<Data::Thread*>)> sent);
} // namespace InlineBots

View File

@@ -0,0 +1,421 @@
/*
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 "inline_bots/inline_bot_downloads.h"
#include "core/file_utilities.h"
#include "data/data_document.h"
#include "data/data_peer_id.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "storage/file_download_web.h"
#include "storage/serialize_common.h"
#include "storage/storage_account.h"
#include "ui/chat/attach/attach_bot_downloads.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/labels.h"
#include "styles/style_chat.h"
#include <QtCore/QBuffer>
#include <QtCore/QDataStream>
#include "base/call_delayed.h"
namespace InlineBots {
namespace {
constexpr auto kDownloadsVersion = 1;
constexpr auto kMaxDownloadsBots = 4096;
constexpr auto kMaxDownloadsPerBot = 16384;
} // namespace
Downloads::Downloads(not_null<Main::Session*> session)
: _session(session) {
}
Downloads::~Downloads() {
base::take(_loaders);
base::take(_lists);
}
DownloadId Downloads::start(StartArgs &&args) {
read();
const auto botId = args.bot->id;
const auto id = ++_autoIncrementId;
auto &list = _lists[botId].list;
list.push_back({
.id = id,
.url = std::move(args.url),
.path = std::move(args.path),
});
load(botId, id, list.back());
return id;
}
void Downloads::load(
PeerId botId,
DownloadId id,
DownloadsEntry &entry) {
entry.loading = 1;
entry.failed = 0;
auto &loader = _loaders[id];
Assert(!loader.loader);
loader.botId = botId;
loader.loader = std::make_unique<webFileLoader>(
_session,
entry.url,
entry.path,
WebRequestType::FullLoad);
applyProgress(botId, id, 0, 0);
loader.loader->updates(
) | rpl::on_next_error_done([=] {
progress(botId, id);
}, [=](FileLoader::Error) {
fail(botId, id);
}, [=] {
done(botId, id);
}, loader.loader->lifetime());
loader.loader->start();
}
void Downloads::progress(PeerId botId, DownloadId id) {
const auto i = _loaders.find(id);
if (i == end(_loaders)) {
return;
}
const auto &loader = i->second.loader;
const auto total = loader->fullSize();
const auto ready = loader->currentOffset();
auto &list = _lists[botId].list;
const auto j = ranges::find(
list,
id,
&DownloadsEntry::id);
Assert(j != end(list));
if (total < 0 || ready > total) {
fail(botId, id);
return;
} else if (ready == total) {
// Wait for 'done' signal.
return;
}
applyProgress(botId, id, total, ready);
}
void Downloads::fail(PeerId botId, DownloadId id, bool cancel) {
const auto i = _loaders.find(id);
if (i == end(_loaders)) {
return;
}
auto loader = std::move(i->second.loader);
_loaders.erase(i);
loader = nullptr;
auto &list = _lists[botId].list;
const auto k = ranges::find(
list,
id,
&DownloadsEntry::id);
Assert(k != end(list));
k->loading = 0;
k->failed = 1;
if (cancel) {
auto copy = *k;
list.erase(k);
applyProgress(botId, copy, 0, 0);
} else {
applyProgress(botId, *k, 0, 0);
}
}
void Downloads::done(PeerId botId, DownloadId id) {
const auto i = _loaders.find(id);
if (i == end(_loaders)) {
return;
}
const auto total = i->second.loader->fullSize();
if (total <= 0) {
fail(botId, id);
return;
}
_loaders.erase(i);
auto &list = _lists[botId].list;
const auto j = ranges::find(
list,
id,
&DownloadsEntry::id);
Assert(j != end(list));
j->loading = 0;
applyProgress(botId, id, total, total);
}
void Downloads::applyProgress(
PeerId botId,
DownloadId id,
int64 total,
int64 ready) {
Expects(total >= 0);
Expects(ready >= 0 && ready <= total);
auto &list = _lists[botId].list;
const auto j = ranges::find(
list,
id,
&DownloadsEntry::id);
Assert(j != end(list));
applyProgress(botId, *j, total, ready);
}
void Downloads::applyProgress(
PeerId botId,
DownloadsEntry &entry,
int64 total,
int64 ready) {
auto &progress = _progressView[botId];
auto current = progress.current();
auto subtract = int64(0);
if (current.ready == current.total) {
subtract = current.ready;
}
if (entry.total != total) {
const auto delta = total - entry.total;
entry.total = total;
current.total += delta;
}
if (entry.ready != ready) {
const auto delta = ready - entry.ready;
entry.ready = ready;
current.ready += delta;
}
if (subtract > 0
&& current.ready >= subtract
&& current.total >= subtract) {
current.ready -= subtract;
current.total -= subtract;
}
if (entry.loading || current.ready < current.total) {
current.loading = 1;
} else {
current.loading = 0;
}
if (total > 0 && total == ready) {
write();
}
progress = current;
}
void Downloads::action(
not_null<UserData*> bot,
DownloadId id,
DownloadsAction type) {
switch (type) {
case DownloadsAction::Open: {
const auto i = ranges::find(
_lists[bot->id].list,
id,
&DownloadsEntry::id);
if (i == end(_lists[bot->id].list)) {
return;
}
File::ShowInFolder(i->path);
} break;
case DownloadsAction::Cancel: {
const auto i = _loaders.find(id);
if (i == end(_loaders)) {
return;
}
const auto botId = i->second.botId;
fail(botId, id, true);
} break;
case DownloadsAction::Retry: {
const auto i = ranges::find(
_lists[bot->id].list,
id,
&DownloadsEntry::id);
if (i == end(_lists[bot->id].list)) {
return;
}
load(bot->id, id, *i);
} break;
}
}
[[nodiscard]] auto Downloads::progress(not_null<UserData*> bot)
->rpl::producer<DownloadsProgress> {
read();
return _progressView[bot->id].value();
}
const std::vector<DownloadsEntry> &Downloads::list(
not_null<UserData*> bot,
bool forceCheck) {
read();
auto &entry = _lists[bot->id];
if (forceCheck) {
const auto was = int(entry.list.size());
for (auto i = begin(entry.list); i != end(entry.list);) {
if (i->loading || i->failed) {
++i;
} else if (auto info = QFileInfo(i->path)
; !info.exists() || info.size() != i->total) {
i = entry.list.erase(i);
} else {
++i;
}
}
if (int(entry.list.size()) != was) {
write();
}
}
return entry.list;
}
void Downloads::read() {
auto bytes = _session->local().readInlineBotsDownloads();
if (bytes.isEmpty()) {
return;
}
Assert(_lists.empty());
auto stream = QDataStream(&bytes, QIODevice::ReadOnly);
stream.setVersion(QDataStream::Qt_5_1);
quint32 version = 0, count = 0;
stream >> version;
if (version != kDownloadsVersion) {
return;
}
stream >> count;
if (!count || count > kMaxDownloadsBots) {
return;
}
auto lists = base::flat_map<PeerId, List>();
for (auto i = 0; i != count; ++i) {
quint64 rawBotId = 0;
quint32 count = 0;
stream >> rawBotId >> count;
const auto botId = DeserializePeerId(rawBotId);
if (!botId
|| !peerIsUser(botId)
|| count > kMaxDownloadsPerBot
|| lists.contains(botId)) {
return;
}
auto &list = lists[botId];
list.list.reserve(count);
for (auto j = 0; j != count; ++j) {
auto entry = DownloadsEntry();
auto size = int64();
stream >> entry.url >> entry.path >> size;
entry.total = entry.ready = size;
entry.id = ++_autoIncrementId;
list.list.push_back(std::move(entry));
}
}
_lists = std::move(lists);
}
void Downloads::write() {
auto size = sizeof(quint32) // version
+ sizeof(quint32); // lists count
for (const auto &[botId, list] : _lists) {
size += sizeof(quint64) // botId
+ sizeof(quint32); // list count
for (const auto &entry : list.list) {
if (entry.total > 0 && entry.ready == entry.total) {
size += Serialize::stringSize(entry.url)
+ Serialize::stringSize(entry.path)
+ sizeof(quint64); // size
}
}
}
auto bytes = QByteArray();
bytes.reserve(size);
auto buffer = QBuffer(&bytes);
buffer.open(QIODevice::WriteOnly);
auto stream = QDataStream(&buffer);
stream.setVersion(QDataStream::Qt_5_1);
stream << quint32(kDownloadsVersion) << quint32(_lists.size());
for (const auto &[botId, list] : _lists) {
stream << SerializePeerId(botId) << quint32(list.list.size());
for (const auto &entry : list.list) {
if (entry.total > 0 && entry.ready == entry.total) {
stream << entry.url << entry.path << entry.total;
}
}
}
buffer.close();
_session->local().writeInlineBotsDownloads(bytes);
}
void DownloadFileBox(not_null<Ui::GenericBox*> box, DownloadBoxArgs args) {
Expects(!args.name.isEmpty());
box->setTitle(tr::lng_bot_download_file());
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_bot_download_file_sure(
lt_bot,
rpl::single(tr::bold(args.bot)),
tr::rich),
st::botDownloadLabel));
//box->addRow(MakeFilePreview(box, args));
const auto done = std::move(args.done);
const auto name = args.name;
const auto session = args.session;
const auto chosen = std::make_shared<bool>();
box->addButton(tr::lng_bot_download_file_button(), [=] {
const auto path = FileNameForSave(
session,
tr::lng_save_file(tr::now),
QString(),
u"file"_q,
name,
false,
QDir());
if (!path.isEmpty()) {
*chosen = true;
box->closeBox();
done(path);
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
box->boxClosing() | rpl::on_next([=] {
if (!*chosen) {
done(QString());
}
}, box->lifetime());
}
} // namespace InlineBots

View File

@@ -0,0 +1,105 @@
/*
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/chat/attach/attach_bot_webview.h"
class webFileLoader;
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class GenericBox;
} // namespace Ui
namespace InlineBots {
using DownloadId = uint32;
using ::Ui::BotWebView::DownloadsProgress;
using ::Ui::BotWebView::DownloadsEntry;
using ::Ui::BotWebView::DownloadsAction;
class Downloads final {
public:
explicit Downloads(not_null<Main::Session*> session);
~Downloads();
struct StartArgs {
not_null<UserData*> bot;
QString url;
QString path;
};
uint32 start(StartArgs &&args); // Returns download id.
void action(
not_null<UserData*> bot,
DownloadId id,
DownloadsAction type);
[[nodiscard]] rpl::producer<DownloadsProgress> progress(
not_null<UserData*> bot);
[[nodiscard]] const std::vector<DownloadsEntry> &list(
not_null<UserData*> bot,
bool check = false);
private:
struct List {
std::vector<DownloadsEntry> list;
};
struct Loader {
std::unique_ptr<webFileLoader> loader;
PeerId botId = 0;
};
void read();
void write();
void load(
PeerId botId,
DownloadId id,
DownloadsEntry &entry);
void progress(PeerId botId, DownloadId id);
void fail(PeerId botId, DownloadId id, bool cancel = false);
void done(PeerId botId, DownloadId id);
void applyProgress(
PeerId botId,
DownloadId id,
int64 total,
int64 ready);
void applyProgress(
PeerId botId,
DownloadsEntry &entry,
int64 total,
int64 ready);
const not_null<Main::Session*> _session;
base::flat_map<PeerId, List> _lists;
base::flat_map<DownloadId, Loader> _loaders;
base::flat_map<
PeerId,
rpl::variable<DownloadsProgress>> _progressView;
DownloadId _autoIncrementId = 0;
};
struct DownloadBoxArgs {
not_null<Main::Session*> session;
QString bot;
QString name;
QString url;
Fn<void(QString)> done;
};
void DownloadFileBox(not_null<Ui::GenericBox*> box, DownloadBoxArgs args);
} // namespace InlineBots

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,436 @@
/*
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 "base/flags.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "media/clip/media_clip_reader.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/text/text.h"
namespace Lottie {
class SinglePlayer;
} // namespace Lottie
namespace Data {
class PhotoMedia;
class DocumentMedia;
} // namespace Data
namespace InlineBots {
namespace Layout {
namespace internal {
class FileBase : public ItemBase {
public:
FileBase(not_null<Context*> context, std::shared_ptr<Result> result);
// For saved gif layouts.
FileBase(not_null<Context*> context, not_null<DocumentData*> document);
protected:
DocumentData *getShownDocument() const;
int content_width() const;
int content_height() const;
int content_duration() const;
};
class DeleteSavedGifClickHandler : public LeftButtonClickHandler {
public:
DeleteSavedGifClickHandler(not_null<DocumentData*> data) : _data(data) {
}
protected:
void onClickImpl() const override;
private:
const not_null<DocumentData*> _data;
};
class Gif final : public FileBase {
public:
Gif(not_null<Context*> context, std::shared_ptr<Result> result);
Gif(
not_null<Context*> context,
not_null<DocumentData*> document,
bool hasDeleteButton);
void setPosition(int32 position) override;
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;
// ClickHandlerHost interface
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
int resizeGetHeight(int width) override;
void unloadHeavyPart() override;
QRect innerContentRect() const override;
private:
enum class StateFlag {
Over = (1 << 0),
DeleteOver = (1 << 1),
};
using StateFlags = base::flags<StateFlag>;
friend inline constexpr auto is_flag_type(StateFlag) { return true; };
struct AnimationData {
template <typename Callback>
AnimationData(Callback &&callback)
: radial(std::forward<Callback>(callback)) {
}
bool over = false;
Ui::Animations::Simple _a_over;
Ui::RadialAnimation radial;
};
void ensureDataMediaCreated(not_null<DocumentData*> document) const;
QSize countFrameSize() const;
void validateThumbnail(
Image *image,
QSize size,
QSize frame,
bool good) const;
void prepareThumbnail(QSize size, QSize frame) const;
void ensureAnimation() const;
bool isRadialAnimation() const;
void radialAnimationCallback(crl::time now) const;
void clipCallback(Media::Clip::Notification notification);
StateFlags _state;
Media::Clip::ReaderPointer _gif;
ClickHandlerPtr _delete;
mutable QImage _thumb;
mutable bool _thumbGood = false;
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
mutable std::unique_ptr<AnimationData> _animation;
mutable Ui::Animations::Simple _a_deleteOver;
};
class Photo : public ItemBase {
public:
Photo(not_null<Context*> context, std::shared_ptr<Result> result);
// Not used anywhere currently.
//Photo(not_null<Context*> context, not_null<PhotoData*> photo);
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:
PhotoData *getShownPhoto() const;
QSize countFrameSize() const;
mutable QPixmap _thumb;
mutable bool _thumbGood = false;
void prepareThumbnail(QSize size, QSize frame) const;
void validateThumbnail(
Image *image,
QSize size,
QSize frame,
bool good) const;
mutable std::shared_ptr<Data::PhotoMedia> _photoMedia;
};
class Sticker : public FileBase {
public:
Sticker(not_null<Context*> context, std::shared_ptr<Result> result);
~Sticker();
// Not used anywhere currently.
//Sticker(not_null<Context*> context, not_null<DocumentData*> document);
void initDimensions() override;
bool isFullLine() const override {
return false;
}
bool hasRightSkip() const override {
return false;
}
void preload() const override;
void paint(Painter &p, const QRect &clip, const PaintContext *context) const override;
TextState getState(
QPoint point,
StateRequest request) const override;
// ClickHandlerHost interface
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void unloadHeavyPart() override;
QRect innerContentRect() const override;
private:
void ensureDataMediaCreated(not_null<DocumentData*> document) const;
void setupLottie() const;
void setupWebm() const;
QSize getThumbSize() const;
QSize boundingBox() const;
void prepareThumbnail() const;
void clipCallback(Media::Clip::Notification notification);
mutable Ui::Animations::Simple _a_over;
mutable bool _active = false;
mutable QPixmap _thumb;
mutable bool _thumbLoaded = false;
mutable std::unique_ptr<Lottie::SinglePlayer> _lottie;
Media::Clip::ReaderPointer _webm;
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
mutable rpl::lifetime _lifetime;
};
class Video : public FileBase {
public:
Video(not_null<Context*> context, std::shared_ptr<Result> result);
void initDimensions() override;
void paint(Painter &p, const QRect &clip, const PaintContext *context) const override;
TextState getState(
QPoint point,
StateRequest request) const override;
void unloadHeavyPart() override;
private:
ClickHandlerPtr _link;
mutable QPixmap _thumb;
mutable std::shared_ptr<Data::DocumentMedia> _documentMedia;
Ui::Text::String _title, _description;
QString _duration;
int _durationWidth = 0;
[[nodiscard]] bool withThumbnail() const;
void prepareThumbnail(QSize size) const;
};
class CancelFileClickHandler : public LeftButtonClickHandler {
public:
CancelFileClickHandler(not_null<Result*> result) : _result(result) {
}
protected:
void onClickImpl() const override;
private:
not_null<Result*> _result;
};
class File : public FileBase {
public:
File(not_null<Context*> context, std::shared_ptr<Result> result);
~File();
void initDimensions() override;
void paint(Painter &p, const QRect &clip, const PaintContext *context) const override;
TextState getState(
QPoint point,
StateRequest request) const override;
// ClickHandlerHost interface
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void unloadHeavyPart() override;
private:
void thumbAnimationCallback();
void radialAnimationCallback(crl::time now) const;
void ensureAnimation() const;
void ensureDataMediaCreated() const;
void checkAnimationFinished() const;
bool updateStatusText() const;
bool isRadialAnimation() const {
if (_animation) {
if (_animation->radial.animating()) {
return true;
}
checkAnimationFinished();
}
return false;
}
bool isThumbAnimation() const {
if (_animation) {
if (_animation->a_thumbOver.animating()) {
return true;
}
checkAnimationFinished();
}
return false;
}
struct AnimationData {
template <typename Callback>
AnimationData(Callback &&radialCallback)
: radial(std::forward<Callback>(radialCallback)) {
}
Ui::Animations::Simple a_thumbOver;
Ui::RadialAnimation radial;
};
mutable std::unique_ptr<AnimationData> _animation;
Ui::Text::String _title, _description;
ClickHandlerPtr _cancel;
// >= 0 will contain download / upload string, _statusSize = loaded bytes
// < 0 will contain played string, _statusSize = -(seconds + 1) played
// 0xFFFFFFF0LL will contain status for not yet downloaded file
// 0xFFFFFFF1LL will contain status for already downloaded file
// 0xFFFFFFF2LL will contain status for failed to download / upload file
mutable int64 _statusSize = 0;
mutable QString _statusText;
// duration = -1 - no duration, duration = -2 - "GIF" duration
void setStatusSize(
int64 newSize,
int64 fullSize,
TimeId duration,
TimeId realDuration) const;
not_null<DocumentData*> _document;
mutable std::shared_ptr<Data::DocumentMedia> _documentMedia;
};
class Contact : public ItemBase {
public:
Contact(not_null<Context*> context, std::shared_ptr<Result> result);
void initDimensions() override;
void paint(Painter &p, const QRect &clip, const PaintContext *context) const override;
TextState getState(
QPoint point,
StateRequest request) const override;
private:
mutable QPixmap _thumb;
Ui::Text::String _title, _description;
void prepareThumbnail(int width, int height) const;
};
class Article : public ItemBase {
public:
Article(
not_null<Context*> context,
std::shared_ptr<Result> result,
bool withThumb);
void initDimensions() override;
int resizeGetHeight(int width) override;
void paint(Painter &p, const QRect &clip, const PaintContext *context) const override;
TextState getState(
QPoint point,
StateRequest request) const override;
private:
ClickHandlerPtr _url, _link;
bool _withThumb;
mutable QPixmap _thumb;
Ui::Text::String _title, _description;
QString _thumbLetter, _urlText;
int32 _urlWidth;
void prepareThumbnail(int width, int height) const;
};
class Game : public ItemBase {
public:
Game(not_null<Context*> context, std::shared_ptr<Result> result);
void setPosition(int32 position) override;
void initDimensions() override;
void paint(Painter &p, const QRect &clip, const PaintContext *context) const override;
TextState getState(
QPoint point,
StateRequest request) const override;
void unloadHeavyPart() override;
private:
void ensureDataMediaCreated(not_null<PhotoData*> photo) const;
void ensureDataMediaCreated(not_null<DocumentData*> document) const;
void countFrameSize();
void prepareThumbnail(QSize size) const;
void validateThumbnail(Image *image, QSize size, bool good) const;
bool isRadialAnimation() const;
void radialAnimationCallback(crl::time now) const;
void clipCallback(Media::Clip::Notification notification);
Media::Clip::ReaderPointer _gif;
mutable std::shared_ptr<Data::PhotoMedia> _photoMedia;
mutable std::shared_ptr<Data::DocumentMedia> _documentMedia;
mutable QImage _thumb;
mutable bool _thumbGood = false;
mutable std::unique_ptr<Ui::RadialAnimation> _radial;
Ui::Text::String _title, _description;
QSize _frameSize;
};
} // namespace internal
} // namespace Layout
} // namespace InlineBots

View File

@@ -0,0 +1,267 @@
/*
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 "inline_bots/inline_bot_layout_item.h"
#include "base/never_freed_pointer.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_file_origin.h"
#include "data/data_document_media.h"
#include "core/click_handler_types.h"
#include "inline_bots/inline_bot_result.h"
#include "inline_bots/inline_bot_layout_internal.h"
#include "storage/localstorage.h"
#include "mainwidget.h"
#include "ui/image/image.h"
#include "ui/empty_userpic.h"
namespace InlineBots {
namespace Layout {
namespace {
base::NeverFreedPointer<DocumentItems> documentItemsMap;
} // namespace
std::shared_ptr<Result> ItemBase::getResult() const {
return _result;
}
DocumentData *ItemBase::getDocument() const {
return _document;
}
PhotoData *ItemBase::getPhoto() const {
return _photo;
}
DocumentData *ItemBase::getPreviewDocument() const {
if (_document) {
return _document;
} else if (_result) {
return _result->_document;
}
return nullptr;
}
PhotoData *ItemBase::getPreviewPhoto() const {
if (_photo) {
return _photo;
} else if (_result) {
return _result->_photo;
}
return nullptr;
}
void ItemBase::preload() const {
const auto origin = fileOrigin();
if (_result) {
if (const auto photo = _result->_photo) {
if (photo->hasExact(Data::PhotoSize::Thumbnail)) {
photo->load(Data::PhotoSize::Thumbnail, origin);
}
} else if (const auto document = _result->_document) {
document->loadThumbnail(origin);
} else if (auto &thumb = _result->_thumbnail; !thumb.empty()) {
thumb.load(_result->_session, origin);
}
} else if (_document) {
_document->loadThumbnail(origin);
} else if (_photo && _photo->hasExact(Data::PhotoSize::Thumbnail)) {
_photo->load(Data::PhotoSize::Thumbnail, origin);
}
}
void ItemBase::update() const {
if (_position >= 0) {
context()->inlineItemRepaint(this);
}
}
void ItemBase::layoutChanged() {
if (_position >= 0) {
context()->inlineItemLayoutChanged(this);
}
}
std::unique_ptr<ItemBase> ItemBase::createLayout(
not_null<Context*> context,
std::shared_ptr<Result> result,
bool forceThumb) {
using Type = Result::Type;
switch (result->_type) {
case Type::Photo:
return std::make_unique<internal::Photo>(context, std::move(result));
case Type::Audio:
case Type::File:
return std::make_unique<internal::File>(context, std::move(result));
case Type::Video:
return std::make_unique<internal::Video>(context, std::move(result));
case Type::Sticker:
return std::make_unique<internal::Sticker>(
context,
std::move(result));
case Type::Gif:
return std::make_unique<internal::Gif>(context, std::move(result));
case Type::Article:
case Type::Geo:
case Type::Venue:
return std::make_unique<internal::Article>(
context,
std::move(result),
forceThumb);
case Type::Game:
return std::make_unique<internal::Game>(context, std::move(result));
case Type::Contact:
return std::make_unique<internal::Contact>(
context,
std::move(result));
}
return nullptr;
}
std::unique_ptr<ItemBase> ItemBase::createLayoutGif(
not_null<Context*> context,
not_null<DocumentData*> document) {
return std::make_unique<internal::Gif>(context, document, true);
}
DocumentData *ItemBase::getResultDocument() const {
return _result ? _result->_document : nullptr;
}
PhotoData *ItemBase::getResultPhoto() const {
return _result ? _result->_photo : nullptr;
}
bool ItemBase::hasResultThumb() const {
return _result
&& (!_result->_thumbnail.empty()
|| !_result->_locationThumbnail.empty());
}
QImage *ItemBase::getResultThumb(Data::FileOrigin origin) const {
if (_result && !_thumbnail) {
if (!_result->_thumbnail.empty()) {
_thumbnail = _result->_thumbnail.createView();
_result->_thumbnail.load(_result->_session, origin);
} else if (!_result->_locationThumbnail.empty()) {
_thumbnail = _result->_locationThumbnail.createView();
_result->_locationThumbnail.load(_result->_session, origin);
}
}
return (_thumbnail && !_thumbnail->isNull())
? _thumbnail.get()
: nullptr;
}
QPixmap ItemBase::getResultContactAvatar(int width, int height) const {
if (_result->_type == Result::Type::Contact) {
auto result = Ui::EmptyUserpic(
Ui::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex(
BareId(qHash(_result->_id)))),
_result->getLayoutTitle()
).generate(width);
if (result.height() != height * style::DevicePixelRatio()) {
result = result.scaled(
QSize(width, height) * style::DevicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
return result;
}
return QPixmap();
}
int ItemBase::getResultDuration() const {
return 0;
}
QString ItemBase::getResultUrl() const {
return _result->_url;
}
ClickHandlerPtr ItemBase::getResultUrlHandler() const {
if (!_result->_url.isEmpty()) {
return std::make_shared<UrlClickHandler>(_result->_url);
}
return ClickHandlerPtr();
}
ClickHandlerPtr ItemBase::getResultPreviewHandler() const {
if (!_result->_content_url.isEmpty()) {
return std::make_shared<UrlClickHandler>(
_result->_content_url,
false);
} else if (const auto document = _result->_document
; document && document->createMediaView()->canBePlayed(nullptr)) {
return std::make_shared<OpenFileClickHandler>();
} else if (_result->_photo) {
return std::make_shared<OpenFileClickHandler>();
}
return ClickHandlerPtr();
}
QString ItemBase::getResultThumbLetter() const {
auto parts = QStringView(_result->_url).split('/');
if (!parts.isEmpty()) {
auto domain = parts.at(0);
if (parts.size() > 2 && domain.endsWith(':') && parts.at(1).isEmpty()) { // http:// and others
domain = parts.at(2);
}
parts = domain.split('@').constLast().split('.');
if (parts.size() > 1) {
return parts.at(parts.size() - 2).at(0).toUpper();
}
}
if (!_result->_title.isEmpty()) {
return _result->_title.at(0).toUpper();
}
return QString();
}
Data::FileOrigin ItemBase::fileOrigin() const {
return _context->inlineItemFileOrigin();
}
const DocumentItems *documentItems() {
return documentItemsMap.data();
}
namespace internal {
void regDocumentItem(
not_null<const DocumentData*> document,
not_null<ItemBase*> item) {
documentItemsMap.createIfNull();
(*documentItemsMap)[document].insert(item);
}
void unregDocumentItem(
not_null<const DocumentData*> document,
not_null<ItemBase*> item) {
if (documentItemsMap) {
auto i = documentItemsMap->find(document);
if (i != documentItemsMap->cend()) {
if (i->second.remove(item) && i->second.empty()) {
documentItemsMap->erase(i);
}
}
if (documentItemsMap->empty()) {
documentItemsMap.clear();
}
}
}
} // namespace internal
} // namespace Layout
} // namespace InlineBots

View File

@@ -0,0 +1,167 @@
/*
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 "layout/layout_item_base.h"
#include "ui/text/text.h"
class Image;
namespace Ui {
class PathShiftGradient;
} // namespace Ui
namespace InlineBots {
class Result;
namespace Layout {
class ItemBase;
class PaintContext : public PaintContextBase {
public:
PaintContext(crl::time ms, bool selecting, bool paused, bool lastRow)
: PaintContextBase(ms, selecting)
, paused(paused)
, lastRow(lastRow) {
}
bool paused, lastRow;
Ui::PathShiftGradient *pathGradient = nullptr;
};
// this type used as a flag, we dynamic_cast<> to it
class SendClickHandler : public ClickHandler {
public:
void onClick(ClickContext context) const override {
}
};
class OpenFileClickHandler : public ClickHandler {
public:
void onClick(ClickContext context) const override {
}
};
class Context {
public:
virtual void inlineItemLayoutChanged(const ItemBase *layout) = 0;
virtual bool inlineItemVisible(const ItemBase *item) = 0;
virtual void inlineItemRepaint(const ItemBase *item) = 0;
virtual Data::FileOrigin inlineItemFileOrigin() = 0;
};
class ItemBase : public LayoutItemBase {
public:
ItemBase(not_null<Context*> context, std::shared_ptr<Result> result)
: _result(result)
, _context(context) {
}
ItemBase(not_null<Context*> context, not_null<DocumentData*> document)
: _document(document)
, _context(context) {
}
// Not used anywhere currently.
//ItemBase(not_null<Context*> context, PhotoData *photo) : _photo(photo), _context(context) {
//}
virtual void paint(Painter &p, const QRect &clip, const PaintContext *context) const = 0;
virtual bool isFullLine() const {
return true;
}
virtual bool hasRightSkip() const {
return false;
}
std::shared_ptr<Result> getResult() const;
DocumentData *getDocument() const;
PhotoData *getPhoto() const;
// Get document or photo (possibly from InlineBots::Result) for
// showing sticker / GIF / photo preview by long mouse press.
DocumentData *getPreviewDocument() const;
PhotoData *getPreviewPhoto() const;
virtual void preload() const;
virtual void unloadHeavyPart() {
_thumbnail = nullptr;
}
void update() const;
void layoutChanged();
// ClickHandlerHost interface
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override {
update();
}
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override {
update();
}
virtual QRect innerContentRect() const {
// Only stickers are supported for now.
Unexpected("Unsupported type to get a rect of inner content.");
}
static std::unique_ptr<ItemBase> createLayout(
not_null<Context*> context,
std::shared_ptr<Result> result,
bool forceThumb);
static std::unique_ptr<ItemBase> createLayoutGif(
not_null<Context*> context,
not_null<DocumentData*> document);
protected:
DocumentData *getResultDocument() const;
PhotoData *getResultPhoto() const;
bool hasResultThumb() const;
QImage *getResultThumb(Data::FileOrigin origin) const;
QPixmap getResultContactAvatar(int width, int height) const;
int getResultDuration() const;
QString getResultUrl() const;
ClickHandlerPtr getResultUrlHandler() const;
ClickHandlerPtr getResultPreviewHandler() const;
QString getResultThumbLetter() const;
not_null<Context*> context() const {
return _context;
}
Data::FileOrigin fileOrigin() const;
std::shared_ptr<Result> _result;
DocumentData *_document = nullptr;
PhotoData *_photo = nullptr;
ClickHandlerPtr _send = ClickHandlerPtr{ new SendClickHandler() };
ClickHandlerPtr _open = ClickHandlerPtr{ new OpenFileClickHandler() };
private:
not_null<Context*> _context;
mutable std::shared_ptr<QImage> _thumbnail;
};
using DocumentItems = std::map<
not_null<const DocumentData*>,
base::flat_set<not_null<ItemBase*>>>;
const DocumentItems *documentItems();
namespace internal {
void regDocumentItem(
not_null<const DocumentData*> document,
not_null<ItemBase*> item);
void unregDocumentItem(
not_null<const DocumentData*> document,
not_null<ItemBase*> item);
} // namespace internal
} // namespace Layout
} // namespace InlineBots

View File

@@ -0,0 +1,526 @@
/*
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 "inline_bots/inline_bot_result.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "data/data_photo_media.h"
#include "data/data_document_media.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_reply_markup.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "inline_bots/inline_bot_send_data.h"
#include "storage/file_download.h"
#include "core/file_utilities.h"
#include "core/mime_type.h"
#include "ui/image/image.h"
#include "ui/image/image_location_factory.h"
#include "mainwidget.h"
#include "main/main_session.h"
#include "styles/style_chat_helpers.h"
namespace InlineBots {
namespace {
const auto kVideoThumbMime = "video/mp4"_q;
QString GetContentUrl(const MTPWebDocument &document) {
switch (document.type()) {
case mtpc_webDocument:
return qs(document.c_webDocument().vurl());
case mtpc_webDocumentNoProxy:
return qs(document.c_webDocumentNoProxy().vurl());
}
Unexpected("Type in GetContentUrl.");
}
} // namespace
Result::Result(not_null<Main::Session*> session, const Creator &creator)
: _session(session)
, _queryId(creator.queryId)
, _type(creator.type) {
}
std::shared_ptr<Result> Result::Create(
not_null<Main::Session*> session,
uint64 queryId,
const MTPBotInlineResult &data) {
using Type = Result::Type;
const auto type = [&] {
static const auto kStringToTypeMap = base::flat_map<QString, Type>{
{ u"photo"_q, Type::Photo },
{ u"video"_q, Type::Video },
{ u"audio"_q, Type::Audio },
{ u"voice"_q, Type::Audio },
{ u"sticker"_q, Type::Sticker },
{ u"file"_q, Type::File },
{ u"gif"_q, Type::Gif },
{ u"article"_q, Type::Article },
{ u"contact"_q, Type::Contact },
{ u"venue"_q, Type::Venue },
{ u"geo"_q, Type::Geo },
{ u"game"_q, Type::Game },
};
const auto type = data.match([](const auto &data) {
return qs(data.vtype());
});
const auto i = kStringToTypeMap.find(type);
return (i != kStringToTypeMap.end()) ? i->second : Type::Unknown;
}();
if (type == Type::Unknown) {
return nullptr;
}
auto result = std::make_shared<Result>(
session,
Creator{ queryId, type });
const auto message = data.match([&](const MTPDbotInlineResult &data) {
result->_id = qs(data.vid());
result->_title = qs(data.vtitle().value_or_empty());
result->_description = qs(data.vdescription().value_or_empty());
result->_url = qs(data.vurl().value_or_empty());
const auto thumbMime = [&] {
if (const auto thumb = data.vthumb()) {
return thumb->match([&](const auto &data) {
return data.vmime_type().v;
});
}
return QByteArray();
}();
const auto contentMime = [&] {
if (const auto content = data.vcontent()) {
return content->match([&](const auto &data) {
return data.vmime_type().v;
});
}
return QByteArray();
}();
const auto imageThumb = !thumbMime.isEmpty()
&& (thumbMime != kVideoThumbMime);
const auto videoThumb = !thumbMime.isEmpty() && !imageThumb;
if (const auto content = data.vcontent()) {
result->_content_url = GetContentUrl(*content);
if (result->_type == Type::Photo) {
result->_photo = session->data().photoFromWeb(
*content,
(imageThumb
? Images::FromWebDocument(*data.vthumb())
: ImageLocation()));
} else if (contentMime != "text/html"_q) {
result->_document = session->data().documentFromWeb(
result->adjustAttributes(*content),
(imageThumb
? Images::FromWebDocument(*data.vthumb())
: ImageLocation()),
(videoThumb
? Images::FromWebDocument(*data.vthumb())
: ImageLocation()));
}
}
if (!result->_photo && !result->_document && imageThumb) {
result->_thumbnail.update(result->_session, ImageWithLocation{
.location = Images::FromWebDocument(*data.vthumb())
});
}
return &data.vsend_message();
}, [&](const MTPDbotInlineMediaResult &data) {
result->_id = qs(data.vid());
result->_title = qs(data.vtitle().value_or_empty());
result->_description = qs(data.vdescription().value_or_empty());
if (const auto photo = data.vphoto()) {
result->_photo = session->data().processPhoto(*photo);
}
if (const auto document = data.vdocument()) {
result->_document = session->data().processDocument(*document);
}
return &data.vsend_message();
});
if ((result->_photo && result->_photo->isNull())
|| (result->_document && result->_document->isNull())) {
return nullptr;
}
// Ensure required media fields for layouts.
if (result->_type == Type::Photo) {
if (!result->_photo) {
return nullptr;
}
} else if (result->_type == Type::Audio
|| result->_type == Type::File
|| result->_type == Type::Sticker
|| result->_type == Type::Gif) {
if (!result->_document) {
return nullptr;
}
}
message->match([&](const MTPDbotInlineMessageMediaAuto &data) {
const auto message = qs(data.vmessage());
const auto entities = Api::EntitiesFromMTP(
session,
data.ventities().value_or_empty());
if (result->_type == Type::Photo) {
if (result->_photo) {
result->sendData = std::make_unique<internal::SendPhoto>(
session,
result->_photo,
message,
entities);
} else {
LOG(("Inline Error: No 'photo' in media-auto, type=photo."));
}
} else if (result->_type == Type::Game) {
result->createGame(session);
result->sendData = std::make_unique<internal::SendGame>(
session,
result->_game);
} else {
if (result->_document) {
result->sendData = std::make_unique<internal::SendFile>(
session,
result->_document,
message,
entities);
} else {
LOG(("Inline Error: No 'document' in media-auto, type=%1."
).arg(int(result->_type)));
}
}
}, [&](const MTPDbotInlineMessageText &data) {
result->sendData = std::make_unique<internal::SendText>(
session,
qs(data.vmessage()),
Api::EntitiesFromMTP(session, data.ventities().value_or_empty()),
data.is_no_webpage());
}, [&](const MTPDbotInlineMessageMediaGeo &data) {
data.vgeo().match([&](const MTPDgeoPoint &geo) {
if (const auto period = data.vperiod()) {
result->sendData = std::make_unique<internal::SendGeo>(
session,
geo,
period->v,
(data.vheading()
? std::make_optional(data.vheading()->v)
: std::nullopt),
(data.vproximity_notification_radius()
? std::make_optional(
data.vproximity_notification_radius()->v)
: std::nullopt));
} else {
result->sendData = std::make_unique<internal::SendGeo>(
session,
geo);
}
}, [&](const MTPDgeoPointEmpty &) {
LOG(("Inline Error: Empty 'geo' in media-geo."));
});
}, [&](const MTPDbotInlineMessageMediaVenue &data) {
data.vgeo().match([&](const MTPDgeoPoint &geo) {
result->sendData = std::make_unique<internal::SendVenue>(
session,
geo,
qs(data.vvenue_id()),
qs(data.vprovider()),
qs(data.vtitle()),
qs(data.vaddress()));
}, [&](const MTPDgeoPointEmpty &) {
LOG(("Inline Error: Empty 'geo' in media-venue."));
});
}, [&](const MTPDbotInlineMessageMediaContact &data) {
result->sendData = std::make_unique<internal::SendContact>(
session,
qs(data.vfirst_name()),
qs(data.vlast_name()),
qs(data.vphone_number()));
}, [&](const MTPDbotInlineMessageMediaInvoice &data) {
using Flag = MTPDmessageMediaInvoice::Flag;
const auto media = MTP_messageMediaInvoice(
MTP_flags((data.is_shipping_address_requested()
? Flag::f_shipping_address_requested
: Flag(0))
| (data.is_test() ? Flag::f_test : Flag(0))
| (data.vphoto() ? Flag::f_photo : Flag(0))),
data.vtitle(),
data.vdescription(),
data.vphoto() ? (*data.vphoto()) : MTPWebDocument(),
MTPint(), // receipt_msg_id
data.vcurrency(),
data.vtotal_amount(),
MTP_string(QString()), // start_param
MTPMessageExtendedMedia());
result->sendData = std::make_unique<internal::SendInvoice>(
session,
media);
}, [&](const MTPDbotInlineMessageMediaWebPage &data) {
result->sendData = std::make_unique<internal::SendText>(
session,
qs(data.vmessage()),
Api::EntitiesFromMTP(session, data.ventities().value_or_empty()),
false);
});
if (!result->sendData || !result->sendData->isValid()) {
return nullptr;
}
message->match([&](const auto &data) {
if (const auto markup = data.vreply_markup()) {
result->_replyMarkup
= std::make_unique<HistoryMessageMarkupData>(markup);
}
});
if (const auto point = result->getLocationPoint()) {
const auto scale = 1 + (cScale() * style::DevicePixelRatio()) / 200;
const auto zoom = 15 + (scale - 1);
const auto w = st::inlineThumbSize / scale;
const auto h = st::inlineThumbSize / scale;
auto location = GeoPointLocation();
location.lat = point->lat();
location.lon = point->lon();
location.access = point->accessHash();
location.width = w;
location.height = h;
location.zoom = zoom;
location.scale = scale;
result->_locationThumbnail.update(result->_session, ImageWithLocation{
.location = ImageLocation({ location }, w, h)
});
}
return result;
}
bool Result::onChoose(Layout::ItemBase *layout) {
if (_photo && _type == Type::Photo) {
const auto media = _photo->activeMediaView();
if (!media || media->image(Data::PhotoSize::Thumbnail)) {
return true;
} else if (!_photo->loading(Data::PhotoSize::Thumbnail)) {
_photo->load(
Data::PhotoSize::Thumbnail,
Data::FileOrigin());
}
return false;
}
if (_document && (
_type == Type::Video ||
_type == Type::Audio ||
_type == Type::Sticker ||
_type == Type::File ||
_type == Type::Gif)) {
if (_type == Type::Gif) {
const auto media = _document->activeMediaView();
const auto preview = Data::VideoPreviewState(media.get());
if (!media || preview.loaded()) {
return true;
} else if (!preview.usingThumbnail()) {
if (preview.loading()) {
_document->cancel();
} else {
DocumentSaveClickHandler::Save(
Data::FileOriginSavedGifs(),
_document);
}
}
return false;
}
return true;
}
return true;
}
Media::View::OpenRequest Result::openRequest() {
using namespace Media::View;
if (_document) {
return OpenRequest(nullptr, _document, nullptr, MsgId(), PeerId());
} else if (_photo) {
return OpenRequest(nullptr, _photo, nullptr, MsgId(), PeerId());
}
return {};
}
void Result::cancelFile() {
if (_document) {
DocumentCancelClickHandler(_document, nullptr).onClick({});
} else if (_photo) {
PhotoCancelClickHandler(_photo, nullptr).onClick({});
}
}
bool Result::hasThumbDisplay() const {
if (!_thumbnail.empty()
|| _photo
|| (_document && _document->hasThumbnail())) {
return true;
} else if (_type == Type::Contact) {
return true;
} else if (sendData->hasLocationCoords()) {
return true;
}
return false;
};
void Result::addToHistory(
not_null<History*> history,
HistoryItemCommonFields &&fields) const {
history->addNewLocalMessage(makeMessage(history, std::move(fields)));
}
not_null<HistoryItem*> Result::makeMessage(
not_null<History*> history,
HistoryItemCommonFields &&fields) const {
fields.flags |= MessageFlag::FromInlineBot | MessageFlag::Local;
if (_replyMarkup) {
fields.markup = *_replyMarkup;
if (!fields.markup.isNull()) {
fields.flags |= MessageFlag::HasReplyMarkup;
}
}
return sendData->makeMessage(this, history, std::move(fields));
}
Data::SendError Result::getErrorOnSend(not_null<History*> history) const {
return sendData->getErrorOnSend(this, history).value_or(
Data::RestrictionError(history->peer, ChatRestriction::SendInline));
}
std::optional<Data::LocationPoint> Result::getLocationPoint() const {
return sendData->getLocationPoint();
}
QString Result::getLayoutTitle() const {
return sendData->getLayoutTitle(this);
}
QString Result::getLayoutDescription() const {
return sendData->getLayoutDescription(this);
}
// just to make unique_ptr see the destructors.
Result::~Result() {
}
void Result::createGame(not_null<Main::Session*> session) {
if (_game) {
return;
}
const auto gameId = base::RandomValue<GameId>();
_game = session->data().game(
gameId,
0,
QString(),
_title,
_description,
_photo,
_document);
}
QSize Result::thumbBox() const {
return (_type == Type::Photo) ? QSize(100, 100) : QSize(90, 90);
}
MTPWebDocument Result::adjustAttributes(const MTPWebDocument &document) {
switch (document.type()) {
case mtpc_webDocument: {
const auto &data = document.c_webDocument();
return MTP_webDocument(
data.vurl(),
data.vaccess_hash(),
data.vsize(),
data.vmime_type(),
adjustAttributes(data.vattributes(), data.vmime_type()));
} break;
case mtpc_webDocumentNoProxy: {
const auto &data = document.c_webDocumentNoProxy();
return MTP_webDocumentNoProxy(
data.vurl(),
data.vsize(),
data.vmime_type(),
adjustAttributes(data.vattributes(), data.vmime_type()));
} break;
}
Unexpected("Type in InlineBots::Result::adjustAttributes.");
}
MTPVector<MTPDocumentAttribute> Result::adjustAttributes(
const MTPVector<MTPDocumentAttribute> &existing,
const MTPstring &mimeType) {
auto result = existing.v;
const auto find = [&](mtpTypeId attributeType) {
return ranges::find(
result,
attributeType,
[](const MTPDocumentAttribute &value) { return value.type(); });
};
const auto exists = [&](mtpTypeId attributeType) {
return find(attributeType) != result.cend();
};
const auto mime = qs(mimeType);
if (_type == Type::Gif) {
if (!exists(mtpc_documentAttributeFilename)) {
auto filename = (mime == u"video/mp4"_q
? "animation.gif.mp4"
: "animation.gif");
result.push_back(MTP_documentAttributeFilename(
MTP_string(filename)));
}
if (!exists(mtpc_documentAttributeAnimated)) {
result.push_back(MTP_documentAttributeAnimated());
}
} else if (_type == Type::Audio) {
const auto audio = find(mtpc_documentAttributeAudio);
if (audio != result.cend()) {
using Flag = MTPDdocumentAttributeAudio::Flag;
if (mime == u"audio/ogg"_q) {
// We always treat audio/ogg as a voice message.
// It was that way before we started to get attributes here.
const auto &fields = audio->c_documentAttributeAudio();
if (!(fields.vflags().v & Flag::f_voice)) {
*audio = MTP_documentAttributeAudio(
MTP_flags(fields.vflags().v | Flag::f_voice),
fields.vduration(),
MTP_bytes(fields.vtitle().value_or_empty()),
MTP_bytes(fields.vperformer().value_or_empty()),
MTP_bytes(fields.vwaveform().value_or_empty()));
}
}
const auto &fields = audio->c_documentAttributeAudio();
if (!exists(mtpc_documentAttributeFilename)
&& !(fields.vflags().v & Flag::f_voice)) {
const auto p = Core::MimeTypeForName(mime).globPatterns();
auto pattern = p.isEmpty() ? QString() : p.front();
const auto extension = pattern.isEmpty()
? u".unknown"_q
: pattern.replace('*', QString());
const auto filename = filedialogDefaultName(
u"inline"_q,
extension,
QString(),
true);
result.push_back(
MTP_documentAttributeFilename(MTP_string(filename)));
}
}
}
return MTP_vector<MTPDocumentAttribute>(std::move(result));
}
} // namespace InlineBots

View File

@@ -0,0 +1,142 @@
/*
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 "data/data_cloud_file.h"
#include "api/api_common.h"
#include "media/view/media_view_open_common.h"
#include "ui/effects/message_sending_animation_common.h"
class FileLoader;
class History;
class UserData;
struct HistoryMessageMarkupData;
struct HistoryItemCommonFields;
namespace Data {
class LocationPoint;
struct SendError;
} // namespace Data
namespace InlineBots {
namespace Layout {
class ItemBase;
} // namespace Layout
namespace internal {
class SendData;
} // namespace internal
class Result {
private:
// See http://stackoverflow.com/a/8147326
struct Creator;
public:
// Constructor is public only for std::make_unique<>() to work.
// You should use create() static method instead.
Result(not_null<Main::Session*> session, const Creator &creator);
static std::shared_ptr<Result> Create(
not_null<Main::Session*> session,
uint64 queryId,
const MTPBotInlineResult &mtpData);
uint64 getQueryId() const {
return _queryId;
}
QString getId() const {
return _id;
}
// This is real SendClickHandler::onClick implementation for the specified
// inline bot result. If it returns true you need to send this result.
bool onChoose(Layout::ItemBase *layout);
Media::View::OpenRequest openRequest();
void cancelFile();
bool hasThumbDisplay() const;
void addToHistory(
not_null<History*> history,
HistoryItemCommonFields &&fields) const;
[[nodiscard]] not_null<HistoryItem*> makeMessage(
not_null<History*> history,
HistoryItemCommonFields &&fields) const;
[[nodiscard]] Data::SendError getErrorOnSend(
not_null<History*> history) const;
// interface for Layout:: usage
std::optional<Data::LocationPoint> getLocationPoint() const;
QString getLayoutTitle() const;
QString getLayoutDescription() const;
~Result();
private:
void createGame(not_null<Main::Session*> session);
QSize thumbBox() const;
MTPWebDocument adjustAttributes(const MTPWebDocument &document);
MTPVector<MTPDocumentAttribute> adjustAttributes(
const MTPVector<MTPDocumentAttribute> &document,
const MTPstring &mimeType);
enum class Type {
Unknown,
Photo,
Video,
Audio,
Sticker,
File,
Gif,
Article,
Contact,
Geo,
Venue,
Game,
};
friend class internal::SendData;
friend class Layout::ItemBase;
struct Creator {
uint64 queryId = 0;
Type type = Type::Unknown;
};
not_null<Main::Session*> _session;
uint64 _queryId = 0;
QString _id;
Type _type = Type::Unknown;
QString _title, _description, _url;
QString _content_url;
DocumentData *_document = nullptr;
PhotoData *_photo = nullptr;
GameData *_game = nullptr;
std::unique_ptr<HistoryMessageMarkupData> _replyMarkup;
Data::CloudImage _thumbnail;
Data::CloudImage _locationThumbnail;
std::unique_ptr<internal::SendData> sendData;
};
struct ResultSelected {
std::shared_ptr<Result> result;
not_null<UserData*> bot;
PeerData *recipientOverride = nullptr;
Api::SendOptions options;
Ui::MessageSendingAnimationFrom messageSendingFrom;
// Open in OverlayWidget;
bool open = false;
};
} // namespace InlineBots

View File

@@ -0,0 +1,156 @@
/*
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 "inline_bots/inline_bot_send_data.h"
#include "api/api_text_entities.h"
#include "data/data_document.h"
#include "inline_bots/inline_bot_result.h"
#include "storage/localstorage.h"
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
#include "data/data_channel.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
namespace InlineBots {
namespace internal {
QString SendData::getLayoutTitle(const Result *owner) const {
return owner->_title;
}
QString SendData::getLayoutDescription(const Result *owner) const {
return owner->_description;
}
not_null<HistoryItem*> SendDataCommon::makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const {
auto distinct = getSentMessageFields();
if (fields.replyTo) {
fields.flags |= MessageFlag::HasReplyInfo;
}
return history->makeMessage(
std::move(fields),
std::move(distinct.text),
std::move(distinct.media));
}
Data::SendError SendDataCommon::getErrorOnSend(
const Result *owner,
not_null<History*> history) const {
const auto type = ChatRestriction::SendOther;
return Data::RestrictionError(history->peer, type);
}
SendDataCommon::SentMessageFields SendText::getSentMessageFields() const {
return { .text = { _message, _entities } };
}
SendDataCommon::SentMessageFields SendGeo::getSentMessageFields() const {
if (_period) {
using Flag = MTPDmessageMediaGeoLive::Flag;
return { .media = MTP_messageMediaGeoLive(
MTP_flags((_heading ? Flag::f_heading : Flag(0))
| (_proximityNotificationRadius
? Flag::f_proximity_notification_radius
: Flag(0))),
_location.toMTP(),
MTP_int(_heading.value_or(0)),
MTP_int(*_period),
MTP_int(_proximityNotificationRadius.value_or(0))) };
}
return { .media = MTP_messageMediaGeo(_location.toMTP()) };
}
SendDataCommon::SentMessageFields SendVenue::getSentMessageFields() const {
return { .media = MTP_messageMediaVenue(
_location.toMTP(),
MTP_string(_title),
MTP_string(_address),
MTP_string(_provider),
MTP_string(_venueId),
MTP_string(QString())) }; // venue_type
}
SendDataCommon::SentMessageFields SendContact::getSentMessageFields() const {
return { .media = MTP_messageMediaContact(
MTP_string(_phoneNumber),
MTP_string(_firstName),
MTP_string(_lastName),
MTP_string(), // vcard
MTP_long(0)) }; // user_id
}
QString SendContact::getLayoutDescription(const Result *owner) const {
auto result = SendData::getLayoutDescription(owner);
if (result.isEmpty()) {
return Ui::FormatPhone(_phoneNumber);
}
return result;
}
not_null<HistoryItem*> SendPhoto::makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const {
return history->makeMessage(
std::move(fields),
_photo,
TextWithEntities{ _message, _entities });
}
Data::SendError SendPhoto::getErrorOnSend(
const Result *owner,
not_null<History*> history) const {
const auto type = ChatRestriction::SendPhotos;
return Data::RestrictionError(history->peer, type);
}
not_null<HistoryItem*> SendFile::makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const {
return history->makeMessage(
std::move(fields),
_document,
TextWithEntities{ _message, _entities });
}
Data::SendError SendFile::getErrorOnSend(
const Result *owner,
not_null<History*> history) const {
const auto type = _document->requiredSendRight();
return Data::RestrictionError(history->peer, type);
}
not_null<HistoryItem*> SendGame::makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const {
return history->makeMessage(std::move(fields), _game);
}
Data::SendError SendGame::getErrorOnSend(
const Result *owner,
not_null<History*> history) const {
const auto type = ChatRestriction::SendGames;
return Data::RestrictionError(history->peer, type);
}
SendDataCommon::SentMessageFields SendInvoice::getSentMessageFields() const {
return { .media = _media };
}
QString SendInvoice::getLayoutDescription(const Result *owner) const {
return qs(_media.c_messageMediaInvoice().vdescription());
}
} // namespace internal
} // namespace InlineBots

View File

@@ -0,0 +1,342 @@
/*
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 "history/history_location_manager.h"
struct HistoryItemCommonFields;
namespace Data {
struct SendError;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
class History;
namespace InlineBots {
class Result;
namespace internal {
// Abstract class describing the message that will be
// sent if the user chooses this inline bot result.
// For each type of message that can be sent there will be a subclass.
class SendData {
public:
explicit SendData(not_null<Main::Session*> session) : _session(session) {
}
SendData(const SendData &other) = delete;
SendData &operator=(const SendData &other) = delete;
virtual ~SendData() = default;
[[nodiscard]] Main::Session &session() const {
return *_session;
}
virtual bool isValid() const = 0;
virtual not_null<HistoryItem*> makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const = 0;
virtual Data::SendError getErrorOnSend(
const Result *owner,
not_null<History*> history) const = 0;
virtual bool hasLocationCoords() const {
return false;
}
virtual std::optional<Data::LocationPoint> getLocationPoint() const {
return std::nullopt;
}
virtual QString getLayoutTitle(const Result *owner) const;
virtual QString getLayoutDescription(const Result *owner) const;
private:
not_null<Main::Session*> _session;
};
// This class implements addHistory() for most of the types hiding
// the differences in getSentMessageFields() method.
// Only SendFile and SendPhoto work by their own.
class SendDataCommon : public SendData {
public:
using SendData::SendData;
struct SentMessageFields {
TextWithEntities text;
MTPMessageMedia media = MTP_messageMediaEmpty();
};
virtual SentMessageFields getSentMessageFields() const = 0;
not_null<HistoryItem*> makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const override;
Data::SendError getErrorOnSend(
const Result *owner,
not_null<History*> history) const override;
};
// Plain text message.
class SendText : public SendDataCommon {
public:
SendText(
not_null<Main::Session*> session,
const QString &message,
const EntitiesInText &entities,
bool/* noWebPage*/)
: SendDataCommon(session)
, _message(message)
, _entities(entities) {
}
bool isValid() const override {
return !_message.isEmpty();
}
SentMessageFields getSentMessageFields() const override;
private:
QString _message;
EntitiesInText _entities;
};
// Message with geo location point media.
class SendGeo : public SendDataCommon {
public:
SendGeo(
not_null<Main::Session*> session,
const MTPDgeoPoint &point)
: SendDataCommon(session)
, _location(point) {
}
SendGeo(
not_null<Main::Session*> session,
const MTPDgeoPoint &point,
int period,
std::optional<int> heading,
std::optional<int> proximityNotificationRadius)
: SendDataCommon(session)
, _location(point)
, _period(period)
, _heading(heading)
, _proximityNotificationRadius(proximityNotificationRadius){
}
bool isValid() const override {
return true;
}
SentMessageFields getSentMessageFields() const override;
bool hasLocationCoords() const override {
return true;
}
std::optional<Data::LocationPoint> getLocationPoint() const override {
return _location;
}
private:
Data::LocationPoint _location;
std::optional<int> _period;
std::optional<int> _heading;
std::optional<int> _proximityNotificationRadius;
};
// Message with venue media.
class SendVenue : public SendDataCommon {
public:
SendVenue(
not_null<Main::Session*> session,
const MTPDgeoPoint &point,
const QString &venueId,
const QString &provider,
const QString &title,
const QString &address)
: SendDataCommon(session)
, _location(point)
, _venueId(venueId)
, _provider(provider)
, _title(title)
, _address(address) {
}
bool isValid() const override {
return true;
}
SentMessageFields getSentMessageFields() const override;
bool hasLocationCoords() const override {
return true;
}
std::optional<Data::LocationPoint> getLocationPoint() const override {
return _location;
}
private:
Data::LocationPoint _location;
QString _venueId, _provider, _title, _address;
};
// Message with shared contact media.
class SendContact : public SendDataCommon {
public:
SendContact(
not_null<Main::Session*> session,
const QString &firstName,
const QString &lastName,
const QString &phoneNumber)
: SendDataCommon(session)
, _firstName(firstName)
, _lastName(lastName)
, _phoneNumber(phoneNumber) {
}
bool isValid() const override {
return (!_firstName.isEmpty() || !_lastName.isEmpty()) && !_phoneNumber.isEmpty();
}
SentMessageFields getSentMessageFields() const override;
QString getLayoutDescription(const Result *owner) const override;
private:
QString _firstName, _lastName, _phoneNumber;
};
// Message with photo.
class SendPhoto : public SendData {
public:
SendPhoto(
not_null<Main::Session*> session,
PhotoData *photo,
const QString &message,
const EntitiesInText &entities)
: SendData(session)
, _photo(photo)
, _message(message)
, _entities(entities) {
}
bool isValid() const override {
return _photo != nullptr;
}
not_null<HistoryItem*> makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const override;
Data::SendError getErrorOnSend(
const Result *owner,
not_null<History*> history) const override;
private:
PhotoData *_photo;
QString _message;
EntitiesInText _entities;
};
// Message with file.
class SendFile : public SendData {
public:
SendFile(
not_null<Main::Session*> session,
DocumentData *document,
const QString &message,
const EntitiesInText &entities)
: SendData(session)
, _document(document)
, _message(message)
, _entities(entities) {
}
bool isValid() const override {
return _document != nullptr;
}
not_null<HistoryItem*> makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const override;
Data::SendError getErrorOnSend(
const Result *owner,
not_null<History*> history) const override;
private:
DocumentData *_document;
QString _message;
EntitiesInText _entities;
};
// Message with game.
class SendGame : public SendData {
public:
SendGame(not_null<Main::Session*> session, GameData *game)
: SendData(session)
, _game(game) {
}
bool isValid() const override {
return _game != nullptr;
}
not_null<HistoryItem*> makeMessage(
const Result *owner,
not_null<History*> history,
HistoryItemCommonFields &&fields) const override;
Data::SendError getErrorOnSend(
const Result *owner,
not_null<History*> history) const override;
private:
GameData *_game;
};
class SendInvoice : public SendDataCommon {
public:
SendInvoice(
not_null<Main::Session*> session,
MTPMessageMedia media)
: SendDataCommon(session)
, _media(media) {
}
bool isValid() const override {
return true;
}
SentMessageFields getSentMessageFields() const override;
QString getLayoutDescription(const Result *owner) const override;
private:
MTPMessageMedia _media;
};
} // namespace internal
} // namespace InlineBots

View File

@@ -0,0 +1,181 @@
/*
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 "inline_bots/inline_bot_storage.h"
#include "main/main_session.h"
#include "storage/storage_account.h"
#include <xxhash.h>
namespace InlineBots {
namespace {
constexpr auto kMaxStorageSize = (5 << 20);
[[nodiscard]] uint64 KeyHash(const QString &key) {
return XXH64(key.data(), key.size(), 0);
}
} // namespace
Storage::Storage(not_null<Main::Session*> session)
: _session(session) {
}
bool Storage::write(
PeerId botId,
const QString &key,
const std::optional<QString> &value) {
if (value && value->size() > kMaxStorageSize) {
return false;
}
readFromDisk(botId);
auto i = _lists.find(botId);
if (i == end(_lists)) {
if (!value) {
return true;
}
i = _lists.emplace(botId).first;
}
auto &list = i->second;
const auto hash = KeyHash(key);
auto j = list.data.find(hash);
if (j == end(list.data)) {
if (!value) {
return true;
}
j = list.data.emplace(hash).first;
}
auto &bykey = j->second;
const auto k = ranges::find(bykey, key, &Entry::key);
if (k == end(bykey) && !value) {
return true;
}
const auto size = list.totalSize
- (k != end(bykey) ? (key.size() + k->value.size()) : 0)
+ (value ? (key.size() + value->size()) : 0);
if (size > kMaxStorageSize) {
return false;
}
if (k == end(bykey)) {
bykey.emplace_back(Entry{ key, *value });
++list.keysCount;
} else if (value) {
k->value = *value;
} else {
bykey.erase(k);
--list.keysCount;
}
if (bykey.empty()) {
list.data.erase(j);
if (list.data.empty()) {
Assert(size == 0);
_lists.erase(i);
} else {
list.totalSize = size;
}
} else {
list.totalSize = size;
}
saveToDisk(botId);
return true;
}
std::optional<QString> Storage::read(PeerId botId, const QString &key) {
readFromDisk(botId);
const auto i = _lists.find(botId);
if (i == end(_lists)) {
return std::nullopt;
}
const auto &list = i->second;
const auto j = list.data.find(KeyHash(key));
if (j == end(list.data)) {
return std::nullopt;
}
const auto &bykey = j->second;
const auto k = ranges::find(bykey, key, &Entry::key);
if (k == end(bykey)) {
return std::nullopt;
}
return k->value;
}
void Storage::clear(PeerId botId) {
if (_lists.remove(botId)) {
saveToDisk(botId);
}
}
void Storage::saveToDisk(PeerId botId) {
const auto i = _lists.find(botId);
if (i != end(_lists)) {
_session->local().writeBotStorage(botId, Serialize(i->second));
} else {
_session->local().writeBotStorage(botId, QByteArray());
}
}
void Storage::readFromDisk(PeerId botId) {
const auto serialized = _session->local().readBotStorage(botId);
if (!serialized.isEmpty()) {
_lists[botId] = Deserialize(serialized);
}
}
QByteArray Storage::Serialize(const List &list) {
auto result = QByteArray();
const auto size = sizeof(quint32)
+ (list.keysCount * sizeof(quint32))
+ (list.totalSize * sizeof(ushort));
result.reserve(size);
{
QDataStream stream(&result, QIODevice::WriteOnly);
auto count = 0;
stream.setVersion(QDataStream::Qt_5_1);
stream << quint32(list.keysCount);
for (const auto &[hash, bykey] : list.data) {
for (const auto &entry : bykey) {
stream << entry.key << entry.value;
++count;
}
}
Assert(count == list.keysCount);
}
return result;
}
Storage::List Storage::Deserialize(const QByteArray &serialized) {
QDataStream stream(serialized);
stream.setVersion(QDataStream::Qt_5_1);
auto count = quint32();
auto result = List();
stream >> count;
if (count > kMaxStorageSize) {
return {};
}
for (auto i = 0; i != count; ++i) {
auto entry = Entry();
stream >> entry.key >> entry.value;
const auto hash = KeyHash(entry.key);
auto j = result.data.find(hash);
if (j == end(result.data)) {
j = result.data.emplace(hash).first;
}
auto &bykey = j->second;
const auto k = ranges::find(bykey, entry.key, &Entry::key);
if (k == end(bykey)) {
bykey.push_back(entry);
result.totalSize += entry.key.size() + entry.value.size();
++result.keysCount;
}
}
return result;
}
} // namespace InlineBots

View File

@@ -0,0 +1,52 @@
/*
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/chat/attach/attach_bot_webview.h"
namespace Main {
class Session;
} // namespace Main
namespace InlineBots {
class Storage final {
public:
explicit Storage(not_null<Main::Session*> session);
bool write(
PeerId botId,
const QString &key,
const std::optional<QString> &value);
std::optional<QString> read(PeerId botId, const QString &key);
void clear(PeerId botId);
private:
struct Entry {
QString key;
QString value;
};
struct List {
base::flat_map<uint64, std::vector<Entry>> data;
int keysCount = 0;
int totalSize = 0;
};
void saveToDisk(PeerId botId);
void readFromDisk(PeerId botId);
[[nodiscard]] static QByteArray Serialize(const List &list);
[[nodiscard]] static List Deserialize(const QByteArray &serialized);
const not_null<Main::Session*> _session;
base::flat_map<PeerId, List> _lists;
};
} // namespace InlineBots

View File

@@ -0,0 +1,753 @@
/*
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 "inline_bots/inline_results_inner.h"
#include "api/api_common.h"
#include "chat_helpers/gifs_list_widget.h" // ChatHelpers::AddGifAction
#include "menu/menu_send.h" // SendMenu::FillSendMenu
#include "core/click_handler_types.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_user.h"
#include "data/data_changes.h"
#include "data/data_chat_participant_status.h"
#include "data/data_session.h"
#include "inline_bots/bot_attach_web_view.h"
#include "inline_bots/inline_bot_result.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "lang/lang_keys.h"
#include "layout/layout_position.h"
#include "mainwindow.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
namespace InlineBots {
namespace Layout {
namespace {
constexpr auto kMinRepaintDelay = crl::time(33);
constexpr auto kMinAfterScrollDelay = crl::time(33);
} // namespace
Inner::Inner(
QWidget *parent,
not_null<Window::SessionController*> controller)
: RpWidget(parent)
, _controller(controller)
, _pathGradient(std::make_unique<Ui::PathShiftGradient>(
st::windowBgRipple,
st::windowBgOver,
[=] { repaintItems(); }))
, _updateInlineItems([=] { updateInlineItems(); })
, _mosaic(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft)
, _previewTimer([=] { showPreview(); }) {
resize(st::emojiPanWidth - st::emojiScroll.width - st::roundRadiusSmall, st::inlineResultsMinHeight);
setMouseTracking(true);
setAttribute(Qt::WA_OpaquePaintEvent);
_controller->session().downloaderTaskFinished(
) | rpl::on_next([=] {
updateInlineItems();
}, lifetime());
controller->gifPauseLevelChanged(
) | rpl::on_next([=] {
if (!_controller->isGifPausedAtLeastFor(
Window::GifPauseReason::InlineResults)) {
updateInlineItems();
}
}, lifetime());
_controller->session().changes().peerUpdates(
Data::PeerUpdate::Flag::Rights
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer.get() == _inlineQueryPeer);
}) | rpl::on_next([=] {
auto isRestricted = (_restrictedLabel != nullptr);
if (isRestricted != isRestrictedView()) {
auto h = countHeight();
if (h != height()) resize(width(), h);
}
}, lifetime());
sizeValue(
) | rpl::on_next([=](const QSize &s) {
_mosaic.setFullWidth(s.width());
}, lifetime());
_mosaic.setRightSkip(st::inlineResultsSkip);
}
void Inner::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
_visibleBottom = visibleBottom;
if (_visibleTop != visibleTop) {
_visibleTop = visibleTop;
_lastScrolledAt = crl::now();
update();
}
}
void Inner::checkRestrictedPeer() {
if (_inlineQueryPeer) {
const auto error = Data::RestrictionError(
_inlineQueryPeer,
ChatRestriction::SendInline);
const auto changed = (_restrictedLabelKey != error.text);
if (!changed) {
return;
}
_restrictedLabelKey = error.text;
if (error) {
const auto window = _controller;
const auto peer = _inlineQueryPeer;
_restrictedLabel.create(
this,
rpl::single(error.boostsToLift
? tr::link(error.text)
: TextWithEntities{ error.text }),
st::stickersRestrictedLabel);
const auto lifting = error.boostsToLift;
_restrictedLabel->setClickHandlerFilter([=](auto...) {
window->resolveBoostState(peer->asChannel(), lifting);
return false;
});
_restrictedLabel->show();
updateRestrictedLabelGeometry();
if (_switchPmButton) {
_switchPmButton->hide();
}
repaintItems();
return;
}
} else {
_restrictedLabelKey = QString();
}
if (_restrictedLabel) {
_restrictedLabel.destroy();
if (_switchPmButton) {
_switchPmButton->show();
}
repaintItems();
}
}
void Inner::updateRestrictedLabelGeometry() {
if (!_restrictedLabel) {
return;
}
auto labelWidth = width() - st::stickerPanPadding * 2;
_restrictedLabel->resizeToWidth(labelWidth);
_restrictedLabel->moveToLeft(
(width() - _restrictedLabel->width()) / 2,
st::stickerPanPadding);
}
bool Inner::isRestrictedView() {
checkRestrictedPeer();
return (_restrictedLabel != nullptr);
}
int Inner::countHeight() {
if (isRestrictedView()) {
return st::stickerPanPadding + _restrictedLabel->height() + st::stickerPanPadding;
} else if (_mosaic.empty() && !_switchPmButton) {
return st::stickerPanPadding + st::normalFont->height + st::stickerPanPadding;
}
auto result = st::stickerPanPadding;
if (_switchPmButton) {
result += _switchPmButton->height() + st::inlineResultsSkip;
}
for (auto i = 0, l = _mosaic.rowsCount(); i < l; ++i) {
result += _mosaic.rowHeightAt(i);
}
return result + st::stickerPanPadding;
}
QString Inner::tooltipText() const {
if (const auto lnk = ClickHandler::getActive()) {
return lnk->tooltip();
}
return QString();
}
QPoint Inner::tooltipPos() const {
return _lastMousePos;
}
bool Inner::tooltipWindowActive() const {
return Ui::AppInFocus() && Ui::InFocusChain(window());
}
rpl::producer<> Inner::inlineRowsCleared() const {
return _inlineRowsCleared.events();
}
Inner::~Inner() = default;
void Inner::resizeEvent(QResizeEvent *e) {
updateRestrictedLabelGeometry();
}
void Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
QRect r = e ? e->rect() : rect();
if (r != rect()) {
p.setClipRect(r);
}
p.fillRect(r, st::emojiPanBg);
paintInlineItems(p, r);
}
void Inner::paintInlineItems(Painter &p, const QRect &r) {
if (_restrictedLabel) {
return;
}
if (_mosaic.empty() && !_switchPmButton) {
p.setFont(st::normalFont);
p.setPen(st::noContactsColor);
p.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), tr::lng_inline_bot_no_results(tr::now), style::al_center);
return;
}
const auto gifPaused = _controller->isGifPausedAtLeastFor(
Window::GifPauseReason::InlineResults);
using namespace InlineBots::Layout;
PaintContext context(crl::now(), false, gifPaused, false);
context.pathGradient = _pathGradient.get();
context.pathGradient->startFrame(0, width(), width() / 2);
auto paintItem = [&](not_null<const ItemBase*> item, QPoint point) {
p.translate(point.x(), point.y());
item->paint(
p,
r.translated(-point),
&context);
p.translate(-point.x(), -point.y());
};
_mosaic.paint(std::move(paintItem), r);
}
void Inner::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
_lastMousePos = e->globalPos();
updateSelected();
_pressed = _selected;
ClickHandler::pressed();
_previewTimer.callOnce(QApplication::startDragTime());
}
void Inner::mouseReleaseEvent(QMouseEvent *e) {
_previewTimer.cancel();
auto pressed = std::exchange(_pressed, -1);
auto activated = ClickHandler::unpressed();
if (_previewShown) {
_previewShown = false;
return;
}
_lastMousePos = e->globalPos();
updateSelected();
if (_selected < 0 || _selected != pressed || !activated) {
return;
}
using namespace InlineBots::Layout;
const auto open = dynamic_cast<OpenFileClickHandler*>(activated.get());
if (dynamic_cast<SendClickHandler*>(activated.get()) || open) {
selectInlineResult(_selected, {}, !!open);
} else {
ActivateClickHandler(window(), activated, {
e->button(),
QVariant::fromValue(ClickHandlerContext{
.sessionWindow = base::make_weak(_controller),
})
});
}
}
void Inner::selectInlineResult(
int index,
Api::SendOptions options,
bool open) {
const auto item = _mosaic.maybeItemAt(index);
if (!item) {
return;
}
const auto messageSendingFrom = [&]() -> Ui::MessageSendingAnimationFrom {
const auto document = item->getDocument()
? item->getDocument()
: item->getPreviewDocument();
if (options.scheduled
|| item->isFullLine()
|| !document
|| (!document->sticker() && !document->isGifv())) {
return {};
}
using Type = Ui::MessageSendingAnimationFrom::Type;
const auto type = document->sticker()
? Type::Sticker
: document->isGifv()
? Type::Gif
: Type::None;
const auto rect = item->innerContentRect().translated(
_mosaic.findRect(index).topLeft());
return {
.type = type,
.localId = _controller->session().data().nextLocalMessageId(),
.globalStartGeometry = mapToGlobal(rect),
.crop = document->isGifv(),
};
};
if (const auto inlineResult = item->getResult()) {
if (inlineResult->onChoose(item)) {
_resultSelectedCallback({
.result = std::move(inlineResult),
.bot = _inlineBot,
.options = std::move(options),
.messageSendingFrom = messageSendingFrom(),
.open = open,
});
}
}
}
void Inner::mouseMoveEvent(QMouseEvent *e) {
_lastMousePos = e->globalPos();
updateSelected();
}
void Inner::leaveEventHook(QEvent *e) {
clearSelection();
Ui::Tooltip::Hide();
}
void Inner::leaveToChildEvent(QEvent *e, QWidget *child) {
clearSelection();
}
void Inner::enterFromChildEvent(QEvent *e, QWidget *child) {
_lastMousePos = QCursor::pos();
updateSelected();
}
void Inner::contextMenuEvent(QContextMenuEvent *e) {
if (_selected < 0 || _pressed >= 0) {
return;
}
auto details = _sendMenuDetails
? _sendMenuDetails()
: SendMenu::Details();
// inline results don't have effects
details.effectAllowed = false;
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
const auto selected = _selected;
const auto send = crl::guard(this, [=](Api::SendOptions options) {
selectInlineResult(selected, options, false);
});
const auto show = _controller->uiShow();
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu(
_menu,
nullptr, // showForEffect
details,
SendMenu::DefaultCallback(show, send));
const auto item = _mosaic.itemAt(_selected);
if (const auto previewDocument = item->getPreviewDocument()) {
auto callback = [&](
const QString &text,
Fn<void()> &&done,
const style::icon *icon) {
_menu->addAction(text, std::move(done), icon);
};
ChatHelpers::AddGifAction(
std::move(callback),
_controller->uiShow(),
previewDocument);
}
SendMenu::AttachSendMenuEffect(
_menu,
show,
details,
SendMenu::DefaultCallback(show, send));
if (!_menu->empty()) {
_menu->popup(QCursor::pos());
}
}
void Inner::clearSelection() {
if (_selected >= 0) {
ClickHandler::clearActive(_mosaic.itemAt(_selected));
setCursor(style::cur_default);
}
_selected = _pressed = -1;
updateInlineItems();
}
void Inner::hideFinished() {
clearHeavyData();
}
void Inner::clearHeavyData() {
clearInlineRows(false);
for (const auto &[result, layout] : _inlineLayouts) {
layout->unloadHeavyPart();
}
}
void Inner::inlineBotChanged() {
refreshInlineRows(nullptr, nullptr, nullptr, true);
}
void Inner::clearInlineRows(bool resultsDeleted) {
if (resultsDeleted) {
_selected = _pressed = -1;
} else {
clearSelection();
}
_mosaic.clearRows(resultsDeleted);
}
ItemBase *Inner::layoutPrepareInlineResult(std::shared_ptr<Result> result) {
const auto raw = result.get();
auto it = _inlineLayouts.find(raw);
if (it == _inlineLayouts.cend()) {
if (auto layout = ItemBase::createLayout(
this,
std::move(result),
_inlineWithThumb)) {
it = _inlineLayouts.emplace(raw, std::move(layout)).first;
it->second->initDimensions();
} else {
return nullptr;
}
}
if (!it->second->maxWidth()) {
return nullptr;
}
return it->second.get();
}
void Inner::deleteUnusedInlineLayouts() {
if (_mosaic.empty()) { // delete all
_inlineLayouts.clear();
} else {
for (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {
if (i->second->position() < 0) {
i = _inlineLayouts.erase(i);
} else {
++i;
}
}
}
}
void Inner::preloadImages() {
_mosaic.forEach([](not_null<const ItemBase*> item) {
item->preload();
});
}
void Inner::hideInlineRowsPanel() {
clearInlineRows(false);
}
void Inner::clearInlineRowsPanel() {
clearInlineRows(false);
}
void Inner::refreshMosaicOffset() {
const auto top = _switchPmButton
? (_switchPmButton->height() + st::inlineResultsSkip)
: 0;
_mosaic.setPadding(st::emojiPanMargins + QMargins(0, top, 0, 0));
}
void Inner::refreshSwitchPmButton(const CacheEntry *entry) {
if (!entry || entry->switchPmText.isEmpty()) {
_switchPmButton.destroy();
_switchPmStartToken.clear();
_switchPmUrl = QByteArray();
} else {
if (!_switchPmButton) {
_switchPmButton.create(this, nullptr, st::switchPmButton);
_switchPmButton->show();
_switchPmButton->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
_switchPmButton->addClickHandler([=] { switchPm(); });
}
_switchPmButton->setText(rpl::single(entry->switchPmText));
_switchPmStartToken = entry->switchPmStartToken;
_switchPmUrl = entry->switchPmUrl;
const auto buttonTop = st::stickerPanPadding;
_switchPmButton->move(st::inlineResultsLeft - st::roundRadiusSmall, buttonTop);
if (isRestrictedView()) {
_switchPmButton->hide();
}
}
repaintItems();
}
int Inner::refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *entry, bool resultsDeleted) {
_inlineBot = bot;
_inlineQueryPeer = queryPeer;
refreshSwitchPmButton(entry);
refreshMosaicOffset();
auto clearResults = [&] {
if (!entry) {
return true;
}
if (entry->results.empty() && entry->switchPmText.isEmpty()) {
return true;
}
return false;
};
auto clearResultsResult = clearResults(); // Clang workaround.
if (clearResultsResult) {
if (resultsDeleted) {
clearInlineRows(true);
deleteUnusedInlineLayouts();
}
_inlineRowsCleared.fire({});
return 0;
}
clearSelection();
Assert(_inlineBot != 0);
const auto count = int(entry->results.size());
const auto from = validateExistingInlineRows(entry->results);
auto added = 0;
if (count) {
const auto resultItems = entry->results | ranges::views::slice(
from,
count
) | ranges::views::transform([&](const std::shared_ptr<Result> &r) {
return layoutPrepareInlineResult(r);
}) | ranges::views::filter([](const ItemBase *item) {
return item != nullptr;
}) | ranges::to<std::vector<not_null<ItemBase*>>>;
_mosaic.addItems(resultItems);
added = resultItems.size();
preloadImages();
}
auto h = countHeight();
if (h != height()) resize(width(), h);
repaintItems();
_lastMousePos = QCursor::pos();
updateSelected();
return added;
}
int Inner::validateExistingInlineRows(const Results &results) {
const auto until = _mosaic.validateExistingRows([&](
not_null<const ItemBase*> item,
int untilIndex) {
return item->getResult().get() != results[untilIndex].get();
}, results.size());
if (_mosaic.empty()) {
_inlineWithThumb = false;
for (int i = until; i < results.size(); ++i) {
if (results.at(i)->hasThumbDisplay()) {
_inlineWithThumb = true;
break;
}
}
}
return until;
}
void Inner::inlineItemLayoutChanged(const ItemBase *layout) {
if (_selected < 0 || !isVisible()) {
return;
}
if (const auto item = _mosaic.maybeItemAt(_selected)) {
if (layout == item) {
updateSelected();
}
}
}
void Inner::inlineItemRepaint(const ItemBase *layout) {
updateInlineItems();
}
bool Inner::inlineItemVisible(const ItemBase *layout) {
int32 position = layout->position();
if (position < 0 || !isVisible()) {
return false;
}
const auto &[row, column] = ::Layout::IndexToPosition(position);
auto top = st::stickerPanPadding;
for (auto i = 0; i != row; ++i) {
top += _mosaic.rowHeightAt(i);
}
return (top < _visibleBottom)
&& (top + _mosaic.itemAt(row, column)->height() > _visibleTop);
}
Data::FileOrigin Inner::inlineItemFileOrigin() {
return Data::FileOrigin();
}
void Inner::updateSelected() {
if (_pressed >= 0 && !_previewShown) {
return;
}
const auto p = mapFromGlobal(_lastMousePos);
const auto sx = rtl() ? (width() - p.x()) : p.x();
const auto sy = p.y();
const auto &[index, exact, relative] = _mosaic.findByPoint({ sx, sy });
const auto selected = exact ? index : -1;
const auto item = exact ? _mosaic.itemAt(selected).get() : nullptr;
const auto link = exact ? item->getState(relative, {}).link : nullptr;
if (_selected != selected) {
if (const auto s = _mosaic.maybeItemAt(_selected)) {
s->update();
}
_selected = selected;
if (item) {
item->update();
}
if (_previewShown && _selected >= 0 && _pressed != _selected) {
_pressed = _selected;
if (item) {
if (const auto preview = item->getPreviewDocument()) {
_controller->widget()->showMediaPreview(
Data::FileOrigin(),
preview);
} else if (const auto preview = item->getPreviewPhoto()) {
_controller->widget()->showMediaPreview(
Data::FileOrigin(),
preview);
}
}
}
}
if (ClickHandler::setActive(link, item)) {
setCursor(link ? style::cur_pointer : style::cur_default);
Ui::Tooltip::Hide();
}
if (link) {
Ui::Tooltip::Show(1000, this);
}
}
void Inner::showPreview() {
if (_pressed < 0) {
return;
}
if (const auto layout = _mosaic.maybeItemAt(_pressed)) {
if (const auto previewDocument = layout->getPreviewDocument()) {
_previewShown = _controller->widget()->showMediaPreview(
Data::FileOrigin(),
previewDocument);
} else if (const auto previewPhoto = layout->getPreviewPhoto()) {
_previewShown = _controller->widget()->showMediaPreview(
Data::FileOrigin(),
previewPhoto);
}
}
}
void Inner::updateInlineItems() {
const auto now = crl::now();
const auto delay = std::max(
_lastScrolledAt + kMinAfterScrollDelay - now,
_lastUpdatedAt + kMinRepaintDelay - now);
if (delay <= 0) {
repaintItems();
} else if (!_updateInlineItems.isActive()
|| _updateInlineItems.remainingTime() > kMinRepaintDelay) {
_updateInlineItems.callOnce(std::max(delay, kMinRepaintDelay));
}
}
void Inner::repaintItems(crl::time now) {
_lastUpdatedAt = now ? now : crl::now();
update();
}
void Inner::switchPm() {
if (!_inlineBot || !_inlineBot->isBot()) {
return;
} else if (!_switchPmUrl.isEmpty()) {
const auto bot = _inlineBot;
_inlineBot->session().attachWebView().open({
.bot = bot,
.context = { .controller = _controller },
.button = { .url = _switchPmUrl },
.source = InlineBots::WebViewSourceSwitch(),
});
} else {
_inlineBot->botInfo->startToken = _switchPmStartToken;
_inlineBot->botInfo->inlineReturnTo
= _controller->dialogsEntryStateCurrent();
_controller->showPeerHistory(
_inlineBot,
Window::SectionShow::Way::ClearStack,
ShowAndStartBotMsgId);
}
}
void Inner::setSendMenuDetails(Fn<SendMenu::Details()> &&callback) {
_sendMenuDetails = std::move(callback);
}
} // namespace Layout
} // namespace InlineBots

View File

@@ -0,0 +1,190 @@
/*
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/abstract_button.h"
#include "ui/widgets/tooltip.h"
#include "ui/effects/animations.h"
#include "ui/effects/panel_animation.h"
#include "dialogs/dialogs_key.h"
#include "base/timer.h"
#include "mtproto/sender.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "layout/layout_mosaic.h"
namespace Api {
struct SendOptions;
} // namespace Api
namespace Ui {
class ScrollArea;
class IconButton;
class LinkButton;
class RoundButton;
class FlatLabel;
class RippleAnimation;
class PopupMenu;
class PathShiftGradient;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace InlineBots {
class Result;
struct ResultSelected;
} // namespace InlineBots
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace InlineBots {
namespace Layout {
class ItemBase;
using Results = std::vector<std::shared_ptr<Result>>;
struct CacheEntry {
QString nextOffset;
QString switchPmText;
QString switchPmStartToken;
QByteArray switchPmUrl;
Results results;
};
class Inner
: public Ui::RpWidget
, public Ui::AbstractTooltipShower
, public Context {
public:
Inner(QWidget *parent, not_null<Window::SessionController*> controller);
void hideFinished();
void clearSelection();
int refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *results, bool resultsDeleted);
void inlineBotChanged();
void hideInlineRowsPanel();
void clearInlineRowsPanel();
void preloadImages();
void inlineItemLayoutChanged(const ItemBase *layout) override;
void inlineItemRepaint(const ItemBase *layout) override;
bool inlineItemVisible(const ItemBase *layout) override;
Data::FileOrigin inlineItemFileOrigin() override;
int countHeight();
void setResultSelectedCallback(Fn<void(ResultSelected)> callback) {
_resultSelectedCallback = std::move(callback);
}
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
// Ui::AbstractTooltipShower interface.
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
rpl::producer<> inlineRowsCleared() const;
~Inner();
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void leaveToChildEvent(QEvent *e, QWidget *child) override;
void enterFromChildEvent(QEvent *e, QWidget *child) override;
void contextMenuEvent(QContextMenuEvent *e) override;
private:
static constexpr bool kRefreshIconsScrollAnimation = true;
static constexpr bool kRefreshIconsNoAnimation = false;
void switchPm();
void updateSelected();
void checkRestrictedPeer();
bool isRestrictedView();
void clearHeavyData();
void paintInlineItems(Painter &p, const QRect &r);
void refreshSwitchPmButton(const CacheEntry *entry);
void refreshMosaicOffset();
void showPreview();
void updateInlineItems();
void repaintItems(crl::time now = 0);
void clearInlineRows(bool resultsDeleted);
ItemBase *layoutPrepareInlineResult(std::shared_ptr<Result> result);
void updateRestrictedLabelGeometry();
void deleteUnusedInlineLayouts();
int validateExistingInlineRows(const Results &results);
void selectInlineResult(
int index,
Api::SendOptions options,
bool open);
not_null<Window::SessionController*> _controller;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
int _visibleTop = 0;
int _visibleBottom = 0;
UserData *_inlineBot = nullptr;
PeerData *_inlineQueryPeer = nullptr;
crl::time _lastScrolledAt = 0;
crl::time _lastUpdatedAt = 0;
base::Timer _updateInlineItems;
bool _inlineWithThumb = false;
object_ptr<Ui::RoundButton> _switchPmButton = { nullptr };
QString _switchPmStartToken;
QByteArray _switchPmUrl;
object_ptr<Ui::FlatLabel> _restrictedLabel = { nullptr };
QString _restrictedLabelKey;
base::unique_qptr<Ui::PopupMenu> _menu;
Mosaic::Layout::MosaicLayout<InlineBots::Layout::ItemBase> _mosaic;
std::map<Result*, std::unique_ptr<ItemBase>> _inlineLayouts;
rpl::event_stream<> _inlineRowsCleared;
int _selected = -1;
int _pressed = -1;
QPoint _lastMousePos;
base::Timer _previewTimer;
bool _previewShown = false;
Fn<void(ResultSelected)> _resultSelectedCallback;
Fn<SendMenu::Details()> _sendMenuDetails;
};
} // namespace Layout
} // namespace InlineBots

View File

@@ -0,0 +1,529 @@
/*
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 "inline_bots/inline_results_widget.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "inline_bots/inline_bot_result.h"
#include "inline_bots/inline_results_inner.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/scroll_area.h"
#include "ui/image/image_prepare.h"
#include "ui/cached_round_corners.h"
#include "ui/ui_utility.h"
#include "styles/style_chat_helpers.h"
namespace InlineBots {
namespace Layout {
namespace {
constexpr auto kInlineBotRequestDelay = 400;
} // namespace
Widget::Widget(
QWidget *parent,
not_null<Window::SessionController*> controller)
: RpWidget(parent)
, _controller(controller)
, _api(&_controller->session().mtp())
, _contentMaxHeight(st::emojiPanMaxHeight)
, _contentHeight(_contentMaxHeight)
, _scroll(this, st::inlineBotsScroll)
, _innerRounding(Ui::PrepareCornerPixmaps(
ImageRoundRadius::Small,
st::emojiPanBg))
, _inlineRequestTimer([=] { onInlineRequest(); }) {
resize(QRect(0, 0, st::emojiPanWidth, _contentHeight).marginsAdded(innerPadding()).size());
_width = width();
_height = height();
_scroll->resize(st::emojiPanWidth - st::roundRadiusSmall, _contentHeight);
_scroll->move(verticalRect().topLeft());
_inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, controller));
_inner->moveToLeft(0, 0, _scroll->width());
_scroll->scrolls(
) | rpl::on_next([=] {
onScroll();
}, lifetime());
_inner->inlineRowsCleared(
) | rpl::on_next([=] {
hideAnimated();
_inner->clearInlineRowsPanel();
}, lifetime());
style::PaletteChanged(
) | rpl::on_next([=] {
_innerRounding = Ui::PrepareCornerPixmaps(
ImageRoundRadius::Small,
st::emojiPanBg);
}, lifetime());
macWindowDeactivateEvents(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::on_next([=] {
leaveEvent(nullptr);
}, lifetime());
// Inner widget has OpaquePaintEvent attribute so it doesn't repaint on scroll.
// But we should force it to repaint so that GIFs will continue to animate without update() calls.
// We do that by creating a transparent widget above our _inner.
auto forceRepaintOnScroll = object_ptr<RpWidget>(this);
forceRepaintOnScroll->setGeometry(innerRect().x() + st::roundRadiusSmall, innerRect().y() + st::roundRadiusSmall, st::roundRadiusSmall, st::roundRadiusSmall);
forceRepaintOnScroll->setAttribute(Qt::WA_TransparentForMouseEvents);
forceRepaintOnScroll->show();
setMouseTracking(true);
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
void Widget::moveBottom(int bottom) {
_bottom = bottom;
updateContentHeight();
}
void Widget::updateContentHeight() {
auto addedHeight = innerPadding().top() + innerPadding().bottom();
auto wantedContentHeight = qRound(st::emojiPanHeightRatio * _bottom) - addedHeight;
auto contentHeight = std::clamp(
wantedContentHeight,
st::inlineResultsMinHeight,
st::inlineResultsMaxHeight);
accumulate_min(contentHeight, _bottom - addedHeight);
accumulate_min(contentHeight, _contentMaxHeight);
auto resultTop = _bottom - addedHeight - contentHeight;
if (contentHeight == _contentHeight) {
move(x(), resultTop);
return;
}
auto was = _contentHeight;
_contentHeight = contentHeight;
resize(QRect(0, 0, innerRect().width(), _contentHeight).marginsAdded(innerPadding()).size());
_height = height();
moveToLeft(0, resultTop);
if (was > _contentHeight) {
_scroll->resize(_scroll->width(), _contentHeight);
auto scrollTop = _scroll->scrollTop();
_inner->setVisibleTopBottom(scrollTop, scrollTop + _contentHeight);
} else {
auto scrollTop = _scroll->scrollTop();
_inner->setVisibleTopBottom(scrollTop, scrollTop + _contentHeight);
_scroll->resize(_scroll->width(), _contentHeight);
}
update();
}
void Widget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto opacityAnimating = _a_opacity.animating();
auto showAnimating = _a_show.animating();
if (_showAnimation && !showAnimating) {
_showAnimation.reset();
if (!opacityAnimating) {
showChildren();
}
}
if (showAnimating) {
Assert(_showAnimation != nullptr);
if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
_showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);
}
} else if (opacityAnimating) {
p.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));
p.drawPixmap(0, 0, _cache);
} else if (_hiding || isHidden()) {
hideFinished();
} else {
if (!_cache.isNull()) _cache = QPixmap();
if (!_inPanelGrab) Ui::Shadow::paint(p, innerRect(), width(), st::emojiPanAnimation.shadow);
paintContent(p);
}
}
void Widget::paintContent(QPainter &p) {
auto inner = innerRect();
const auto radius = st::roundRadiusSmall;
const auto top = Ui::CornersPixmaps{
.p = { _innerRounding.p[0], _innerRounding.p[1], QPixmap(), QPixmap() },
};
Ui::FillRoundRect(p, inner.x(), inner.y(), inner.width(), radius, st::emojiPanBg, top);
const auto bottom = Ui::CornersPixmaps{
.p = { QPixmap(), QPixmap(), _innerRounding.p[2], _innerRounding.p[3] },
};
Ui::FillRoundRect(p, inner.x(), inner.y() + inner.height() - radius, inner.width(), radius, st::emojiPanBg, bottom);
auto horizontal = horizontalRect();
auto sidesTop = horizontal.y();
auto sidesHeight = horizontal.height();
p.fillRect(myrtlrect(inner.x() + inner.width() - st::emojiScroll.width, sidesTop, st::emojiScroll.width, sidesHeight), st::emojiPanBg);
p.fillRect(myrtlrect(inner.x(), sidesTop, st::roundRadiusSmall, sidesHeight), st::emojiPanBg);
}
void Widget::moveByBottom() {
updateContentHeight();
}
void Widget::hideFast() {
if (isHidden()) return;
_hiding = false;
_a_opacity.stop();
hideFinished();
}
void Widget::opacityAnimationCallback() {
update();
if (!_a_opacity.animating()) {
if (_hiding) {
_hiding = false;
hideFinished();
} else if (!_a_show.animating()) {
showChildren();
}
}
}
void Widget::prepareCache() {
if (_a_opacity.animating()) return;
auto showAnimation = base::take(_a_show);
auto showAnimationData = base::take(_showAnimation);
showChildren();
_cache = Ui::GrabWidget(this);
_showAnimation = base::take(showAnimationData);
_a_show = base::take(showAnimation);
if (_a_show.animating()) {
hideChildren();
}
}
void Widget::startOpacityAnimation(bool hiding) {
_hiding = false;
prepareCache();
_hiding = hiding;
hideChildren();
_a_opacity.start([this] { opacityAnimationCallback(); }, _hiding ? 1. : 0., _hiding ? 0. : 1., st::emojiPanDuration);
}
void Widget::startShowAnimation() {
if (!_a_show.animating()) {
auto cache = base::take(_cache);
auto opacityAnimation = base::take(_a_opacity);
showChildren();
auto image = grabForPanelAnimation();
_a_opacity = base::take(opacityAnimation);
_cache = base::take(cache);
_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, Ui::PanelAnimation::Origin::BottomLeft);
auto inner = rect().marginsRemoved(st::emojiPanMargins);
_showAnimation->setFinalImage(
std::move(image),
QRect(
inner.topLeft() * style::DevicePixelRatio(),
inner.size() * style::DevicePixelRatio()));
_showAnimation->setCornerMasks(Images::CornersMask(ImageRoundRadius::Small));
_showAnimation->start();
}
hideChildren();
_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);
}
QImage Widget::grabForPanelAnimation() {
Ui::SendPendingMoveResizeEvents(this);
auto result = QImage(
size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
_inPanelGrab = true;
render(&result);
_inPanelGrab = false;
return result;
}
void Widget::setResultSelectedCallback(Fn<void(ResultSelected)> callback) {
_inner->setResultSelectedCallback(std::move(callback));
}
void Widget::setSendMenuDetails(Fn<SendMenu::Details()> &&callback) {
_inner->setSendMenuDetails(std::move(callback));
}
void Widget::hideAnimated() {
if (isHidden()) return;
if (_hiding) return;
startOpacityAnimation(true);
}
Widget::~Widget() = default;
void Widget::hideFinished() {
hide();
_controller->disableGifPauseReason(
Window::GifPauseReason::InlineResults);
_inner->hideFinished();
_a_show.stop();
_showAnimation.reset();
_cache = QPixmap();
_hiding = false;
_scroll->scrollToY(0);
}
void Widget::showAnimated() {
showStarted();
}
void Widget::showStarted() {
if (isHidden()) {
recountContentMaxHeight();
_inner->preloadImages();
show();
_controller->enableGifPauseReason(
Window::GifPauseReason::InlineResults);
startShowAnimation();
} else if (_hiding) {
startOpacityAnimation(false);
}
}
void Widget::onScroll() {
auto st = _scroll->scrollTop();
if (st + _scroll->height() > _scroll->scrollTopMax()) {
onInlineRequest();
}
_inner->setVisibleTopBottom(st, st + _scroll->height());
}
style::margins Widget::innerPadding() const {
return st::emojiPanMargins;
}
QRect Widget::innerRect() const {
return rect().marginsRemoved(innerPadding());
}
QRect Widget::horizontalRect() const {
return innerRect().marginsRemoved(style::margins(0, st::roundRadiusSmall, 0, st::roundRadiusSmall));
}
QRect Widget::verticalRect() const {
return innerRect().marginsRemoved(style::margins(st::roundRadiusSmall, 0, st::roundRadiusSmall, 0));
}
void Widget::clearInlineBot() {
inlineBotChanged();
}
bool Widget::overlaps(const QRect &globalRect) const {
if (isHidden() || !_cache.isNull()) return false;
auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());
auto inner = rect().marginsRemoved(st::emojiPanMargins);
return inner.marginsRemoved(QMargins(st::roundRadiusSmall, 0, st::roundRadiusSmall, 0)).contains(testRect)
|| inner.marginsRemoved(QMargins(0, st::roundRadiusSmall, 0, st::roundRadiusSmall)).contains(testRect);
}
void Widget::inlineBotChanged() {
if (!_inlineBot) {
return;
}
if (!isHidden() && !_hiding) {
hideAnimated();
}
_api.request(base::take(_inlineRequestId)).cancel();
_inlineQuery = _inlineNextQuery = _inlineNextOffset = QString();
_inlineBot = nullptr;
_inlineCache.clear();
_inner->inlineBotChanged();
_inner->hideInlineRowsPanel();
_requesting.fire(false);
}
void Widget::inlineResultsDone(const MTPmessages_BotResults &result) {
_inlineRequestId = 0;
_requesting.fire(false);
auto it = _inlineCache.find(_inlineQuery);
auto adding = (it != _inlineCache.cend());
if (result.type() == mtpc_messages_botResults) {
auto &d = result.c_messages_botResults();
_controller->session().data().processUsers(d.vusers());
auto &v = d.vresults().v;
auto queryId = d.vquery_id().v;
if (it == _inlineCache.cend()) {
it = _inlineCache.emplace(
_inlineQuery,
std::make_unique<CacheEntry>()).first;
}
auto entry = it->second.get();
entry->nextOffset = qs(d.vnext_offset().value_or_empty());
if (const auto switchPm = d.vswitch_pm()) {
entry->switchPmText = qs(switchPm->data().vtext());
entry->switchPmStartToken = qs(switchPm->data().vstart_param());
entry->switchPmUrl = QByteArray();
} else if (const auto switchWebView = d.vswitch_webview()) {
entry->switchPmText = qs(switchWebView->data().vtext());
entry->switchPmStartToken = QString();
entry->switchPmUrl = switchWebView->data().vurl().v;
}
if (const auto count = v.size()) {
entry->results.reserve(entry->results.size() + count);
}
auto added = 0;
for (const auto &res : v) {
auto result = InlineBots::Result::Create(
&_controller->session(),
queryId,
res);
if (result) {
++added;
entry->results.push_back(std::move(result));
}
}
if (!added) {
entry->nextOffset = QString();
}
} else if (adding) {
it->second->nextOffset = QString();
}
if (!showInlineRows(!adding)) {
it->second->nextOffset = QString();
}
onScroll();
}
void Widget::queryInlineBot(UserData *bot, PeerData *peer, QString query) {
bool force = false;
_inlineQueryPeer = peer;
if (bot != _inlineBot) {
inlineBotChanged();
_inlineBot = bot;
force = true;
}
if (_inlineQuery != query || force) {
if (_inlineRequestId) {
_api.request(_inlineRequestId).cancel();
_inlineRequestId = 0;
_requesting.fire(false);
}
if (_inlineCache.find(query) != _inlineCache.cend()) {
_inlineRequestTimer.cancel();
_inlineQuery = _inlineNextQuery = query;
showInlineRows(true);
} else {
_inlineNextQuery = query;
_inlineRequestTimer.callOnce(kInlineBotRequestDelay);
}
}
}
void Widget::onInlineRequest() {
if (_inlineRequestId || !_inlineBot || !_inlineQueryPeer) return;
_inlineQuery = _inlineNextQuery;
QString nextOffset;
auto it = _inlineCache.find(_inlineQuery);
if (it != _inlineCache.cend()) {
nextOffset = it->second->nextOffset;
if (nextOffset.isEmpty()) {
return;
}
}
_requesting.fire(true);
_inlineRequestId = _api.request(MTPmessages_GetInlineBotResults(
MTP_flags(0),
_inlineBot->inputUser(),
_inlineQueryPeer->input(),
MTPInputGeoPoint(),
MTP_string(_inlineQuery),
MTP_string(nextOffset)
)).done([=](const MTPmessages_BotResults &result) {
inlineResultsDone(result);
}).fail([=] {
// show error?
_requesting.fire(false);
_inlineRequestId = 0;
}).handleAllErrors().send();
}
bool Widget::refreshInlineRows(int *added) {
auto it = _inlineCache.find(_inlineQuery);
const CacheEntry *entry = nullptr;
if (it != _inlineCache.cend()) {
if (!it->second->results.empty() || !it->second->switchPmText.isEmpty()) {
entry = it->second.get();
}
_inlineNextOffset = it->second->nextOffset;
}
if (!entry) prepareCache();
auto result = _inner->refreshInlineRows(_inlineQueryPeer, _inlineBot, entry, false);
if (added) *added = result;
return (entry != nullptr);
}
int Widget::showInlineRows(bool newResults) {
auto added = 0;
auto clear = !refreshInlineRows(&added);
if (newResults) {
_scroll->scrollToY(0);
}
auto hidden = isHidden();
if (!hidden && !clear) {
recountContentMaxHeight();
}
if (clear) {
if (!hidden) {
hideAnimated();
} else if (!_hiding) {
_cache = QPixmap(); // clear after refreshInlineRows()
}
} else {
if (hidden || _hiding) {
showAnimated();
}
}
return added;
}
void Widget::recountContentMaxHeight() {
_contentMaxHeight = _inner->countHeight();
updateContentHeight();
}
} // namespace Layout
} // namespace InlineBots

View File

@@ -0,0 +1,161 @@
/*
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/abstract_button.h"
#include "ui/cached_round_corners.h"
#include "ui/widgets/tooltip.h"
#include "ui/effects/animations.h"
#include "ui/effects/panel_animation.h"
#include "base/timer.h"
#include "mtproto/sender.h"
#include "inline_bots/inline_bot_layout_item.h"
namespace Api {
struct SendOptions;
} // namespace Api
namespace Ui {
class ScrollArea;
class IconButton;
class LinkButton;
class RoundButton;
class FlatLabel;
class RippleAnimation;
class PopupMenu;
} // namespace Ui
namespace Dialogs {
struct EntryState;
} // namespace Dialogs
namespace Window {
class SessionController;
} // namespace Window
namespace InlineBots {
class Result;
struct ResultSelected;
} // namespace InlineBots
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace InlineBots {
namespace Layout {
struct CacheEntry;
class Inner;
class Widget : public Ui::RpWidget {
public:
Widget(QWidget *parent, not_null<Window::SessionController*> controller);
~Widget();
void moveBottom(int bottom);
void hideFast();
bool hiding() const {
return _hiding;
}
void queryInlineBot(UserData *bot, PeerData *peer, QString query);
void clearInlineBot();
bool overlaps(const QRect &globalRect) const;
void showAnimated();
void hideAnimated();
void setResultSelectedCallback(Fn<void(ResultSelected)> callback);
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
[[nodiscard]] rpl::producer<bool> requesting() const {
return _requesting.events();
}
protected:
void paintEvent(QPaintEvent *e) override;
private:
void moveByBottom();
void paintContent(QPainter &p);
style::margins innerPadding() const;
void onScroll();
void onInlineRequest();
// Rounded rect which has shadow around it.
QRect innerRect() const;
// Inner rect with removed st::roundRadiusSmall from top and bottom.
// This one is allowed to be not rounded.
QRect horizontalRect() const;
// Inner rect with removed st::roundRadiusSmall from left and right.
// This one is allowed to be not rounded.
QRect verticalRect() const;
QImage grabForPanelAnimation();
void startShowAnimation();
void startOpacityAnimation(bool hiding);
void prepareCache();
class Container;
void opacityAnimationCallback();
void hideFinished();
void showStarted();
void updateContentHeight();
void inlineBotChanged();
int showInlineRows(bool newResults);
void recountContentMaxHeight();
bool refreshInlineRows(int *added = nullptr);
void inlineResultsDone(const MTPmessages_BotResults &result);
const not_null<Window::SessionController*> _controller;
MTP::Sender _api;
int _contentMaxHeight = 0;
int _contentHeight = 0;
int _width = 0;
int _height = 0;
int _bottom = 0;
std::unique_ptr<Ui::PanelAnimation> _showAnimation;
Ui::Animations::Simple _a_show;
bool _hiding = false;
QPixmap _cache;
Ui::Animations::Simple _a_opacity;
bool _inPanelGrab = false;
object_ptr<Ui::ScrollArea> _scroll;
QPointer<Inner> _inner;
Ui::CornersPixmaps _innerRounding;
std::map<QString, std::unique_ptr<CacheEntry>> _inlineCache;
base::Timer _inlineRequestTimer;
UserData *_inlineBot = nullptr;
PeerData *_inlineQueryPeer = nullptr;
QString _inlineQuery, _inlineNextQuery, _inlineNextOffset;
mtpRequestId _inlineRequestId = 0;
rpl::event_stream<bool> _requesting;
};
} // namespace Layout
} // namespace InlineBots