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
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:
2749
Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
Normal file
2749
Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
Normal file
File diff suppressed because it is too large
Load Diff
475
Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
Normal file
475
Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
Normal 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
|
||||
240
Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.cpp
Normal file
240
Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.cpp
Normal 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
|
||||
@@ -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
|
||||
421
Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp
Normal file
421
Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp
Normal 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
|
||||
105
Telegram/SourceFiles/inline_bots/inline_bot_downloads.h
Normal file
105
Telegram/SourceFiles/inline_bots/inline_bot_downloads.h
Normal 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
|
||||
1722
Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
Normal file
1722
Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
Normal file
File diff suppressed because it is too large
Load Diff
436
Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h
Normal file
436
Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h
Normal 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
|
||||
267
Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
Normal file
267
Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
Normal 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
|
||||
167
Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
Normal file
167
Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
Normal 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
|
||||
526
Telegram/SourceFiles/inline_bots/inline_bot_result.cpp
Normal file
526
Telegram/SourceFiles/inline_bots/inline_bot_result.cpp
Normal 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
|
||||
142
Telegram/SourceFiles/inline_bots/inline_bot_result.h
Normal file
142
Telegram/SourceFiles/inline_bots/inline_bot_result.h
Normal 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
|
||||
156
Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
Normal file
156
Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp
Normal 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
|
||||
342
Telegram/SourceFiles/inline_bots/inline_bot_send_data.h
Normal file
342
Telegram/SourceFiles/inline_bots/inline_bot_send_data.h
Normal 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
|
||||
181
Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp
Normal file
181
Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp
Normal 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
|
||||
52
Telegram/SourceFiles/inline_bots/inline_bot_storage.h
Normal file
52
Telegram/SourceFiles/inline_bots/inline_bot_storage.h
Normal 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
|
||||
753
Telegram/SourceFiles/inline_bots/inline_results_inner.cpp
Normal file
753
Telegram/SourceFiles/inline_bots/inline_results_inner.cpp
Normal 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
|
||||
190
Telegram/SourceFiles/inline_bots/inline_results_inner.h
Normal file
190
Telegram/SourceFiles/inline_bots/inline_results_inner.h
Normal 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
|
||||
529
Telegram/SourceFiles/inline_bots/inline_results_widget.cpp
Normal file
529
Telegram/SourceFiles/inline_bots/inline_results_widget.cpp
Normal 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
|
||||
161
Telegram/SourceFiles/inline_bots/inline_results_widget.h
Normal file
161
Telegram/SourceFiles/inline_bots/inline_results_widget.h
Normal 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
|
||||
Reference in New Issue
Block a user