mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-02-20 07:07:48 +00:00
Implement bot downloads list UI.
This commit is contained in:
parent
ef521624a0
commit
338122793c
@ -3431,6 +3431,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_bot_download_file" = "Download File";
|
||||
"lng_bot_download_file_sure" = "{bot} suggests you download the following file:";
|
||||
"lng_bot_download_file_button" = "Download";
|
||||
"lng_bot_download_starting" = "Starting...";
|
||||
"lng_bot_download_failed" = "Failed. {retry}";
|
||||
"lng_bot_download_retry" = "Retry";
|
||||
|
||||
"lng_bot_status_users#one" = "{count} monthly user";
|
||||
"lng_bot_status_users#other" = "{count} monthly users";
|
||||
|
@ -1361,7 +1361,7 @@ void WebViewInstance::show(ShowArgs &&args) {
|
||||
.menuButtons = buttons,
|
||||
.fullscreen = args.fullscreen,
|
||||
.allowClipboardRead = allowClipboardRead,
|
||||
.downloadsProgress = downloads->downloadsProgress(_bot),
|
||||
.downloadsProgress = downloads->progress(_bot),
|
||||
});
|
||||
started(args.queryId);
|
||||
|
||||
@ -1450,6 +1450,17 @@ Webview::ThemeParams WebViewInstance::botThemeParams() {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto WebViewInstance::botDownloads(bool forceCheck)
|
||||
-> const std::vector<Ui::BotWebView::DownloadsEntry> & {
|
||||
return _session->attachWebView().downloads().list(_bot, forceCheck);
|
||||
}
|
||||
|
||||
void WebViewInstance::botDownloadsAction(
|
||||
uint32 id,
|
||||
Ui::BotWebView::DownloadsAction type) {
|
||||
_session->attachWebView().downloads().action(_bot, id, type);
|
||||
}
|
||||
|
||||
bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
|
||||
const auto local = Core::TryConvertUrlToLocal(uri);
|
||||
if (Core::InternalPassportLink(local)) {
|
||||
|
@ -28,6 +28,7 @@ class DropdownMenu;
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
class Panel;
|
||||
struct DownloadsEntry;
|
||||
} // namespace Ui::BotWebView
|
||||
|
||||
namespace Main {
|
||||
@ -250,6 +251,11 @@ private:
|
||||
-> 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;
|
||||
|
@ -7,6 +7,7 @@ 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"
|
||||
@ -15,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "storage/file_download_web.h"
|
||||
#include "storage/serialize_common.h"
|
||||
#include "storage/storage_account.h"
|
||||
#include "ui/chat/attach/attach_bot_webview.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"
|
||||
@ -24,6 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtCore/QDataStream>
|
||||
|
||||
#include "base/call_delayed.h"
|
||||
|
||||
namespace InlineBots {
|
||||
namespace {
|
||||
|
||||
@ -37,7 +40,10 @@ Downloads::Downloads(not_null<Main::Session*> session)
|
||||
: _session(session) {
|
||||
}
|
||||
|
||||
Downloads::~Downloads() = default;
|
||||
Downloads::~Downloads() {
|
||||
base::take(_loaders);
|
||||
base::take(_lists);
|
||||
}
|
||||
|
||||
DownloadId Downloads::start(StartArgs &&args) {
|
||||
read();
|
||||
@ -50,14 +56,28 @@ DownloadId Downloads::start(StartArgs &&args) {
|
||||
.url = std::move(args.url),
|
||||
.path = std::move(args.path),
|
||||
});
|
||||
auto &entry = list.back();
|
||||
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::start_with_next_error_done([=] {
|
||||
progress(botId, id);
|
||||
@ -66,9 +86,8 @@ DownloadId Downloads::start(StartArgs &&args) {
|
||||
}, [=] {
|
||||
done(botId, id);
|
||||
}, loader.loader->lifetime());
|
||||
loader.loader->start();
|
||||
|
||||
return id;
|
||||
loader.loader->start();
|
||||
}
|
||||
|
||||
void Downloads::progress(PeerId botId, DownloadId id) {
|
||||
@ -87,22 +106,18 @@ void Downloads::progress(PeerId botId, DownloadId id) {
|
||||
&DownloadsEntry::id);
|
||||
Assert(j != end(list));
|
||||
|
||||
if (total < 0
|
||||
|| ready > total
|
||||
|| (j->total && j->total != total)) {
|
||||
fail(botId, id);
|
||||
return;
|
||||
} else if (ready > total) {
|
||||
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) {
|
||||
void Downloads::fail(PeerId botId, DownloadId id, bool cancel) {
|
||||
const auto i = _loaders.find(id);
|
||||
if (i == end(_loaders)) {
|
||||
return;
|
||||
@ -117,7 +132,16 @@ void Downloads::fail(PeerId botId, DownloadId id) {
|
||||
id,
|
||||
&DownloadsEntry::id);
|
||||
Assert(k != end(list));
|
||||
k->ready = -1;
|
||||
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) {
|
||||
@ -125,19 +149,20 @@ void Downloads::done(PeerId botId, DownloadId 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));
|
||||
|
||||
const auto total = i->second.loader->fullSize();
|
||||
if (total <= 0 || (j->total && j->total != total)) {
|
||||
fail(botId, id);
|
||||
return;
|
||||
}
|
||||
_loaders.erase(i);
|
||||
j->loading = 0;
|
||||
|
||||
applyProgress(botId, id, total, total);
|
||||
}
|
||||
@ -147,7 +172,7 @@ void Downloads::applyProgress(
|
||||
DownloadId id,
|
||||
int64 total,
|
||||
int64 ready) {
|
||||
Expects(total > 0);
|
||||
Expects(total >= 0);
|
||||
Expects(ready >= 0 && ready <= total);
|
||||
|
||||
auto &list = _lists[botId].list;
|
||||
@ -157,52 +182,117 @@ void Downloads::applyProgress(
|
||||
&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();
|
||||
if (!j->total) {
|
||||
j->total = total;
|
||||
current.total += total;
|
||||
auto subtract = int64(0);
|
||||
if (current.ready == current.total) {
|
||||
subtract = current.ready;
|
||||
}
|
||||
if (j->ready != ready) {
|
||||
const auto delta = ready - j->ready;
|
||||
j->ready = 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 == ready) {
|
||||
if (total > 0 && total == ready) {
|
||||
write();
|
||||
}
|
||||
|
||||
progress = current;
|
||||
if (current.ready == current.total) {
|
||||
progress = DownloadsProgress();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void Downloads::cancel(DownloadId id) {
|
||||
const auto i = _loaders.find(id);
|
||||
if (i == end(_loaders)) {
|
||||
return;
|
||||
}
|
||||
const auto botId = i->second.botId;
|
||||
fail(botId, id);
|
||||
|
||||
auto &list = _lists[botId].list;
|
||||
list.erase(
|
||||
ranges::remove(list, id, &DownloadsEntry::id),
|
||||
end(list));
|
||||
|
||||
auto &progress = _progressView[botId];
|
||||
progress.force_assign(progress.current());
|
||||
}
|
||||
|
||||
[[nodiscard]] auto Downloads::downloadsProgress(not_null<UserData*> bot)
|
||||
[[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()) {
|
||||
@ -239,8 +329,9 @@ void Downloads::read() {
|
||||
list.list.reserve(count);
|
||||
for (auto j = 0; j != count; ++j) {
|
||||
auto entry = DownloadsEntry();
|
||||
stream >> entry.url >> entry.path >> entry.total;
|
||||
entry.ready = entry.total;
|
||||
auto size = int64();
|
||||
stream >> entry.url >> entry.path >> size;
|
||||
entry.total = entry.ready = size;
|
||||
entry.id = ++_autoIncrementId;
|
||||
list.list.push_back(std::move(entry));
|
||||
}
|
||||
@ -259,7 +350,7 @@ void Downloads::write() {
|
||||
if (entry.total > 0 && entry.ready == entry.total) {
|
||||
size += Serialize::stringSize(entry.url)
|
||||
+ Serialize::stringSize(entry.path)
|
||||
+ sizeof(quint64); // total
|
||||
+ sizeof(quint64); // size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_bot_webview.h"
|
||||
|
||||
class webFileLoader;
|
||||
|
||||
namespace Main {
|
||||
@ -17,23 +19,13 @@ namespace Ui {
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
struct DownloadsProgress;
|
||||
} // namespace Ui::BotWebView
|
||||
|
||||
namespace InlineBots {
|
||||
|
||||
using DownloadId = uint32;
|
||||
|
||||
using ::Ui::BotWebView::DownloadsProgress;
|
||||
|
||||
struct DownloadsEntry {
|
||||
DownloadId id = 0;
|
||||
QString url;
|
||||
QString path;
|
||||
uint64 ready = 0;
|
||||
uint64 total = 0;
|
||||
};
|
||||
using ::Ui::BotWebView::DownloadsEntry;
|
||||
using ::Ui::BotWebView::DownloadsAction;
|
||||
|
||||
class Downloads final {
|
||||
public:
|
||||
@ -47,15 +39,20 @@ public:
|
||||
};
|
||||
uint32 start(StartArgs &&args); // Returns download id.
|
||||
|
||||
void cancel(DownloadId id);
|
||||
void action(
|
||||
not_null<UserData*> bot,
|
||||
DownloadId id,
|
||||
DownloadsAction type);
|
||||
|
||||
[[nodiscard]] auto downloadsProgress(not_null<UserData*> bot)
|
||||
-> rpl::producer<DownloadsProgress>;
|
||||
[[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;
|
||||
bool checked = false;
|
||||
};
|
||||
struct Loader {
|
||||
std::unique_ptr<webFileLoader> loader;
|
||||
@ -65,14 +62,23 @@ private:
|
||||
void read();
|
||||
void write();
|
||||
|
||||
void load(
|
||||
PeerId botId,
|
||||
DownloadId id,
|
||||
DownloadsEntry &entry);
|
||||
void progress(PeerId botId, DownloadId id);
|
||||
void fail(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;
|
||||
|
||||
|
@ -150,3 +150,7 @@ botWebViewBottomButton: RoundButton(paymentsPanelSubmit) {
|
||||
}
|
||||
textTop: 11px;
|
||||
}
|
||||
botWebViewRadialStroke: 3px;
|
||||
botWebViewMenu: PopupMenu(popupMenuWithIcons) {
|
||||
maxHeight: 360px;
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ void WebLoadManager::progress(
|
||||
).arg(status));
|
||||
failed(id, reply);
|
||||
} else {
|
||||
notify(id, reply, ready, total);
|
||||
notify(id, reply, ready, std::max(ready, total));
|
||||
}
|
||||
}
|
||||
|
||||
|
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
@ -0,0 +1,268 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_bot_downloads.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
namespace {
|
||||
|
||||
class Action final : public Menu::ItemBase {
|
||||
public:
|
||||
Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const DownloadsEntry &entry,
|
||||
Fn<void(DownloadsAction)> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override { return _dummyAction; }
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
void refresh(const DownloadsEntry &entry);
|
||||
|
||||
private:
|
||||
QPoint prepareRippleStartPosition() const override {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
QImage prepareRippleMask() const override {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
int contentHeight() const override { return _height; }
|
||||
|
||||
void prepare();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st = st::defaultMenu;
|
||||
|
||||
DownloadsEntry _entry;
|
||||
Text::String _name;
|
||||
FlatLabel _progress;
|
||||
IconButton _cancel;
|
||||
int _textWidth = 0;
|
||||
const int _height;
|
||||
};
|
||||
|
||||
Action::Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const DownloadsEntry &entry,
|
||||
Fn<void(DownloadsAction)> callback)
|
||||
: ItemBase(parent, st::defaultMenu)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _progress(this, st::botDownloadProgress)
|
||||
, _cancel(this, st::botDownloadCancel)
|
||||
, _height(st::ttlItemPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st::ttlItemTimerFont->height
|
||||
+ st::ttlItemPadding.bottom()) {
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback([=] {
|
||||
if (isEnabled()) {
|
||||
callback(DownloadsAction::Open);
|
||||
}
|
||||
});
|
||||
_cancel.setClickedCallback([=] {
|
||||
callback(DownloadsAction::Cancel);
|
||||
});
|
||||
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
widthValue() | rpl::start_with_next([=](int width) {
|
||||
_progress.moveToLeft(
|
||||
_st.itemPadding.left(),
|
||||
st::ttlItemPadding.top() + _st.itemStyle.font->height,
|
||||
width);
|
||||
|
||||
_cancel.moveToRight(
|
||||
_st.itemPadding.right(),
|
||||
(_height - _cancel.height()) / 2,
|
||||
width);
|
||||
}, lifetime());
|
||||
|
||||
_progress.setClickHandlerFilter([=](const auto &...) {
|
||||
callback(DownloadsAction::Retry);
|
||||
return false;
|
||||
});
|
||||
|
||||
enableMouseSelecting();
|
||||
refresh(entry);
|
||||
}
|
||||
|
||||
void Action::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_name.drawLeftElided(
|
||||
p,
|
||||
_st.itemPadding.left(),
|
||||
st::ttlItemPadding.top(),
|
||||
_textWidth,
|
||||
width());
|
||||
|
||||
_progress.setTextColorOverride(
|
||||
selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
|
||||
}
|
||||
|
||||
void Action::prepare() {
|
||||
const auto filenameWidth = _name.maxWidth();
|
||||
const auto progressWidth = _progress.textMaxWidth();
|
||||
const auto &padding = _st.itemPadding;
|
||||
|
||||
const auto goodWidth = std::max(filenameWidth, progressWidth);
|
||||
|
||||
// Example max width: "4000 / 4000 MB"
|
||||
const auto countWidth = [&](const QString &text) {
|
||||
return st::ttlItemTimerFont->width(text);
|
||||
};
|
||||
const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
|
||||
tr::now,
|
||||
lt_ready,
|
||||
"0000",
|
||||
lt_total,
|
||||
"0000",
|
||||
lt_mb,
|
||||
"MB"));
|
||||
const auto maxStartingWidth = countWidth(
|
||||
tr::lng_bot_download_starting(tr::now));
|
||||
const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
|
||||
tr::now,
|
||||
lt_retry,
|
||||
tr::lng_bot_download_retry(tr::now)));
|
||||
|
||||
const auto cancel = _cancel.width() + padding.right();
|
||||
const auto paddings = padding.left() + padding.right() + cancel;
|
||||
const auto w = std::clamp(
|
||||
paddings + std::max({
|
||||
goodWidth,
|
||||
maxProgressWidth,
|
||||
maxStartingWidth,
|
||||
maxFailedWidth,
|
||||
}),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
_textWidth = w - paddings;
|
||||
_progress.resizeToWidth(_textWidth);
|
||||
setMinWidth(w);
|
||||
update();
|
||||
}
|
||||
|
||||
bool Action::isEnabled() const {
|
||||
return _entry.total > 0 && _entry.ready == _entry.total;
|
||||
}
|
||||
|
||||
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
void Action::refresh(const DownloadsEntry &entry) {
|
||||
_entry = entry;
|
||||
const auto filename = entry.path.split('/').last();
|
||||
_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
|
||||
|
||||
const auto progressText = (entry.total && entry.total == entry.ready)
|
||||
? TextWithEntities{ FormatSizeText(entry.total) }
|
||||
: entry.loading
|
||||
? (entry.total
|
||||
? TextWithEntities{
|
||||
FormatProgressText(entry.ready, entry.total),
|
||||
}
|
||||
: tr::lng_bot_download_starting(tr::now, Text::WithEntities))
|
||||
: tr::lng_bot_download_failed(
|
||||
tr::now,
|
||||
lt_retry,
|
||||
Text::Link(tr::lng_bot_download_retry(tr::now)),
|
||||
Text::WithEntities);
|
||||
_progress.setMarkedText(progressText);
|
||||
|
||||
const auto enabled = isEnabled();
|
||||
setCursor(enabled ? style::cur_pointer : style::cur_default);
|
||||
_cancel.setVisible(!enabled && _entry.loading);
|
||||
_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
|
||||
|
||||
prepare();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
|
||||
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||
Fn<void(uint32, DownloadsAction)> callback) {
|
||||
return [callback, moved = std::move(content)](
|
||||
not_null<PopupMenu*> menu) mutable {
|
||||
struct Row {
|
||||
not_null<Action*> action;
|
||||
uint32 id = 0;
|
||||
};
|
||||
struct State {
|
||||
std::vector<Row> rows;
|
||||
};
|
||||
const auto state = menu->lifetime().make_state<State>();
|
||||
std::move(
|
||||
moved
|
||||
) | rpl::start_with_next([=](
|
||||
const std::vector<DownloadsEntry> &entries) {
|
||||
auto found = base::flat_set<uint32>();
|
||||
for (const auto &entry : entries | ranges::views::reverse) {
|
||||
const auto id = entry.id;
|
||||
const auto path = entry.path;
|
||||
const auto i = ranges::find(state->rows, id, &Row::id);
|
||||
found.emplace(id);
|
||||
|
||||
if (i != end(state->rows)) {
|
||||
i->action->refresh(entry);
|
||||
} else {
|
||||
auto action = base::make_unique_q<Action>(
|
||||
menu,
|
||||
entry,
|
||||
[=](DownloadsAction type) { callback(id, type); });
|
||||
state->rows.push_back({
|
||||
.action = action.get(),
|
||||
.id = id,
|
||||
});
|
||||
menu->addAction(std::move(action));
|
||||
}
|
||||
}
|
||||
for (auto i = begin(state->rows); i != end(state->rows);) {
|
||||
if (!found.contains(i->id)) {
|
||||
menu->removeAction(i - begin(state->rows));
|
||||
i = state->rows.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}, menu->lifetime());
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Ui::BotWebView
|
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
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 Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
|
||||
struct DownloadsProgress {
|
||||
uint64 ready = 0;
|
||||
uint64 total : 63 = 0;
|
||||
uint64 loading : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const DownloadsProgress &a,
|
||||
const DownloadsProgress &b) = default;
|
||||
};
|
||||
|
||||
struct DownloadsEntry {
|
||||
uint32 id = 0;
|
||||
QString url;
|
||||
QString path;
|
||||
uint64 ready : 63 = 0;
|
||||
uint64 loading : 1 = 0;
|
||||
uint64 total : 63 = 0;
|
||||
uint64 failed : 1 = 0;
|
||||
};
|
||||
|
||||
enum class DownloadsAction {
|
||||
Open,
|
||||
Retry,
|
||||
Cancel,
|
||||
};
|
||||
|
||||
[[nodiscard]] auto FillAttachBotDownloadsSubmenu(
|
||||
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||
Fn<void(uint32, DownloadsAction)> callback)
|
||||
-> FnMut<void(not_null<PopupMenu*>)>;
|
||||
|
||||
} // namespace Ui::BotWebView
|
@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/chat/attach/attach_bot_webview.h"
|
||||
|
||||
#include "core/file_utilities.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/style/style_core_palette.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/separate_panel.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/debug_log.h"
|
||||
#include "base/invoke_queued.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_payments.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
@ -370,7 +373,9 @@ Panel::Panel(Args &&args)
|
||||
: _storageId(args.storageId)
|
||||
, _delegate(args.delegate)
|
||||
, _menuButtons(args.menuButtons)
|
||||
, _widget(std::make_unique<SeparatePanel>())
|
||||
, _widget(std::make_unique<SeparatePanel>(Ui::SeparatePanelArgs{
|
||||
.menuSt = &st::botWebViewMenu,
|
||||
}))
|
||||
, _fullscreen(args.fullscreen)
|
||||
, _allowClipboardRead(args.allowClipboardRead) {
|
||||
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
|
||||
@ -425,7 +430,9 @@ Panel::Panel(Args &&args)
|
||||
setTitle(std::move(args.title));
|
||||
_widget->setTitleBadge(std::move(args.titleBadge));
|
||||
|
||||
if (!showWebview(args.url, params, std::move(args.bottom))) {
|
||||
if (showWebview(args.url, params, std::move(args.bottom))) {
|
||||
setupDownloadsProgress(rpl::duplicate(args.downloadsProgress));
|
||||
} else {
|
||||
const auto available = Webview::Availability();
|
||||
if (available.error != Webview::Available::Error::None) {
|
||||
showWebviewError(tr::lng_bot_no_webview(tr::now), available);
|
||||
@ -441,6 +448,97 @@ Panel::~Panel() {
|
||||
_widget = nullptr;
|
||||
}
|
||||
|
||||
void Panel::setupDownloadsProgress(rpl::producer<DownloadsProgress> progress) {
|
||||
Expects(_menuToggle != nullptr);
|
||||
|
||||
const auto widget = Ui::CreateChild<RpWidget>(_menuToggle.data());
|
||||
widget->show();
|
||||
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_menuToggle->sizeValue() | rpl::start_with_next([=](QSize size) {
|
||||
widget->setGeometry(QRect(QPoint(), size));
|
||||
}, widget->lifetime());
|
||||
|
||||
struct State {
|
||||
State(QWidget *parent, Fn<float64()> progress)
|
||||
: animation([=](crl::time now) {
|
||||
const auto updated = animation.update(progress(), false, now);
|
||||
if (!anim::Disabled() || updated) {
|
||||
parent->update();
|
||||
}
|
||||
}) {
|
||||
}
|
||||
|
||||
RadialAnimation animation;
|
||||
Animations::Simple fade;
|
||||
bool shown = false;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<State>(widget, [=] {
|
||||
const auto total = _downloadsProgress.total;
|
||||
return total ? (_downloadsProgress.ready / float64(total)) : 0.;
|
||||
});
|
||||
std::move(
|
||||
progress
|
||||
) | rpl::start_with_next([=](DownloadsProgress progress) {
|
||||
const auto toggle = [&](bool shown) {
|
||||
if (state->shown == shown) {
|
||||
return;
|
||||
}
|
||||
state->shown = shown;
|
||||
if (shown && !state->fade.animating()) {
|
||||
return;
|
||||
}
|
||||
state->fade.start([=] {
|
||||
widget->update();
|
||||
if (!state->shown
|
||||
&& !state->fade.animating()
|
||||
&& (!_downloadsProgress.total
|
||||
|| (_downloadsProgress.ready
|
||||
== _downloadsProgress.total))) {
|
||||
state->animation.stop();
|
||||
}
|
||||
}, shown ? 0. : 2., shown ? 2. : 0., st::radialDuration * 2);
|
||||
};
|
||||
if (!state->shown && progress.loading) {
|
||||
if (!state->animation.animating()) {
|
||||
state->animation.start(0.);
|
||||
}
|
||||
toggle(true);
|
||||
} else if ((_downloadsProgress.total && !progress.total)
|
||||
|| (_downloadsProgress.ready < _downloadsProgress.total
|
||||
&& progress.ready == progress.total)) {
|
||||
state->animation.update(1., false, crl::now());
|
||||
toggle(false);
|
||||
}
|
||||
_downloadsProgress = progress;
|
||||
_downloadsUpdated.fire({});
|
||||
}, widget->lifetime());
|
||||
|
||||
widget->paintRequest() | rpl::start_with_next([=] {
|
||||
const auto opacity = std::clamp(
|
||||
state->fade.value(state->shown ? 2. : 0.) - 1.,
|
||||
0.,
|
||||
1.);
|
||||
if (!opacity) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(widget);
|
||||
p.setOpacity(opacity);
|
||||
const auto palette = _widget->titleOverridePalette();
|
||||
const auto color = palette
|
||||
? palette->boxTitleCloseFg()
|
||||
: st::paymentsLoading.color;
|
||||
const auto &st = st::separatePanelMenu;
|
||||
const auto size = st.rippleAreaSize;
|
||||
const auto rect = QRect(st.rippleAreaPosition, QSize(size, size));
|
||||
const auto stroke = st::botWebViewRadialStroke;
|
||||
const auto shift = stroke * 1.5;
|
||||
const auto inner = QRectF(rect).marginsRemoved(
|
||||
QMarginsF{ shift, shift, shift, shift });
|
||||
state->animation.draw(p, inner, stroke, color);
|
||||
}, widget->lifetime());
|
||||
}
|
||||
|
||||
void Panel::requestActivate() {
|
||||
_widget->showAndActivate();
|
||||
if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
|
||||
@ -582,7 +680,32 @@ bool Panel::showWebview(
|
||||
updateThemeParams(params);
|
||||
_webview->window.navigate(url);
|
||||
_widget->setBackAllowed(allowBack);
|
||||
_widget->setMenuAllowed([=](const Ui::Menu::MenuCallback &callback) {
|
||||
|
||||
_menuToggle = _widget->setMenuAllowed([=](
|
||||
const Ui::Menu::MenuCallback &callback) {
|
||||
auto list = _delegate->botDownloads(true);
|
||||
if (!list.empty()) {
|
||||
auto value = rpl::single(
|
||||
std::move(list)
|
||||
) | rpl::then(_downloadsUpdated.events(
|
||||
) | rpl::map([=] {
|
||||
return _delegate->botDownloads();
|
||||
}));
|
||||
const auto action = [=](uint32 id, DownloadsAction type) {
|
||||
_delegate->botDownloadsAction(id, type);
|
||||
};
|
||||
callback(Ui::Menu::MenuCallback::Args{
|
||||
.text = tr::lng_downloads_section(tr::now),
|
||||
.icon = &st::menuIconDownload,
|
||||
.fillSubmenu = FillAttachBotDownloadsSubmenu(
|
||||
std::move(value),
|
||||
action),
|
||||
});
|
||||
callback({
|
||||
.separatorSt = &st::expandedMenuSeparator,
|
||||
.isSeparator = true,
|
||||
});
|
||||
}
|
||||
if (_webview && _webview->window.widget() && _hasSettingsButton) {
|
||||
callback(tr::lng_bot_settings(tr::now), [=] {
|
||||
postEvent("settings_button_pressed");
|
||||
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/flags.h"
|
||||
#include "ui/chat/attach/attach_bot_downloads.h"
|
||||
#include "ui/rect_part.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "webview/webview_common.h"
|
||||
@ -22,6 +23,7 @@ namespace Ui {
|
||||
class BoxContent;
|
||||
class RpWidget;
|
||||
class SeparatePanel;
|
||||
class IconButton;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
} // namespace Ui
|
||||
@ -63,15 +65,6 @@ struct DownloadFileRequest {
|
||||
Fn<void(bool)> callback;
|
||||
};
|
||||
|
||||
struct DownloadsProgress {
|
||||
uint64 ready = 0;
|
||||
uint64 total = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const DownloadsProgress &a,
|
||||
const DownloadsProgress &b) = default;
|
||||
};
|
||||
|
||||
struct SendPreparedMessageRequest {
|
||||
QString id = 0;
|
||||
Fn<void(QString)> callback;
|
||||
@ -79,7 +72,12 @@ struct SendPreparedMessageRequest {
|
||||
|
||||
class Delegate {
|
||||
public:
|
||||
virtual Webview::ThemeParams botThemeParams() = 0;
|
||||
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
|
||||
[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)
|
||||
-> const std::vector<DownloadsEntry> & = 0;
|
||||
virtual void botDownloadsAction(
|
||||
uint32 id,
|
||||
Ui::BotWebView::DownloadsAction type) = 0;
|
||||
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
|
||||
virtual void botHandleInvoice(QString slug) = 0;
|
||||
virtual void botHandleMenuButton(MenuButton button) = 0;
|
||||
@ -158,6 +156,7 @@ private:
|
||||
void createWebviewBottom();
|
||||
void showWebviewProgress();
|
||||
void hideWebviewProgress();
|
||||
void setupDownloadsProgress(rpl::producer<DownloadsProgress> progress);
|
||||
void setTitle(rpl::producer<QString> title);
|
||||
void sendDataMessage(const QJsonObject &args);
|
||||
void switchInlineQueryMessage(const QJsonObject &args);
|
||||
@ -214,6 +213,7 @@ private:
|
||||
bool _hasSettingsButton = false;
|
||||
MenuButtons _menuButtons = {};
|
||||
std::unique_ptr<SeparatePanel> _widget;
|
||||
QPointer<IconButton> _menuToggle;
|
||||
std::unique_ptr<WebviewWithLifetime> _webview;
|
||||
std::unique_ptr<RpWidget> _webviewBottom;
|
||||
rpl::variable<QString> _bottomText;
|
||||
@ -229,6 +229,8 @@ private:
|
||||
rpl::lifetime _headerColorLifetime;
|
||||
rpl::lifetime _bodyColorLifetime;
|
||||
rpl::lifetime _bottomBarColorLifetime;
|
||||
DownloadsProgress _downloadsProgress;
|
||||
rpl::event_stream<> _downloadsUpdated;
|
||||
rpl::variable<bool> _fullscreen = false;
|
||||
bool _layerShown : 1 = false;
|
||||
bool _webviewProgress : 1 = false;
|
||||
|
@ -1183,3 +1183,21 @@ botEmojiStatusEmoji: FlatLabel(botEmojiStatusName) {
|
||||
}
|
||||
|
||||
botDownloadLabel: boxLabel;
|
||||
botDownloadProgress: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: ttlItemTimerFont;
|
||||
}
|
||||
}
|
||||
botDownloadCancel: IconButton {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
icon: smallCloseIcon;
|
||||
iconOver: smallCloseIconOver;
|
||||
iconPosition: point(-1px, -1px);
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 20px;
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
@ -310,6 +310,8 @@ PRIVATE
|
||||
ui/chat/attach/attach_album_preview.h
|
||||
ui/chat/attach/attach_album_thumbnail.cpp
|
||||
ui/chat/attach/attach_album_thumbnail.h
|
||||
ui/chat/attach/attach_bot_downloads.cpp
|
||||
ui/chat/attach/attach_bot_downloads.h
|
||||
ui/chat/attach/attach_bot_webview.cpp
|
||||
ui/chat/attach/attach_bot_webview.h
|
||||
ui/chat/attach/attach_controls.cpp
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 967ae393e82eb52174fd25e3a5a15b8029e21938
|
||||
Subproject commit 3b5ef7899e5edd544e37ecdf8b1e7e3ba0ca2dc0
|
Loading…
Reference in New Issue
Block a user