diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 2e8f3f3456057d..6df73b436d6b1b 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -7,34 +7,34 @@ For license and copyright information please follow this link: */ #include "ui/chat/attach/attach_bot_webview.h" +#include "base/debug_log.h" +#include "base/invoke_queued.h" +#include "base/qt_signal_producer.h" #include "core/file_utilities.h" +#include "lang/lang_keys.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_payments.h" #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" +#include "ui/integration.h" #include "ui/layers/box_content.h" +#include "ui/painter.h" #include "ui/text/text_utilities.h" -#include "ui/widgets/separate_panel.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/separate_panel.h" #include "ui/wrap/fade_wrap.h" -#include "ui/integration.h" -#include "ui/painter.h" -#include "lang/lang_keys.h" -#include "webview/webview_embed.h" #include "webview/webview_dialog.h" +#include "webview/webview_embed.h" #include "webview/webview_interface.h" -#include "base/debug_log.h" -#include "base/invoke_queued.h" -#include "base/qt_signal_producer.h" -#include "styles/style_payments.h" -#include "styles/style_layers.h" -#include "styles/style_menu_icons.h" +#include #include #include -#include -#include #include +#include #include namespace Ui::BotWebView { @@ -46,692 +46,677 @@ constexpr auto kProgressOpacity = 0.3; constexpr auto kLightnessThreshold = 128; constexpr auto kLightnessDelta = 32; -[[nodiscard]] QJsonObject ParseMethodArgs(const QString &json) { - if (json.isEmpty()) { - return {}; - } - auto error = QJsonParseError(); - const auto dictionary = QJsonDocument::fromJson(json.toUtf8(), &error); - if (error.error != QJsonParseError::NoError) { - LOG(("BotWebView Error: Could not parse \"%1\".").arg(json)); - return {}; - } - return dictionary.object(); +[[nodiscard]] QJsonObject ParseMethodArgs(const QString& json) { + if (json.isEmpty()) { + return {}; + } + auto error = QJsonParseError(); + const auto dictionary = QJsonDocument::fromJson(json.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) { + LOG(("BotWebView Error: Could not parse \"%1\".").arg(json)); + return {}; + } + return dictionary.object(); +} + +[[nodiscard]] std::optional ParseColor(const QString& text) { + if (!text.startsWith('#') || text.size() != 7) { + return {}; + } + const auto data = text.data() + 1; + const auto hex = [&](int from) -> std::optional { + const auto parse = [](QChar ch) -> std::optional { + const auto code = ch.unicode(); + return (code >= 'a' && code <= 'f') ? std::make_optional(10 + (code - 'a')) + : (code >= 'A' && code <= 'F') ? std::make_optional(10 + (code - 'A')) + : (code >= '0' && code <= '9') ? std::make_optional(code - '0') + : std::nullopt; + }; + const auto h = parse(data[from]), l = parse(data[from + 1]); + return (h && l) ? std::make_optional(*h * 16 + *l) : std::nullopt; + }; + const auto r = hex(0), g = hex(2), b = hex(4); + return (r && g && b) ? QColor(*r, *g, *b) : std::optional(); } -[[nodiscard]] std::optional ParseColor(const QString &text) { - if (!text.startsWith('#') || text.size() != 7) { - return {}; - } - const auto data = text.data() + 1; - const auto hex = [&](int from) -> std::optional { - const auto parse = [](QChar ch) -> std::optional { - const auto code = ch.unicode(); - return (code >= 'a' && code <= 'f') - ? std::make_optional(10 + (code - 'a')) - : (code >= 'A' && code <= 'F') - ? std::make_optional(10 + (code - 'A')) - : (code >= '0' && code <= '9') - ? std::make_optional(code - '0') - : std::nullopt; - }; - const auto h = parse(data[from]), l = parse(data[from + 1]); - return (h && l) ? std::make_optional(*h * 16 + *l) : std::nullopt; - }; - const auto r = hex(0), g = hex(2), b = hex(4); - return (r && g && b) ? QColor(*r, *g, *b) : std::optional(); +[[nodiscard]] QColor ResolveRipple(QColor background) { + auto hue = 0; + auto saturation = 0; + auto lightness = 0; + auto alpha = 0; + background.getHsv(&hue, &saturation, &lightness, &alpha); + return QColor::fromHsv( + hue, saturation, + lightness - (lightness > kLightnessThreshold ? kLightnessDelta : -kLightnessDelta), alpha + ); } -[[nodiscard]] QColor ResolveRipple(QColor background) { - auto hue = 0; - auto saturation = 0; - auto lightness = 0; - auto alpha = 0; - background.getHsv(&hue, &saturation, &lightness, &alpha); - return QColor::fromHsv( - hue, - saturation, - lightness - (lightness > kLightnessThreshold - ? kLightnessDelta - : -kLightnessDelta), - alpha); -} - -} // namespace +} // namespace class Panel::Button final : public RippleButton { public: - Button(QWidget *parent, const style::RoundButton &st); - ~Button(); + Button(QWidget* parent, const style::RoundButton& st); + ~Button(); - void updateBg(QColor bg); - void updateFg(QColor fg); - void updateArgs(MainButtonArgs &&args); + void updateBg(QColor bg); + void updateFg(QColor fg); + void updateArgs(MainButtonArgs&& args); private: - void paintEvent(QPaintEvent *e) override; + void paintEvent(QPaintEvent* e) override; - QImage prepareRippleMask() const override; - QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; - void toggleProgress(bool shown); - void setupProgressGeometry(); + void toggleProgress(bool shown); + void setupProgressGeometry(); - std::unique_ptr _progress; - rpl::variable _textFull; - Ui::Text::String _text; - - const style::RoundButton &_st; - QColor _fg; - style::owned_color _bg; - RoundRect _roundRect; + std::unique_ptr _progress; + rpl::variable _textFull; + Ui::Text::String _text; + const style::RoundButton& _st; + QColor _fg; + style::owned_color _bg; + RoundRect _roundRect; }; struct Panel::Progress { - Progress(QWidget *parent, Fn rect); + Progress(QWidget* parent, Fn rect); - RpWidget widget; - InfiniteRadialAnimation animation; - Animations::Simple shownAnimation; - bool shown = true; - rpl::lifetime geometryLifetime; + RpWidget widget; + InfiniteRadialAnimation animation; + Animations::Simple shownAnimation; + bool shown = true; + rpl::lifetime geometryLifetime; }; struct Panel::WebviewWithLifetime { - WebviewWithLifetime( - QWidget *parent = nullptr, - Webview::WindowConfig config = Webview::WindowConfig()); - - Webview::Window window; - QPointer lastHidingBox; - rpl::lifetime lifetime; + WebviewWithLifetime( + QWidget* parent = nullptr, + Webview::WindowConfig config = Webview::WindowConfig() + ); + + Webview::Window window; + QPointer lastHidingBox; + rpl::lifetime lifetime; }; -Panel::Button::Button(QWidget *parent, const style::RoundButton &st) -: RippleButton(parent, st.ripple) -, _st(st) -, _bg(st::windowBgActive->c) -, _roundRect(st::callRadius, st::windowBgActive) { - _textFull.value( - ) | rpl::start_with_next([=](const QString &text) { - _text.setText(st::semiboldTextStyle, text); - update(); - }, lifetime()); - - resize( - _st.padding.left() + _text.maxWidth() + _st.padding.right(), - _st.padding.top() + _st.height + _st.padding.bottom()); +Panel::Button::Button(QWidget* parent, const style::RoundButton& st) + : RippleButton(parent, st.ripple), + _st(st), + _bg(st::windowBgActive->c), + _roundRect(st::callRadius, st::windowBgActive) { + _textFull.value() | rpl::start_with_next( + [=](const QString& text) { + _text.setText(st::semiboldTextStyle, text); + update(); + }, + lifetime() + ); + + resize( + _st.padding.left() + _text.maxWidth() + _st.padding.right(), + _st.padding.top() + _st.height + _st.padding.bottom() + ); } Panel::Button::~Button() = default; void Panel::Button::updateBg(QColor bg) { - _bg.update(bg); - _roundRect.setColor(_bg.color()); - update(); + _bg.update(bg); + _roundRect.setColor(_bg.color()); + update(); } void Panel::Button::updateFg(QColor fg) { - _fg = fg; - update(); + _fg = fg; + update(); } -void Panel::Button::updateArgs(MainButtonArgs &&args) { - _textFull = std::move(args.text); - setDisabled(!args.isActive); - setPointerCursor(false); - setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor); - setVisible(args.isVisible); - toggleProgress(args.isProgressVisible); - update(); +void Panel::Button::updateArgs(MainButtonArgs&& args) { + _textFull = std::move(args.text); + setDisabled(!args.isActive); + setPointerCursor(false); + setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor); + setVisible(args.isVisible); + toggleProgress(args.isProgressVisible); + update(); } void Panel::Button::toggleProgress(bool shown) { - if (!_progress) { - if (!shown) { - return; - } - _progress = std::make_unique( - this, - [=] { return _progress->widget.rect(); }); - _progress->widget.paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(&_progress->widget); - p.setOpacity( - _progress->shownAnimation.value(_progress->shown ? 1. : 0.)); - auto thickness = st::paymentsLoading.thickness; - const auto rect = _progress->widget.rect().marginsRemoved( - { thickness, thickness, thickness, thickness }); - InfiniteRadialAnimation::Draw( - p, - _progress->animation.computeState(), - rect.topLeft(), - rect.size() - QSize(), - _progress->widget.width(), - _fg, - thickness); - }, _progress->widget.lifetime()); - _progress->widget.show(); - _progress->animation.start(); - } else if (_progress->shown == shown) { - return; - } - const auto callback = [=] { - if (!_progress->shownAnimation.animating() && !_progress->shown) { - _progress = nullptr; - } else { - _progress->widget.update(); - } - }; - _progress->shown = shown; - _progress->shownAnimation.start( - callback, - shown ? 0. : 1., - shown ? 1. : 0., - kProgressDuration); - if (shown) { - setupProgressGeometry(); - } + if (!_progress) { + if (!shown) { + return; + } + _progress = std::make_unique(this, [=] { return _progress->widget.rect(); }); + _progress->widget.paintRequest() | + rpl::start_with_next( + [=](QRect clip) { + auto p = QPainter(&_progress->widget); + p.setOpacity(_progress->shownAnimation.value(_progress->shown ? 1. : 0.)); + auto thickness = st::paymentsLoading.thickness; + const auto rect = _progress->widget.rect().marginsRemoved( + {thickness, thickness, thickness, thickness} + ); + InfiniteRadialAnimation::Draw( + p, _progress->animation.computeState(), rect.topLeft(), + rect.size() - QSize(), _progress->widget.width(), _fg, thickness + ); + }, + _progress->widget.lifetime() + ); + _progress->widget.show(); + _progress->animation.start(); + } else if (_progress->shown == shown) { + return; + } + const auto callback = [=] { + if (!_progress->shownAnimation.animating() && !_progress->shown) { + _progress = nullptr; + } else { + _progress->widget.update(); + } + }; + _progress->shown = shown; + _progress->shownAnimation.start(callback, shown ? 0. : 1., shown ? 1. : 0., kProgressDuration); + if (shown) { + setupProgressGeometry(); + } } void Panel::Button::setupProgressGeometry() { - if (!_progress || !_progress->shown) { - return; - } - _progress->geometryLifetime.destroy(); - sizeValue( - ) | rpl::start_with_next([=](QSize outer) { - const auto height = outer.height(); - const auto size = st::paymentsLoading.size; - const auto skip = (height - size.height()) / 2; - const auto right = outer.width(); - const auto top = outer.height() - height; - _progress->widget.setGeometry(QRect{ - QPoint(right - skip - size.width(), top + skip), - size }); - }, _progress->geometryLifetime); - - _progress->widget.show(); - _progress->widget.raise(); - if (_progress->shown - && Ui::AppInFocus() - && Ui::InFocusChain(_progress->widget.window())) { - _progress->widget.setFocus(); - } -} - -void Panel::Button::paintEvent(QPaintEvent *e) { - Painter p(this); - - _roundRect.paintSomeRounded( - p, - rect().marginsAdded({ 0, st::callRadius * 2, 0, 0 }), - RectPart::BottomLeft | RectPart::BottomRight); - - if (!isDisabled()) { - const auto ripple = ResolveRipple(_bg.color()->c); - paintRipple(p, rect().topLeft(), &ripple); - } - - p.setFont(_st.font); - - const auto height = rect().height(); - const auto progress = st::paymentsLoading.size; - const auto skip = (height - progress.height()) / 2; - const auto padding = skip + progress.width() + skip; - - const auto space = width() - padding * 2; - const auto textWidth = std::min(space, _text.maxWidth()); - const auto textTop = _st.padding.top() + _st.textTop; - const auto textLeft = padding + (space - textWidth) / 2; - p.setPen(_fg); - _text.drawLeftElided(p, textLeft, textTop, textWidth, width()); + if (!_progress || !_progress->shown) { + return; + } + _progress->geometryLifetime.destroy(); + sizeValue() | rpl::start_with_next( + [=](QSize outer) { + const auto height = outer.height(); + const auto size = st::paymentsLoading.size; + const auto skip = (height - size.height()) / 2; + const auto right = outer.width(); + const auto top = outer.height() - height; + _progress->widget.setGeometry( + QRect{QPoint(right - skip - size.width(), top + skip), size} + ); + }, + _progress->geometryLifetime + ); + + _progress->widget.show(); + _progress->widget.raise(); + if (_progress->shown && Ui::AppInFocus() && Ui::InFocusChain(_progress->widget.window())) { + _progress->widget.setFocus(); + } +} + +void Panel::Button::paintEvent(QPaintEvent* e) { + Painter p(this); + + _roundRect.paintSomeRounded( + p, rect().marginsAdded({0, st::callRadius * 2, 0, 0}), + RectPart::BottomLeft | RectPart::BottomRight + ); + + if (!isDisabled()) { + const auto ripple = ResolveRipple(_bg.color()->c); + paintRipple(p, rect().topLeft(), &ripple); + } + + p.setFont(_st.font); + + const auto height = rect().height(); + const auto progress = st::paymentsLoading.size; + const auto skip = (height - progress.height()) / 2; + const auto padding = skip + progress.width() + skip; + + const auto space = width() - padding * 2; + const auto textWidth = std::min(space, _text.maxWidth()); + const auto textTop = _st.padding.top() + _st.textTop; + const auto textLeft = padding + (space - textWidth) / 2; + p.setPen(_fg); + _text.drawLeftElided(p, textLeft, textTop, textWidth, width()); } QImage Panel::Button::prepareRippleMask() const { - return RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) { - p.drawRoundedRect( - rect().marginsAdded({ 0, st::callRadius * 2, 0, 0 }), - st::callRadius, - st::callRadius); - }); + return RippleAnimation::MaskByDrawer(size(), false, [&](QPainter& p) { + p.drawRoundedRect( + rect().marginsAdded({0, st::callRadius * 2, 0, 0}), st::callRadius, st::callRadius + ); + }); } QPoint Panel::Button::prepareRippleStartPosition() const { - return mapFromGlobal(QCursor::pos()) - - QPoint(_st.padding.left(), _st.padding.top()); + return mapFromGlobal(QCursor::pos()) - QPoint(_st.padding.left(), _st.padding.top()); } -Panel::WebviewWithLifetime::WebviewWithLifetime( - QWidget *parent, - Webview::WindowConfig config) -: window(parent, std::move(config)) { -} +Panel::WebviewWithLifetime::WebviewWithLifetime(QWidget* parent, Webview::WindowConfig config) + : window(parent, std::move(config)) {} -Panel::Progress::Progress(QWidget *parent, Fn rect) -: widget(parent) -, animation( - [=] { if (!anim::Disabled()) widget.update(rect()); }, - st::paymentsLoading) { -} +Panel::Progress::Progress(QWidget* parent, Fn rect) + : widget(parent), + animation( + [=] { + if (!anim::Disabled()) + widget.update(rect()); + }, + st::paymentsLoading + ) {} Panel::Panel( - const Webview::StorageId &storageId, - rpl::producer title, - not_null delegate, - MenuButtons menuButtons, - bool allowClipboardRead) -: _storageId(storageId) -, _delegate(delegate) -, _menuButtons(menuButtons) -, _widget(std::make_unique()) -, _allowClipboardRead(allowClipboardRead) { - _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); - _widget->setInnerSize(st::botWebViewPanelSize); - - _widget->closeRequests( - ) | rpl::start_with_next([=] { - if (_closeNeedConfirmation) { - scheduleCloseWithConfirmation(); - } else { - _delegate->botClose(); - } - }, _widget->lifetime()); - - _widget->closeEvents( - ) | rpl::filter([=] { - return !_hiddenForPayment; - }) | rpl::start_with_next([=] { - _delegate->botClose(); - }, _widget->lifetime()); - - _widget->backRequests( - ) | rpl::start_with_next([=] { - postEvent("back_button_pressed"); - }, _widget->lifetime()); - - rpl::merge( - style::PaletteChanged(), - _themeUpdateForced.events() - ) | rpl::filter([=] { - return !_themeUpdateScheduled; - }) | rpl::start_with_next([=] { - _themeUpdateScheduled = true; - crl::on_main(_widget.get(), [=] { - _themeUpdateScheduled = false; - updateThemeParams(_delegate->botThemeParams()); - }); - }, _widget->lifetime()); - - setTitle(std::move(title)); + const Webview::StorageId& storageId, + rpl::producer title, + not_null delegate, + MenuButtons menuButtons, + bool allowClipboardRead +) + : _storageId(storageId), + _delegate(delegate), + _menuButtons(menuButtons), + _widget(std::make_unique()), + _allowClipboardRead(allowClipboardRead) { + _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); + _widget->setInnerSize(st::botWebViewPanelSize, false); + + _widget->closeRequests() | rpl::start_with_next( + [=] { + if (_closeNeedConfirmation) { + scheduleCloseWithConfirmation(); + } else { + _delegate->botClose(); + } + }, + _widget->lifetime() + ); + + _widget->closeEvents() | rpl::filter([=] { return !_hiddenForPayment; }) | + rpl::start_with_next([=] { _delegate->botClose(); }, _widget->lifetime()); + + _widget->backRequests() | + rpl::start_with_next([=] { postEvent("back_button_pressed"); }, _widget->lifetime()); + + rpl::merge(style::PaletteChanged(), _themeUpdateForced.events()) | + rpl::filter([=] { return !_themeUpdateScheduled; }) | + rpl::start_with_next( + [=] { + _themeUpdateScheduled = true; + crl::on_main(_widget.get(), [=] { + _themeUpdateScheduled = false; + updateThemeParams(_delegate->botThemeParams()); + }); + }, + _widget->lifetime() + ); + + setTitle(std::move(title)); } Panel::~Panel() { - base::take(_webview); - _progress = nullptr; - _widget = nullptr; + base::take(_webview); + _progress = nullptr; + _widget = nullptr; } void Panel::requestActivate() { - _widget->showAndActivate(); - if (const auto widget = _webview ? _webview->window.widget() : nullptr) { - InvokeQueued(widget, [=] { - if (widget->isVisible()) { - _webview->window.focus(); - } - }); - } + _widget->showAndActivate(); + if (const auto widget = _webview ? _webview->window.widget() : nullptr) { + InvokeQueued(widget, [=] { + if (widget->isVisible()) { + _webview->window.focus(); + } + }); + } } void Panel::toggleProgress(bool shown) { - if (!_progress) { - if (!shown) { - return; - } - _progress = std::make_unique( - _widget.get(), - [=] { return progressRect(); }); - _progress->widget.paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(&_progress->widget); - p.setOpacity( - _progress->shownAnimation.value(_progress->shown ? 1. : 0.)); - auto thickness = st::paymentsLoading.thickness; - if (progressWithBackground()) { - auto color = st::windowBg->c; - color.setAlphaF(kProgressOpacity); - p.fillRect(clip, color); - } - const auto rect = progressRect().marginsRemoved( - { thickness, thickness, thickness, thickness }); - InfiniteRadialAnimation::Draw( - p, - _progress->animation.computeState(), - rect.topLeft(), - rect.size() - QSize(), - _progress->widget.width(), - st::paymentsLoading.color, - thickness); - }, _progress->widget.lifetime()); - _progress->widget.show(); - _progress->animation.start(); - } else if (_progress->shown == shown) { - return; - } - const auto callback = [=] { - if (!_progress->shownAnimation.animating() && !_progress->shown) { - _progress = nullptr; - } else { - _progress->widget.update(); - } - }; - _progress->shown = shown; - _progress->shownAnimation.start( - callback, - shown ? 0. : 1., - shown ? 1. : 0., - kProgressDuration); - if (shown) { - setupProgressGeometry(); - } + if (!_progress) { + if (!shown) { + return; + } + _progress = std::make_unique(_widget.get(), [=] { return progressRect(); }); + _progress->widget.paintRequest() | + rpl::start_with_next( + [=](QRect clip) { + auto p = QPainter(&_progress->widget); + p.setOpacity(_progress->shownAnimation.value(_progress->shown ? 1. : 0.)); + auto thickness = st::paymentsLoading.thickness; + if (progressWithBackground()) { + auto color = st::windowBg->c; + color.setAlphaF(kProgressOpacity); + p.fillRect(clip, color); + } + const auto rect = + progressRect().marginsRemoved({thickness, thickness, thickness, thickness}); + InfiniteRadialAnimation::Draw( + p, _progress->animation.computeState(), rect.topLeft(), + rect.size() - QSize(), _progress->widget.width(), st::paymentsLoading.color, + thickness + ); + }, + _progress->widget.lifetime() + ); + _progress->widget.show(); + _progress->animation.start(); + } else if (_progress->shown == shown) { + return; + } + const auto callback = [=] { + if (!_progress->shownAnimation.animating() && !_progress->shown) { + _progress = nullptr; + } else { + _progress->widget.update(); + } + }; + _progress->shown = shown; + _progress->shownAnimation.start(callback, shown ? 0. : 1., shown ? 1. : 0., kProgressDuration); + if (shown) { + setupProgressGeometry(); + } } bool Panel::progressWithBackground() const { - return (_progress->widget.width() == _widget->innerGeometry().width()); + return (_progress->widget.width() == _widget->innerGeometry().width()); } QRect Panel::progressRect() const { - const auto rect = _progress->widget.rect(); - if (!progressWithBackground()) { - return rect; - } - const auto size = st::defaultBoxButton.height; - return QRect( - rect.x() + (rect.width() - size) / 2, - rect.y() + (rect.height() - size) / 2, - size, - size); + const auto rect = _progress->widget.rect(); + if (!progressWithBackground()) { + return rect; + } + const auto size = st::defaultBoxButton.height; + return QRect( + rect.x() + (rect.width() - size) / 2, rect.y() + (rect.height() - size) / 2, size, size + ); } void Panel::setupProgressGeometry() { - if (!_progress || !_progress->shown) { - return; - } - _progress->geometryLifetime.destroy(); - if (_webviewBottom) { - _webviewBottom->geometryValue( - ) | rpl::start_with_next([=](QRect bottom) { - const auto height = bottom.height(); - const auto size = st::paymentsLoading.size; - const auto skip = (height - size.height()) / 2; - const auto inner = _widget->innerGeometry(); - const auto right = inner.x() + inner.width(); - const auto top = inner.y() + inner.height() - height; - // This doesn't work, because first we get the correct bottom - // geometry and after that we get the previous event (which - // triggered the 'fire' of correct geometry before getting here). - //const auto right = bottom.x() + bottom.width(); - //const auto top = bottom.y(); - _progress->widget.setGeometry(QRect{ - QPoint(right - skip - size.width(), top + skip), - size }); - }, _progress->geometryLifetime); - } - _progress->widget.show(); - _progress->widget.raise(); - if (_progress->shown) { - _progress->widget.setFocus(); - } + if (!_progress || !_progress->shown) { + return; + } + _progress->geometryLifetime.destroy(); + if (_webviewBottom) { + _webviewBottom->geometryValue() | + rpl::start_with_next( + [=](QRect bottom) { + const auto height = bottom.height(); + const auto size = st::paymentsLoading.size; + const auto skip = (height - size.height()) / 2; + const auto inner = _widget->innerGeometry(); + const auto right = inner.x() + inner.width(); + const auto top = inner.y() + inner.height() - height; + // This doesn't work, because first we get the correct bottom + // geometry and after that we get the previous event (which + // triggered the 'fire' of correct geometry before getting here). + // const auto right = bottom.x() + bottom.width(); + // const auto top = bottom.y(); + _progress->widget.setGeometry( + QRect{QPoint(right - skip - size.width(), top + skip), size} + ); + }, + _progress->geometryLifetime + ); + } + _progress->widget.show(); + _progress->widget.raise(); + if (_progress->shown) { + _progress->widget.setFocus(); + } } void Panel::showWebviewProgress() { - if (_webviewProgress && _progress && _progress->shown) { - return; - } - _webviewProgress = true; - toggleProgress(true); + if (_webviewProgress && _progress && _progress->shown) { + return; + } + _webviewProgress = true; + toggleProgress(true); } void Panel::hideWebviewProgress() { - if (!_webviewProgress) { - return; - } - _webviewProgress = false; - toggleProgress(false); + if (!_webviewProgress) { + return; + } + _webviewProgress = false; + toggleProgress(false); } bool Panel::showWebview( - const QString &url, - const Webview::ThemeParams ¶ms, - rpl::producer bottomText) { - _bottomText = std::move(bottomText); - if (!_webview && !createWebview(params)) { - return false; - } - const auto allowBack = false; - showWebviewProgress(); - _widget->hideLayer(anim::type::instant); - updateThemeParams(params); - _webview->window.navigate(url); - _widget->setBackAllowed(allowBack); - _widget->setMenuAllowed([=](const Ui::Menu::MenuCallback &callback) { - if (_webview && _webview->window.widget() && _hasSettingsButton) { - callback(tr::lng_bot_settings(tr::now), [=] { - postEvent("settings_button_pressed"); - }, &st::menuIconSettings); - } - if (_menuButtons & MenuButton::OpenBot) { - callback(tr::lng_bot_open(tr::now), [=] { - _delegate->botHandleMenuButton(MenuButton::OpenBot); - }, &st::menuIconLeave); - } - callback(tr::lng_bot_reload_page(tr::now), [=] { - if (_webview && _webview->window.widget()) { - _webview->window.reload(); - } else if (const auto params = _delegate->botThemeParams() - ; createWebview(params)) { - showWebviewProgress(); - updateThemeParams(params); - _webview->window.navigate(url); - } - }, &st::menuIconRestore); - if (_menuButtons & MenuButton::ShareGame) { - callback(tr::lng_iv_share(tr::now), [=] { - _delegate->botHandleMenuButton(MenuButton::ShareGame); - }, &st::menuIconShare); - } else { - callback(tr::lng_bot_terms(tr::now), [=] { - File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now)); - }, &st::menuIconGroupLog); - callback(tr::lng_profile_bot_privacy(tr::now), [=] { - _delegate->botOpenPrivacyPolicy(); - }, &st::menuIconAntispam); - } - const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu); - if (main || (_menuButtons & MenuButton::RemoveFromMenu)) { - const auto handler = [=] { - _delegate->botHandleMenuButton(main - ? MenuButton::RemoveFromMainMenu - : MenuButton::RemoveFromMenu); - }; - callback({ - .text = (main - ? tr::lng_bot_remove_from_side_menu - : tr::lng_bot_remove_from_menu)(tr::now), - .handler = handler, - .icon = &st::menuIconDeleteAttention, - .isAttention = true, - }); - } - }); - return true; + const QString& url, + const Webview::ThemeParams& params, + rpl::producer bottomText +) { + _bottomText = std::move(bottomText); + if (!_webview && !createWebview(params)) { + return false; + } + const auto allowBack = false; + showWebviewProgress(); + _widget->hideLayer(anim::type::instant); + updateThemeParams(params); + _webview->window.navigate(url); + _widget->setBackAllowed(allowBack); + _widget->setMenuAllowed([=](const Ui::Menu::MenuCallback& callback) { + if (_webview && _webview->window.widget() && _hasSettingsButton) { + callback( + tr::lng_bot_settings(tr::now), [=] { postEvent("settings_button_pressed"); }, + &st::menuIconSettings + ); + } + if (_menuButtons & MenuButton::OpenBot) { + callback( + tr::lng_bot_open(tr::now), + [=] { _delegate->botHandleMenuButton(MenuButton::OpenBot); }, &st::menuIconLeave + ); + } + callback( + tr::lng_bot_reload_page(tr::now), + [=] { + if (_webview && _webview->window.widget()) { + _webview->window.reload(); + } else if (const auto params = _delegate->botThemeParams(); createWebview(params)) { + showWebviewProgress(); + updateThemeParams(params); + _webview->window.navigate(url); + } + }, + &st::menuIconRestore + ); + if (_menuButtons & MenuButton::ShareGame) { + callback( + tr::lng_iv_share(tr::now), + [=] { _delegate->botHandleMenuButton(MenuButton::ShareGame); }, &st::menuIconShare + ); + } else { + callback( + tr::lng_bot_terms(tr::now), + [=] { File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now)); }, &st::menuIconGroupLog + ); + callback( + tr::lng_profile_bot_privacy(tr::now), [=] { _delegate->botOpenPrivacyPolicy(); }, + &st::menuIconAntispam + ); + } + const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu); + if (main || (_menuButtons & MenuButton::RemoveFromMenu)) { + const auto handler = [=] { + _delegate->botHandleMenuButton( + main ? MenuButton::RemoveFromMainMenu : MenuButton::RemoveFromMenu + ); + }; + callback({ + .text = + (main ? tr::lng_bot_remove_from_side_menu + : tr::lng_bot_remove_from_menu)(tr::now), + .handler = handler, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + } + }); + return true; } void Panel::createWebviewBottom() { - _webviewBottom = std::make_unique(_widget.get()); - const auto bottom = _webviewBottom.get(); - bottom->show(); - - const auto &padding = st::paymentsPanelPadding; - const auto label = CreateChild( - _webviewBottom.get(), - _bottomText.value(), - st::paymentsWebviewBottom); - const auto height = padding.top() - + label->heightNoMargins() - + padding.bottom(); - rpl::combine( - _webviewBottom->widthValue(), - label->widthValue() - ) | rpl::start_with_next([=](int outerWidth, int width) { - label->move((outerWidth - width) / 2, padding.top()); - }, label->lifetime()); - label->show(); - _webviewBottom->resize(_webviewBottom->width(), height); - - rpl::combine( - _webviewParent->geometryValue() | rpl::map([=] { - return _widget->innerGeometry(); - }), - bottom->heightValue() - ) | rpl::start_with_next([=](QRect inner, int height) { - bottom->move(inner.x(), inner.y() + inner.height() - height); - bottom->resizeToWidth(inner.width()); - updateFooterHeight(); - }, bottom->lifetime()); -} - -bool Panel::createWebview(const Webview::ThemeParams ¶ms) { - auto outer = base::make_unique_q(_widget.get()); - const auto container = outer.get(); - _widget->showInner(std::move(outer)); - _webviewParent = container; - - createWebviewBottom(); - - container->show(); - _webview = std::make_unique( - container, - Webview::WindowConfig{ - .opaqueBg = params.opaqueBg, - .storageId = _storageId, - }); - const auto raw = &_webview->window; - - const auto bottom = _webviewBottom.get(); - QObject::connect(container, &QObject::destroyed, [=] { - if (_webview && &_webview->window == raw) { - base::take(_webview); - if (_webviewProgress) { - hideWebviewProgress(); - if (_progress && !_progress->shown) { - _progress = nullptr; - } - } - } - if (_webviewBottom.get() == bottom) { - _webviewBottom = nullptr; - _mainButton = nullptr; - } - }); - if (!raw->widget()) { - return false; - } - QObject::connect(raw->widget(), &QObject::destroyed, [=] { - const auto parent = _webviewParent.data(); - if (!_webview - || &_webview->window != raw - || !parent - || _widget->inner() != parent) { - // If we destroyed _webview ourselves, - // or if we changed _widget->inner ourselves, - // we don't show any message, nothing crashed. - return; - } - crl::on_main(this, [=] { - showCriticalError({ "Error: WebView has crashed." }); - }); - }); - - updateFooterHeight(); - rpl::combine( - container->geometryValue(), - _footerHeight.value() - ) | rpl::start_with_next([=](QRect geometry, int footer) { - if (const auto view = raw->widget()) { - view->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer })); - } - }, _webview->lifetime); - - raw->setMessageHandler([=](const QJsonDocument &message) { - if (!message.isArray()) { - LOG(("BotWebView Error: " - "Not an array received in buy_callback arguments.")); - return; - } - const auto list = message.array(); - const auto command = list.at(0).toString(); - const auto arguments = ParseMethodArgs(list.at(1).toString()); - if (command == "web_app_close") { - _delegate->botClose(); - } else if (command == "web_app_data_send") { - sendDataMessage(arguments); - } else if (command == "web_app_switch_inline_query") { - switchInlineQueryMessage(arguments); - } else if (command == "web_app_setup_main_button") { - processMainButtonMessage(arguments); - } else if (command == "web_app_setup_back_button") { - processBackButtonMessage(arguments); - } else if (command == "web_app_setup_settings_button") { - processSettingsButtonMessage(arguments); - } else if (command == "web_app_request_theme") { - _themeUpdateForced.fire({}); - } else if (command == "web_app_request_viewport") { - sendViewport(); - } else if (command == "web_app_open_tg_link") { - openTgLink(arguments); - } else if (command == "web_app_open_link") { - openExternalLink(arguments); - } else if (command == "web_app_open_invoice") { - openInvoice(arguments); - } else if (command == "web_app_open_popup") { - openPopup(arguments); - } else if (command == "web_app_open_scan_qr_popup") { - openScanQrPopup(arguments); - } else if (command == "web_app_share_to_story") { - openShareStory(arguments); - } else if (command == "web_app_request_write_access") { - requestWriteAccess(); - } else if (command == "web_app_request_phone") { - requestPhone(); - } else if (command == "web_app_invoke_custom_method") { - invokeCustomMethod(arguments); - } else if (command == "web_app_setup_closing_behavior") { - setupClosingBehaviour(arguments); - } else if (command == "web_app_read_text_from_clipboard") { - requestClipboardText(arguments); - } else if (command == "web_app_set_header_color") { - processHeaderColor(arguments); - } else if (command == "share_score") { - _delegate->botHandleMenuButton(MenuButton::ShareGame); - } - }); - - raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { - if (_delegate->botHandleLocalUri(uri, false)) { - return false; - } else if (newWindow) { - return true; - } - showWebviewProgress(); - return true; - }); - raw->setNavigationDoneHandler([=](bool success) { - hideWebviewProgress(); - }); - - raw->init(R"( + _webviewBottom = std::make_unique(_widget.get()); + const auto bottom = _webviewBottom.get(); + bottom->show(); + + const auto& padding = st::paymentsPanelPadding; + const auto label = CreateChild( + _webviewBottom.get(), _bottomText.value(), st::paymentsWebviewBottom + ); + const auto height = padding.top() + label->heightNoMargins() + padding.bottom(); + rpl::combine(_webviewBottom->widthValue(), label->widthValue()) | + rpl::start_with_next( + [=](int outerWidth, int width) { + label->move((outerWidth - width) / 2, padding.top()); + }, + label->lifetime() + ); + label->show(); + _webviewBottom->resize(_webviewBottom->width(), height); + + rpl::combine( + _webviewParent->geometryValue() | rpl::map([=] { return _widget->innerGeometry(); }), + bottom->heightValue() + ) | + rpl::start_with_next( + [=](QRect inner, int height) { + bottom->move(inner.x(), inner.y() + inner.height() - height); + bottom->resizeToWidth(inner.width()); + updateFooterHeight(); + }, + bottom->lifetime() + ); +} + +bool Panel::createWebview(const Webview::ThemeParams& params) { + auto outer = base::make_unique_q(_widget.get()); + const auto container = outer.get(); + _widget->showInner(std::move(outer)); + _webviewParent = container; + + createWebviewBottom(); + + container->show(); + _webview = std::make_unique( + container, + Webview::WindowConfig{ + .opaqueBg = params.opaqueBg, + .storageId = _storageId, + } + ); + const auto raw = &_webview->window; + + const auto bottom = _webviewBottom.get(); + QObject::connect(container, &QObject::destroyed, [=] { + if (_webview && &_webview->window == raw) { + base::take(_webview); + if (_webviewProgress) { + hideWebviewProgress(); + if (_progress && !_progress->shown) { + _progress = nullptr; + } + } + } + if (_webviewBottom.get() == bottom) { + _webviewBottom = nullptr; + _mainButton = nullptr; + } + }); + if (!raw->widget()) { + return false; + } + QObject::connect(raw->widget(), &QObject::destroyed, [=] { + const auto parent = _webviewParent.data(); + if (!_webview || &_webview->window != raw || !parent || _widget->inner() != parent) { + // If we destroyed _webview ourselves, + // or if we changed _widget->inner ourselves, + // we don't show any message, nothing crashed. + return; + } + crl::on_main(this, [=] { showCriticalError({"Error: WebView has crashed."}); }); + }); + + updateFooterHeight(); + rpl::combine(container->geometryValue(), _footerHeight.value()) | + rpl::start_with_next( + [=](QRect geometry, int footer) { + if (const auto view = raw->widget()) { + view->setGeometry(geometry.marginsRemoved({0, 0, 0, footer})); + } + }, + _webview->lifetime + ); + + raw->setMessageHandler([=](const QJsonDocument& message) { + if (!message.isArray()) { + LOG( + ("BotWebView Error: " + "Not an array received in buy_callback arguments.") + ); + return; + } + const auto list = message.array(); + const auto command = list.at(0).toString(); + const auto arguments = ParseMethodArgs(list.at(1).toString()); + if (command == "web_app_close") { + _delegate->botClose(); + } else if (command == "web_app_data_send") { + sendDataMessage(arguments); + } else if (command == "web_app_switch_inline_query") { + switchInlineQueryMessage(arguments); + } else if (command == "web_app_setup_main_button") { + processMainButtonMessage(arguments); + } else if (command == "web_app_setup_back_button") { + processBackButtonMessage(arguments); + } else if (command == "web_app_setup_settings_button") { + processSettingsButtonMessage(arguments); + } else if (command == "web_app_request_theme") { + _themeUpdateForced.fire({}); + } else if (command == "web_app_request_viewport") { + sendViewport(); + } else if (command == "web_app_open_tg_link") { + openTgLink(arguments); + } else if (command == "web_app_open_link") { + openExternalLink(arguments); + } else if (command == "web_app_open_invoice") { + openInvoice(arguments); + } else if (command == "web_app_open_popup") { + openPopup(arguments); + } else if (command == "web_app_open_scan_qr_popup") { + openScanQrPopup(arguments); + } else if (command == "web_app_share_to_story") { + openShareStory(arguments); + } else if (command == "web_app_request_write_access") { + requestWriteAccess(); + } else if (command == "web_app_request_phone") { + requestPhone(); + } else if (command == "web_app_invoke_custom_method") { + invokeCustomMethod(arguments); + } else if (command == "web_app_setup_closing_behavior") { + setupClosingBehaviour(arguments); + } else if (command == "web_app_read_text_from_clipboard") { + requestClipboardText(arguments); + } else if (command == "web_app_set_header_color") { + processHeaderColor(arguments); + } else if (command == "share_score") { + _delegate->botHandleMenuButton(MenuButton::ShareGame); + } + }); + + raw->setNavigationStartHandler([=](const QString& uri, bool newWindow) { + if (_delegate->botHandleLocalUri(uri, false)) { + return false; + } else if (newWindow) { + return true; + } + showWebviewProgress(); + return true; + }); + raw->setNavigationDoneHandler([=](bool success) { hideWebviewProgress(); }); + + raw->init(R"( window.TelegramWebviewProxy = { postEvent: function(eventType, eventData) { if (window.external && window.external.invoke) { @@ -740,739 +725,701 @@ postEvent: function(eventType, eventData) { } };)"); - if (!_webview) { - return false; - } + if (!_webview) { + return false; + } - setupProgressGeometry(); + setupProgressGeometry(); - base::qt_signal_producer( - qApp, - &QGuiApplication::focusWindowChanged - ) | rpl::filter([=](QWindow *focused) { - const auto handle = _widget->window()->windowHandle(); - const auto widget = _webview ? _webview->window.widget() : nullptr; - return widget - && !widget->isHidden() - && handle - && (focused == handle); - }) | rpl::start_with_next([=] { - _webview->window.focus(); - }, _webview->lifetime); + base::qt_signal_producer(qApp, &QGuiApplication::focusWindowChanged) | + rpl::filter([=](QWindow* focused) { + const auto handle = _widget->window()->windowHandle(); + const auto widget = _webview ? _webview->window.widget() : nullptr; + return widget && !widget->isHidden() && handle && (focused == handle); + }) | + rpl::start_with_next([=] { _webview->window.focus(); }, _webview->lifetime); - return true; + return true; } void Panel::sendViewport() { - postEvent("viewport_changed", "{ " - "height: window.innerHeight, " - "is_state_stable: true, " - "is_expanded: true }"); + postEvent( + "viewport_changed", + "{ " + "height: window.innerHeight, " + "is_state_stable: true, " + "is_expanded: true }" + ); } void Panel::setTitle(rpl::producer title) { - _widget->setTitle(std::move(title)); -} - -void Panel::sendDataMessage(const QJsonObject &args) { - if (args.isEmpty()) { - _delegate->botClose(); - return; - } - const auto data = args["data"].toString(); - if (data.isEmpty()) { - LOG(("BotWebView Error: Bad 'data' in sendDataMessage.")); - _delegate->botClose(); - return; - } - _delegate->botSendData(data.toUtf8()); -} - -void Panel::switchInlineQueryMessage(const QJsonObject &args) { - if (args.isEmpty()) { - _delegate->botClose(); - return; - } - const auto query = args["query"].toString(); - if (query.isEmpty()) { - LOG(("BotWebView Error: Bad 'query' in switchInlineQueryMessage.")); - _delegate->botClose(); - return; - } - const auto valid = base::flat_set{ - u"users"_q, - u"bots"_q, - u"groups"_q, - u"channels"_q, - }; - const auto typeArray = args["chat_types"].toArray(); - auto types = std::vector(); - for (const auto &value : typeArray) { - const auto type = value.toString(); - if (valid.contains(type)) { - types.push_back(type); - } else { - LOG(("BotWebView Error: " - "Bad chat type in switchInlineQueryMessage: %1.").arg(type)); - types.clear(); - break; - } - } - _delegate->botSwitchInlineQuery(types, query); -} - -void Panel::openTgLink(const QJsonObject &args) { - if (args.isEmpty()) { - LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'.")); - _delegate->botClose(); - return; - } - const auto path = args["path_full"].toString(); - if (path.isEmpty()) { - LOG(("BotWebView Error: Bad 'path_full' in 'web_app_open_tg_link'.")); - _delegate->botClose(); - return; - } - _delegate->botHandleLocalUri("https://t.me" + path, true); -} - -void Panel::openExternalLink(const QJsonObject &args) { - if (args.isEmpty()) { - _delegate->botClose(); - return; - } - const auto iv = args["try_instant_view"].toBool(); - const auto url = args["url"].toString(); - if (!_delegate->botValidateExternalLink(url)) { - LOG(("BotWebView Error: Bad url in openExternalLink: %1").arg(url)); - _delegate->botClose(); - return; - } else if (!allowOpenLink()) { - return; - } else if (iv) { - _delegate->botOpenIvLink(url); - } else { - File::OpenUrl(url); - } -} - -void Panel::openInvoice(const QJsonObject &args) { - if (args.isEmpty()) { - _delegate->botClose(); - return; - } - const auto slug = args["slug"].toString(); - if (slug.isEmpty()) { - LOG(("BotWebView Error: Bad 'slug' in openInvoice.")); - _delegate->botClose(); - return; - } - _delegate->botHandleInvoice(slug); -} - -void Panel::openPopup(const QJsonObject &args) { - if (args.isEmpty()) { - _delegate->botClose(); - return; - } - using Button = Webview::PopupArgs::Button; - using Type = Button::Type; - const auto message = args["message"].toString(); - const auto types = base::flat_map{ - { "default", Type::Default }, - { "ok", Type::Ok }, - { "close", Type::Close }, - { "cancel", Type::Cancel }, - { "destructive", Type::Destructive }, - }; - const auto buttonArray = args["buttons"].toArray(); - auto buttons = std::vector(); - for (const auto button : buttonArray) { - const auto fields = button.toObject(); - const auto i = types.find(fields["type"].toString()); - if (i == end(types)) { - LOG(("BotWebView Error: Bad 'type' in openPopup buttons.")); - _delegate->botClose(); - return; - } - buttons.push_back({ - .id = fields["id"].toString(), - .text = fields["text"].toString(), - .type = i->second, - }); - } - if (message.isEmpty()) { - LOG(("BotWebView Error: Bad 'message' in openPopup.")); - _delegate->botClose(); - return; - } else if (buttons.empty()) { - LOG(("BotWebView Error: Bad 'buttons' in openPopup.")); - _delegate->botClose(); - return; - } - const auto widget = _webview->window.widget(); - const auto weak = base::make_weak(this); - const auto result = Webview::ShowBlockingPopup({ - .parent = widget ? widget->window() : nullptr, - .title = args["title"].toString(), - .text = message, - .buttons = std::move(buttons), - }); - if (weak) { - postEvent("popup_closed", result.id - ? QJsonObject{ { u"button_id"_q, *result.id } } - : EventData()); - } -} - -void Panel::openScanQrPopup(const QJsonObject &args) { - const auto widget = _webview->window.widget(); - [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({ - .parent = widget ? widget->window() : nullptr, - .text = tr::lng_bot_no_scan_qr(tr::now), - .buttons = { { - .id = "ok", - .text = tr::lng_box_ok(tr::now), - .type = Webview::PopupArgs::Button::Type::Ok, - }}, - }); -} - -void Panel::openShareStory(const QJsonObject &args) { - const auto widget = _webview->window.widget(); - [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({ - .parent = widget ? widget->window() : nullptr, - .text = tr::lng_bot_no_share_story(tr::now), - .buttons = { { - .id = "ok", - .text = tr::lng_box_ok(tr::now), - .type = Webview::PopupArgs::Button::Type::Ok, - }}, - }); + _widget->setTitle(std::move(title)); +} + +void Panel::sendDataMessage(const QJsonObject& args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + const auto data = args["data"].toString(); + if (data.isEmpty()) { + LOG(("BotWebView Error: Bad 'data' in sendDataMessage.")); + _delegate->botClose(); + return; + } + _delegate->botSendData(data.toUtf8()); +} + +void Panel::switchInlineQueryMessage(const QJsonObject& args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + const auto query = args["query"].toString(); + if (query.isEmpty()) { + LOG(("BotWebView Error: Bad 'query' in switchInlineQueryMessage.")); + _delegate->botClose(); + return; + } + const auto valid = base::flat_set{ + u"users"_q, + u"bots"_q, + u"groups"_q, + u"channels"_q, + }; + const auto typeArray = args["chat_types"].toArray(); + auto types = std::vector(); + for (const auto& value : typeArray) { + const auto type = value.toString(); + if (valid.contains(type)) { + types.push_back(type); + } else { + LOG(("BotWebView Error: " + "Bad chat type in switchInlineQueryMessage: %1.") + .arg(type)); + types.clear(); + break; + } + } + _delegate->botSwitchInlineQuery(types, query); +} + +void Panel::openTgLink(const QJsonObject& args) { + if (args.isEmpty()) { + LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'.")); + _delegate->botClose(); + return; + } + const auto path = args["path_full"].toString(); + if (path.isEmpty()) { + LOG(("BotWebView Error: Bad 'path_full' in 'web_app_open_tg_link'.")); + _delegate->botClose(); + return; + } + _delegate->botHandleLocalUri("https://t.me" + path, true); +} + +void Panel::openExternalLink(const QJsonObject& args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + const auto iv = args["try_instant_view"].toBool(); + const auto url = args["url"].toString(); + if (!_delegate->botValidateExternalLink(url)) { + LOG(("BotWebView Error: Bad url in openExternalLink: %1").arg(url)); + _delegate->botClose(); + return; + } else if (!allowOpenLink()) { + return; + } else if (iv) { + _delegate->botOpenIvLink(url); + } else { + File::OpenUrl(url); + } +} + +void Panel::openInvoice(const QJsonObject& args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + const auto slug = args["slug"].toString(); + if (slug.isEmpty()) { + LOG(("BotWebView Error: Bad 'slug' in openInvoice.")); + _delegate->botClose(); + return; + } + _delegate->botHandleInvoice(slug); +} + +void Panel::openPopup(const QJsonObject& args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + using Button = Webview::PopupArgs::Button; + using Type = Button::Type; + const auto message = args["message"].toString(); + const auto types = base::flat_map{ + {"default", Type::Default}, + {"ok", Type::Ok}, + {"close", Type::Close}, + {"cancel", Type::Cancel}, + {"destructive", Type::Destructive}, + }; + const auto buttonArray = args["buttons"].toArray(); + auto buttons = std::vector(); + for (const auto button : buttonArray) { + const auto fields = button.toObject(); + const auto i = types.find(fields["type"].toString()); + if (i == end(types)) { + LOG(("BotWebView Error: Bad 'type' in openPopup buttons.")); + _delegate->botClose(); + return; + } + buttons.push_back({ + .id = fields["id"].toString(), + .text = fields["text"].toString(), + .type = i->second, + }); + } + if (message.isEmpty()) { + LOG(("BotWebView Error: Bad 'message' in openPopup.")); + _delegate->botClose(); + return; + } else if (buttons.empty()) { + LOG(("BotWebView Error: Bad 'buttons' in openPopup.")); + _delegate->botClose(); + return; + } + const auto widget = _webview->window.widget(); + const auto weak = base::make_weak(this); + const auto result = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .title = args["title"].toString(), + .text = message, + .buttons = std::move(buttons), + }); + if (weak) { + postEvent( + "popup_closed", result.id ? QJsonObject{{u"button_id"_q, *result.id}} : EventData() + ); + } +} + +void Panel::openScanQrPopup(const QJsonObject& args) { + const auto widget = _webview->window.widget(); + [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .text = tr::lng_bot_no_scan_qr(tr::now), + .buttons = {{ + .id = "ok", + .text = tr::lng_box_ok(tr::now), + .type = Webview::PopupArgs::Button::Type::Ok, + }}, + }); +} + +void Panel::openShareStory(const QJsonObject& args) { + const auto widget = _webview->window.widget(); + [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .text = tr::lng_bot_no_share_story(tr::now), + .buttons = {{ + .id = "ok", + .text = tr::lng_box_ok(tr::now), + .type = Webview::PopupArgs::Button::Type::Ok, + }}, + }); } void Panel::requestWriteAccess() { - if (_inBlockingRequest) { - replyRequestWriteAccess(false); - return; - } - _inBlockingRequest = true; - const auto finish = [=](bool allowed) { - _inBlockingRequest = false; - replyRequestWriteAccess(allowed); - }; - const auto weak = base::make_weak(this); - _delegate->botCheckWriteAccess([=](bool allowed) { - if (!weak) { - return; - } else if (allowed) { - finish(true); - return; - } - using Button = Webview::PopupArgs::Button; - const auto widget = _webview->window.widget(); - const auto integration = &Ui::Integration::Instance(); - const auto result = Webview::ShowBlockingPopup({ - .parent = widget ? widget->window() : nullptr, - .title = integration->phraseBotAllowWriteTitle(), - .text = integration->phraseBotAllowWrite(), - .buttons = { - { - .id = "allow", - .text = integration->phraseBotAllowWriteConfirm(), - }, - { .id = "cancel", .type = Button::Type::Cancel }, - }, - }); - if (!weak) { - return; - } else if (result.id == "allow") { - _delegate->botAllowWriteAccess(crl::guard(this, finish)); - } else { - finish(false); - } - }); + if (_inBlockingRequest) { + replyRequestWriteAccess(false); + return; + } + _inBlockingRequest = true; + const auto finish = [=](bool allowed) { + _inBlockingRequest = false; + replyRequestWriteAccess(allowed); + }; + const auto weak = base::make_weak(this); + _delegate->botCheckWriteAccess([=](bool allowed) { + if (!weak) { + return; + } else if (allowed) { + finish(true); + return; + } + using Button = Webview::PopupArgs::Button; + const auto widget = _webview->window.widget(); + const auto integration = &Ui::Integration::Instance(); + const auto result = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .title = integration->phraseBotAllowWriteTitle(), + .text = integration->phraseBotAllowWrite(), + .buttons = + { + { + .id = "allow", + .text = integration->phraseBotAllowWriteConfirm(), + }, + {.id = "cancel", .type = Button::Type::Cancel}, + }, + }); + if (!weak) { + return; + } else if (result.id == "allow") { + _delegate->botAllowWriteAccess(crl::guard(this, finish)); + } else { + finish(false); + } + }); } void Panel::replyRequestWriteAccess(bool allowed) { - postEvent("write_access_requested", QJsonObject{ - { u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q } - }); + postEvent( + "write_access_requested", + QJsonObject{{u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q}} + ); } void Panel::requestPhone() { - if (_inBlockingRequest) { - replyRequestPhone(false); - return; - } - _inBlockingRequest = true; - const auto finish = [=](bool shared) { - _inBlockingRequest = false; - replyRequestPhone(shared); - }; - using Button = Webview::PopupArgs::Button; - const auto widget = _webview->window.widget(); - const auto weak = base::make_weak(this); - const auto integration = &Ui::Integration::Instance(); - const auto result = Webview::ShowBlockingPopup({ - .parent = widget ? widget->window() : nullptr, - .title = integration->phraseBotSharePhoneTitle(), - .text = integration->phraseBotSharePhone(), - .buttons = { - { - .id = "share", - .text = integration->phraseBotSharePhoneConfirm(), - }, - { .id = "cancel", .type = Button::Type::Cancel }, - }, - }); - if (!weak) { - return; - } else if (result.id == "share") { - _delegate->botSharePhone(crl::guard(this, finish)); - } else { - finish(false); - } + if (_inBlockingRequest) { + replyRequestPhone(false); + return; + } + _inBlockingRequest = true; + const auto finish = [=](bool shared) { + _inBlockingRequest = false; + replyRequestPhone(shared); + }; + using Button = Webview::PopupArgs::Button; + const auto widget = _webview->window.widget(); + const auto weak = base::make_weak(this); + const auto integration = &Ui::Integration::Instance(); + const auto result = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .title = integration->phraseBotSharePhoneTitle(), + .text = integration->phraseBotSharePhone(), + .buttons = + { + { + .id = "share", + .text = integration->phraseBotSharePhoneConfirm(), + }, + {.id = "cancel", .type = Button::Type::Cancel}, + }, + }); + if (!weak) { + return; + } else if (result.id == "share") { + _delegate->botSharePhone(crl::guard(this, finish)); + } else { + finish(false); + } } void Panel::replyRequestPhone(bool shared) { - postEvent("phone_requested", QJsonObject{ - { u"status"_q, shared ? u"sent"_q : u"cancelled"_q } - }); -} - -void Panel::invokeCustomMethod(const QJsonObject &args) { - const auto requestId = args["req_id"]; - if (requestId.isUndefined()) { - return; - } - const auto finish = [=](QJsonObject response) { - replyCustomMethod(requestId, std::move(response)); - }; - auto callback = crl::guard(this, [=](CustomMethodResult result) { - if (result) { - auto error = QJsonParseError(); - const auto parsed = QJsonDocument::fromJson( - "{ \"result\": " + *result + '}', - &error); - if (error.error != QJsonParseError::NoError - || !parsed.isObject() - || parsed.object().size() != 1) { - finish({ { u"error"_q, u"Could not parse response."_q } }); - } else { - finish(parsed.object()); - } - } else { - finish({ { u"error"_q, result.error() } }); - } - }); - const auto params = QJsonDocument( - args["params"].toObject() - ).toJson(QJsonDocument::Compact); - _delegate->botInvokeCustomMethod({ - .method = args["method"].toString(), - .params = params, - .callback = std::move(callback), - }); + postEvent("phone_requested", QJsonObject{{u"status"_q, shared ? u"sent"_q : u"cancelled"_q}}); +} + +void Panel::invokeCustomMethod(const QJsonObject& args) { + const auto requestId = args["req_id"]; + if (requestId.isUndefined()) { + return; + } + const auto finish = [=](QJsonObject response) { + replyCustomMethod(requestId, std::move(response)); + }; + auto callback = crl::guard(this, [=](CustomMethodResult result) { + if (result) { + auto error = QJsonParseError(); + const auto parsed = QJsonDocument::fromJson("{ \"result\": " + *result + '}', &error); + if (error.error != QJsonParseError::NoError || !parsed.isObject() || + parsed.object().size() != 1) { + finish({{u"error"_q, u"Could not parse response."_q}}); + } else { + finish(parsed.object()); + } + } else { + finish({{u"error"_q, result.error()}}); + } + }); + const auto params = QJsonDocument(args["params"].toObject()).toJson(QJsonDocument::Compact); + _delegate->botInvokeCustomMethod({ + .method = args["method"].toString(), + .params = params, + .callback = std::move(callback), + }); } void Panel::replyCustomMethod(QJsonValue requestId, QJsonObject response) { - response["req_id"] = requestId; - postEvent(u"custom_method_invoked"_q, response); + response["req_id"] = requestId; + postEvent(u"custom_method_invoked"_q, response); } -void Panel::requestClipboardText(const QJsonObject &args) { - const auto requestId = args["req_id"]; - if (requestId.isUndefined()) { - return; - } - auto result = QJsonObject(); - result["req_id"] = requestId; - if (allowClipboardQuery()) { - result["data"] = QGuiApplication::clipboard()->text(); - } - postEvent(u"clipboard_text_received"_q, result); +void Panel::requestClipboardText(const QJsonObject& args) { + const auto requestId = args["req_id"]; + if (requestId.isUndefined()) { + return; + } + auto result = QJsonObject(); + result["req_id"] = requestId; + if (allowClipboardQuery()) { + result["data"] = QGuiApplication::clipboard()->text(); + } + postEvent(u"clipboard_text_received"_q, result); } bool Panel::allowOpenLink() const { - const auto now = crl::now(); - if (_mainButtonLastClick - && _mainButtonLastClick + kProcessClickTimeout >= now) { - _mainButtonLastClick = 0; - return true; - } - return true; + const auto now = crl::now(); + if (_mainButtonLastClick && _mainButtonLastClick + kProcessClickTimeout >= now) { + _mainButtonLastClick = 0; + return true; + } + return true; } bool Panel::allowClipboardQuery() const { - if (!_allowClipboardRead) { - return false; - } - const auto now = crl::now(); - if (_mainButtonLastClick - && _mainButtonLastClick + kProcessClickTimeout >= now) { - _mainButtonLastClick = 0; - return true; - } - return true; + if (!_allowClipboardRead) { + return false; + } + const auto now = crl::now(); + if (_mainButtonLastClick && _mainButtonLastClick + kProcessClickTimeout >= now) { + _mainButtonLastClick = 0; + return true; + } + return true; } void Panel::scheduleCloseWithConfirmation() { - if (!_closeWithConfirmationScheduled) { - _closeWithConfirmationScheduled = true; - InvokeQueued(_widget.get(), [=] { closeWithConfirmation(); }); - } + if (!_closeWithConfirmationScheduled) { + _closeWithConfirmationScheduled = true; + InvokeQueued(_widget.get(), [=] { closeWithConfirmation(); }); + } } void Panel::closeWithConfirmation() { - using Button = Webview::PopupArgs::Button; - const auto widget = _webview->window.widget(); - const auto weak = base::make_weak(this); - const auto integration = &Ui::Integration::Instance(); - const auto result = Webview::ShowBlockingPopup({ - .parent = widget ? widget->window() : nullptr, - .title = integration->phrasePanelCloseWarning(), - .text = integration->phrasePanelCloseUnsaved(), - .buttons = { - { - .id = "close", - .text = integration->phrasePanelCloseAnyway(), - .type = Button::Type::Destructive, - }, - { .id = "cancel", .type = Button::Type::Cancel }, - }, - .ignoreFloodCheck = true, - }); - if (!weak) { - return; - } else if (result.id == "close") { - _delegate->botClose(); - } else { - _closeWithConfirmationScheduled = false; - } -} - -void Panel::setupClosingBehaviour(const QJsonObject &args) { - _closeNeedConfirmation = args["need_confirmation"].toBool(); -} - -void Panel::processMainButtonMessage(const QJsonObject &args) { - if (args.isEmpty()) { - _delegate->botClose(); - return; - } - - const auto shown = [&] { - return _mainButton && !_mainButton->isHidden(); - }; - const auto wasShown = shown(); - const auto guard = gsl::finally([&] { - if (shown() != wasShown) { - crl::on_main(this, [=] { - sendViewport(); - }); - } - }); - - if (!_mainButton) { - if (args["is_visible"].toBool()) { - createMainButton(); - } else { - return; - } - } - - if (const auto bg = ParseColor(args["color"].toString())) { - _mainButton->updateBg(*bg); - _bgLifetime.destroy(); - } else { - _mainButton->updateBg(st::windowBgActive->c); - _bgLifetime = style::PaletteChanged( - ) | rpl::start_with_next([=] { - _mainButton->updateBg(st::windowBgActive->c); - }); - } - - if (const auto fg = ParseColor(args["text_color"].toString())) { - _mainButton->updateFg(*fg); - _fgLifetime.destroy(); - } else { - _mainButton->updateFg(st::windowFgActive->c); - _fgLifetime = style::PaletteChanged( - ) | rpl::start_with_next([=] { - _mainButton->updateFg(st::windowFgActive->c); - }); - } - - _mainButton->updateArgs({ - .isActive = args["is_active"].toBool(), - .isVisible = args["is_visible"].toBool(), - .isProgressVisible = args["is_progress_visible"].toBool(), - .text = args["text"].toString(), - }); -} - -void Panel::processBackButtonMessage(const QJsonObject &args) { - _widget->setBackAllowed(args["is_visible"].toBool()); -} - -void Panel::processSettingsButtonMessage(const QJsonObject &args) { - _hasSettingsButton = args["is_visible"].toBool(); -} - -void Panel::processHeaderColor(const QJsonObject &args) { - if (const auto color = ParseColor(args["color"].toString())) { - _widget->overrideTitleColor(color); - _headerColorLifetime.destroy(); - } else if (args["color_key"].toString() == u"secondary_bg_color"_q) { - _widget->overrideTitleColor(st::boxDividerBg->c); - _headerColorLifetime = style::PaletteChanged( - ) | rpl::start_with_next([=] { - _widget->overrideTitleColor(st::boxDividerBg->c); - }); - } else { - _widget->overrideTitleColor(std::nullopt); - _headerColorLifetime.destroy(); - } + using Button = Webview::PopupArgs::Button; + const auto widget = _webview->window.widget(); + const auto weak = base::make_weak(this); + const auto integration = &Ui::Integration::Instance(); + const auto result = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .title = integration->phrasePanelCloseWarning(), + .text = integration->phrasePanelCloseUnsaved(), + .buttons = + { + { + .id = "close", + .text = integration->phrasePanelCloseAnyway(), + .type = Button::Type::Destructive, + }, + {.id = "cancel", .type = Button::Type::Cancel}, + }, + .ignoreFloodCheck = true, + }); + if (!weak) { + return; + } else if (result.id == "close") { + _delegate->botClose(); + } else { + _closeWithConfirmationScheduled = false; + } +} + +void Panel::setupClosingBehaviour(const QJsonObject& args) { + _closeNeedConfirmation = args["need_confirmation"].toBool(); +} + +void Panel::processMainButtonMessage(const QJsonObject& args) { + if (args.isEmpty()) { + _delegate->botClose(); + return; + } + + const auto shown = [&] { return _mainButton && !_mainButton->isHidden(); }; + const auto wasShown = shown(); + const auto guard = gsl::finally([&] { + if (shown() != wasShown) { + crl::on_main(this, [=] { sendViewport(); }); + } + }); + + if (!_mainButton) { + if (args["is_visible"].toBool()) { + createMainButton(); + } else { + return; + } + } + + if (const auto bg = ParseColor(args["color"].toString())) { + _mainButton->updateBg(*bg); + _bgLifetime.destroy(); + } else { + _mainButton->updateBg(st::windowBgActive->c); + _bgLifetime = style::PaletteChanged() | + rpl::start_with_next([=] { _mainButton->updateBg(st::windowBgActive->c); }); + } + + if (const auto fg = ParseColor(args["text_color"].toString())) { + _mainButton->updateFg(*fg); + _fgLifetime.destroy(); + } else { + _mainButton->updateFg(st::windowFgActive->c); + _fgLifetime = style::PaletteChanged() | + rpl::start_with_next([=] { _mainButton->updateFg(st::windowFgActive->c); }); + } + + _mainButton->updateArgs({ + .isActive = args["is_active"].toBool(), + .isVisible = args["is_visible"].toBool(), + .isProgressVisible = args["is_progress_visible"].toBool(), + .text = args["text"].toString(), + }); +} + +void Panel::processBackButtonMessage(const QJsonObject& args) { + _widget->setBackAllowed(args["is_visible"].toBool()); +} + +void Panel::processSettingsButtonMessage(const QJsonObject& args) { + _hasSettingsButton = args["is_visible"].toBool(); +} + +void Panel::processHeaderColor(const QJsonObject& args) { + if (const auto color = ParseColor(args["color"].toString())) { + _widget->overrideTitleColor(color); + _headerColorLifetime.destroy(); + } else if (args["color_key"].toString() == u"secondary_bg_color"_q) { + _widget->overrideTitleColor(st::boxDividerBg->c); + _headerColorLifetime = style::PaletteChanged() | rpl::start_with_next([=] { + _widget->overrideTitleColor(st::boxDividerBg->c); + }); + } else { + _widget->overrideTitleColor(std::nullopt); + _headerColorLifetime.destroy(); + } } void Panel::createMainButton() { - _mainButton = std::make_unique