From 338122793ce3c4f7257a059fbd27b03f5f4d274a Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 14 Nov 2024 00:02:02 +0400 Subject: [PATCH] Implement bot downloads list UI. --- Telegram/Resources/langs/lang.strings | 3 + .../inline_bots/bot_attach_web_view.cpp | 13 +- .../inline_bots/bot_attach_web_view.h | 6 + .../inline_bots/inline_bot_downloads.cpp | 193 +++++++++---- .../inline_bots/inline_bot_downloads.h | 40 +-- .../SourceFiles/payments/ui/payments.style | 4 + .../SourceFiles/storage/file_download_web.cpp | 2 +- .../ui/chat/attach/attach_bot_downloads.cpp | 268 ++++++++++++++++++ .../ui/chat/attach/attach_bot_downloads.h | 47 +++ .../ui/chat/attach/attach_bot_webview.cpp | 129 ++++++++- .../ui/chat/attach/attach_bot_webview.h | 22 +- Telegram/SourceFiles/ui/chat/chat.style | 18 ++ Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_ui | 2 +- 14 files changed, 665 insertions(+), 84 deletions(-) create mode 100644 Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp create mode 100644 Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e925aeaa6..905db2f92 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 19d811922..90a40dbff 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -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 & { + 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)) { diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 9b3e7b500..febf74dfa 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -28,6 +28,7 @@ class DropdownMenu; namespace Ui::BotWebView { class Panel; +struct DownloadsEntry; } // namespace Ui::BotWebView namespace Main { @@ -250,6 +251,11 @@ private: -> Fn; Webview::ThemeParams botThemeParams() override; + auto botDownloads(bool forceCheck = false) + -> const std::vector & 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; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp index 40644f6e5..cbb4ede82 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp @@ -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 #include +#include "base/call_delayed.h" + namespace InlineBots { namespace { @@ -37,7 +40,10 @@ Downloads::Downloads(not_null 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( _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 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 bot) +[[nodiscard]] auto Downloads::progress(not_null bot) ->rpl::producer { read(); return _progressView[bot->id].value(); } +const std::vector &Downloads::list( + not_null 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 } } } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h index 33383cd7f..1d66e6c8d 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h @@ -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 bot, + DownloadId id, + DownloadsAction type); - [[nodiscard]] auto downloadsProgress(not_null bot) - -> rpl::producer; + [[nodiscard]] rpl::producer progress( + not_null bot); + [[nodiscard]] const std::vector &list( + not_null bot, + bool check = false); private: struct List { std::vector list; - bool checked = false; }; struct Loader { std::unique_ptr 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 _session; diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index fd9e57bab..2369181db 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -150,3 +150,7 @@ botWebViewBottomButton: RoundButton(paymentsPanelSubmit) { } textTop: 11px; } +botWebViewRadialStroke: 3px; +botWebViewMenu: PopupMenu(popupMenuWithIcons) { + maxHeight: 360px; +} diff --git a/Telegram/SourceFiles/storage/file_download_web.cpp b/Telegram/SourceFiles/storage/file_download_web.cpp index bda894d4c..1c56d8b06 100644 --- a/Telegram/SourceFiles/storage/file_download_web.cpp +++ b/Telegram/SourceFiles/storage/file_download_web.cpp @@ -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)); } } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp new file mode 100644 index 000000000..5f6fe432c --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp @@ -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 parent, + const DownloadsEntry &entry, + Fn callback); + + bool isEnabled() const override; + not_null action() const override { return _dummyAction; } + void handleKeyPress(not_null 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 _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 parent, + const DownloadsEntry &entry, + Fn 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 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)> FillAttachBotDownloadsSubmenu( + rpl::producer> content, + Fn callback) { + return [callback, moved = std::move(content)]( + not_null menu) mutable { + struct Row { + not_null action; + uint32 id = 0; + }; + struct State { + std::vector rows; + }; + const auto state = menu->lifetime().make_state(); + std::move( + moved + ) | rpl::start_with_next([=]( + const std::vector &entries) { + auto found = base::flat_set(); + 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( + 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 diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h new file mode 100644 index 000000000..4e85e5e16 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h @@ -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> content, + Fn callback) +-> FnMut)>; + +} // namespace Ui::BotWebView diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 98e965a86..670d714bc 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -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()) +, _widget(std::make_unique(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 progress) { + Expects(_menuToggle != nullptr); + + const auto widget = Ui::CreateChild(_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 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(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"); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 5b4fa16cb..7f59731b1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -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; } // namespace Ui @@ -63,15 +65,6 @@ struct DownloadFileRequest { Fn 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 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 & = 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 progress); void setTitle(rpl::producer title); void sendDataMessage(const QJsonObject &args); void switchInlineQueryMessage(const QJsonObject &args); @@ -214,6 +213,7 @@ private: bool _hasSettingsButton = false; MenuButtons _menuButtons = {}; std::unique_ptr _widget; + QPointer _menuToggle; std::unique_ptr _webview; std::unique_ptr _webviewBottom; rpl::variable _bottomText; @@ -229,6 +229,8 @@ private: rpl::lifetime _headerColorLifetime; rpl::lifetime _bodyColorLifetime; rpl::lifetime _bottomBarColorLifetime; + DownloadsProgress _downloadsProgress; + rpl::event_stream<> _downloadsUpdated; rpl::variable _fullscreen = false; bool _layerShown : 1 = false; bool _webviewProgress : 1 = false; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index b7bb520bc..b3fec713b 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -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; +} diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index c7af0a748..716d4d6b8 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -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 diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 967ae393e..3b5ef7899 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 967ae393e82eb52174fd25e3a5a15b8029e21938 +Subproject commit 3b5ef7899e5edd544e37ecdf8b1e7e3ba0ca2dc0