Implement bot downloads list UI.

This commit is contained in:
John Preston 2024-11-14 00:02:02 +04:00
parent ef521624a0
commit 338122793c
14 changed files with 665 additions and 84 deletions

View File

@ -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";

View File

@ -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)) {

View File

@ -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;

View File

@ -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
}
}
}

View File

@ -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;

View File

@ -150,3 +150,7 @@ botWebViewBottomButton: RoundButton(paymentsPanelSubmit) {
}
textTop: 11px;
}
botWebViewRadialStroke: 3px;
botWebViewMenu: PopupMenu(popupMenuWithIcons) {
maxHeight: 360px;
}

View File

@ -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));
}
}

View 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

View 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

View File

@ -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");

View File

@ -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;

View File

@ -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;
}

View File

@ -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