495 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			495 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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_webview.h"
 | |
| 
 | |
| #include "core/file_utilities.h"
 | |
| #include "ui/effects/radial_animation.h"
 | |
| #include "ui/layers/box_content.h"
 | |
| #include "ui/text/text_utilities.h"
 | |
| #include "ui/widgets/separate_panel.h"
 | |
| #include "ui/widgets/labels.h"
 | |
| #include "ui/wrap/fade_wrap.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "webview/webview_embed.h"
 | |
| #include "webview/webview_interface.h"
 | |
| #include "base/debug_log.h"
 | |
| #include "styles/style_payments.h"
 | |
| #include "styles/style_layers.h"
 | |
| 
 | |
| #include <QtCore/QJsonDocument>
 | |
| #include <QtCore/QJsonObject>
 | |
| #include <QtCore/QJsonArray>
 | |
| 
 | |
| namespace Ui::BotWebView {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kProgressDuration = crl::time(200);
 | |
| constexpr auto kProgressOpacity = 0.3;
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct Panel::Progress {
 | |
| 	Progress(QWidget *parent, Fn<QRect()> rect);
 | |
| 
 | |
| 	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<RpWidget> lastHidingBox;
 | |
| 	rpl::lifetime lifetime;
 | |
| };
 | |
| 
 | |
| Panel::WebviewWithLifetime::WebviewWithLifetime(
 | |
| 	QWidget *parent,
 | |
| 	Webview::WindowConfig config)
 | |
| : window(parent, std::move(config)) {
 | |
| }
 | |
| 
 | |
| Panel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)
 | |
| : widget(parent)
 | |
| , animation(
 | |
| 	[=] { if (!anim::Disabled()) widget.update(rect()); },
 | |
| 	st::paymentsLoading) {
 | |
| }
 | |
| 
 | |
| Panel::Panel(
 | |
| 	const QString &userDataPath,
 | |
| 	rpl::producer<QString> title,
 | |
| 	Fn<void(QByteArray)> sendData,
 | |
| 	Fn<void()> close,
 | |
| 	Fn<QByteArray()> themeParams)
 | |
| : _userDataPath(userDataPath)
 | |
| , _sendData(std::move(sendData))
 | |
| , _close(std::move(close))
 | |
| , _widget(std::make_unique<SeparatePanel>()) {
 | |
| 	_widget->setInnerSize(st::paymentsPanelSize);
 | |
| 	_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
 | |
| 
 | |
| 	_widget->closeRequests(
 | |
| 	) | rpl::start_with_next(_close, _widget->lifetime());
 | |
| 
 | |
| 	_widget->closeEvents(
 | |
| 	) | rpl::start_with_next(_close, _widget->lifetime());
 | |
| 
 | |
| 	style::PaletteChanged(
 | |
| 	) | rpl::filter([=] {
 | |
| 		return !_themeUpdateScheduled;
 | |
| 	}) | rpl::start_with_next([=] {
 | |
| 		_themeUpdateScheduled = true;
 | |
| 		crl::on_main(_widget.get(), [=] {
 | |
| 			_themeUpdateScheduled = false;
 | |
| 			updateThemeParams(themeParams());
 | |
| 		});
 | |
| 	}, _widget->lifetime());
 | |
| 
 | |
| 	setTitle(std::move(title));
 | |
| }
 | |
| 
 | |
| Panel::~Panel() {
 | |
| 	_webview = nullptr;
 | |
| 	_progress = nullptr;
 | |
| 	_widget = nullptr;
 | |
| }
 | |
| 
 | |
| void Panel::requestActivate() {
 | |
| 	_widget->showAndActivate();
 | |
| }
 | |
| 
 | |
| void Panel::toggleProgress(bool shown) {
 | |
| 	if (!_progress) {
 | |
| 		if (!shown) {
 | |
| 			return;
 | |
| 		}
 | |
| 		_progress = std::make_unique<Progress>(
 | |
| 			_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());
 | |
| }
 | |
| 
 | |
| 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);
 | |
| }
 | |
| 
 | |
| 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();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Panel::showWebviewProgress() {
 | |
| 	if (_webviewProgress && _progress && _progress->shown) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_webviewProgress = true;
 | |
| 	toggleProgress(true);
 | |
| }
 | |
| 
 | |
| void Panel::hideWebviewProgress() {
 | |
| 	if (!_webviewProgress) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_webviewProgress = false;
 | |
| 	toggleProgress(false);
 | |
| }
 | |
| 
 | |
| bool Panel::showWebview(
 | |
| 		const QString &url,
 | |
| 		rpl::producer<QString> bottomText) {
 | |
| 	if (!_webview && !createWebview()) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	const auto allowBack = false;
 | |
| 	showWebviewProgress();
 | |
| 	_widget->destroyLayer();
 | |
| 	_webview->window.navigate(url);
 | |
| 	_widget->setBackAllowed(allowBack);
 | |
| 	if (bottomText) {
 | |
| 		const auto &padding = st::paymentsPanelPadding;
 | |
| 		const auto label = CreateChild<FlatLabel>(
 | |
| 			_webviewBottom.get(),
 | |
| 			std::move(bottomText),
 | |
| 			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);
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| bool Panel::createWebview() {
 | |
| 	auto container = base::make_unique_q<RpWidget>(_widget.get());
 | |
| 
 | |
| 	_webviewBottom = std::make_unique<RpWidget>(_widget.get());
 | |
| 	const auto bottom = _webviewBottom.get();
 | |
| 	bottom->show();
 | |
| 
 | |
| 	bottom->heightValue(
 | |
| 	) | rpl::start_with_next([=, raw = container.get()](int height) {
 | |
| 		const auto inner = _widget->innerGeometry();
 | |
| 		bottom->move(inner.x(), inner.y() + inner.height() - height);
 | |
| 		raw->resize(inner.width(), inner.height() - height);
 | |
| 		bottom->resizeToWidth(inner.width());
 | |
| 	}, bottom->lifetime());
 | |
| 	container->show();
 | |
| 
 | |
| 	_webview = std::make_unique<WebviewWithLifetime>(
 | |
| 		container.get(),
 | |
| 		Webview::WindowConfig{
 | |
| 			.userDataPath = _userDataPath,
 | |
| 		});
 | |
| 	const auto raw = &_webview->window;
 | |
| 	QObject::connect(container.get(), &QObject::destroyed, [=] {
 | |
| 		if (_webview && &_webview->window == raw) {
 | |
| 			_webview = nullptr;
 | |
| 			if (_webviewProgress) {
 | |
| 				hideWebviewProgress();
 | |
| 				if (_progress && !_progress->shown) {
 | |
| 					_progress = nullptr;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		if (_webviewBottom.get() == bottom) {
 | |
| 			_webviewBottom = nullptr;
 | |
| 		}
 | |
| 	});
 | |
| 	if (!raw->widget()) {
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	container->geometryValue(
 | |
| 	) | rpl::start_with_next([=](QRect geometry) {
 | |
| 		raw->widget()->setGeometry(geometry);
 | |
| 	}, _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();
 | |
| 		if (command == "web_app_close") {
 | |
| 			_close();
 | |
| 		} else if (command == "web_app_data_send") {
 | |
| 			auto error = QJsonParseError();
 | |
| 			auto json = list.at(1).toString();
 | |
| 			const auto dictionary = QJsonDocument::fromJson(
 | |
| 				json.toUtf8(),
 | |
| 				&error);
 | |
| 			if (error.error != QJsonParseError::NoError) {
 | |
| 				LOG(("BotWebView Error: Could not parse \"%1\".").arg(json));
 | |
| 				_close();
 | |
| 				return;
 | |
| 			}
 | |
| 			const auto data = dictionary.object()["data"].toString();
 | |
| 			if (data.isEmpty()) {
 | |
| 				LOG(("BotWebView Error: Bad data \"%1\".").arg(json));
 | |
| 				_close();
 | |
| 				return;
 | |
| 			}
 | |
| 			_sendData(data.toUtf8());
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	raw->setNavigationStartHandler([=](const QString &uri) {
 | |
| 		showWebviewProgress();
 | |
| 		return true;
 | |
| 	});
 | |
| 	raw->setNavigationDoneHandler([=](bool success) {
 | |
| 		hideWebviewProgress();
 | |
| 	});
 | |
| 
 | |
| 	raw->init(R"(
 | |
| window.TelegramWebviewProxy = {
 | |
| postEvent: function(eventType, eventData) {
 | |
| 	if (window.external && window.external.invoke) {
 | |
| 		window.external.invoke(JSON.stringify([eventType, eventData]));
 | |
| 	}
 | |
| }
 | |
| };)");
 | |
| 
 | |
| 	_widget->showInner(std::move(container));
 | |
| 
 | |
| 	setupProgressGeometry();
 | |
| 
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void Panel::setTitle(rpl::producer<QString> title) {
 | |
| 	_widget->setTitle(std::move(title));
 | |
| }
 | |
| 
 | |
| void Panel::showBox(object_ptr<BoxContent> box) {
 | |
| 	if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
 | |
| 		const auto hideNow = !widget->isHidden();
 | |
| 		if (hideNow || _webview->lastHidingBox) {
 | |
| 			const auto raw = _webview->lastHidingBox = box.data();
 | |
| 			box->boxClosing(
 | |
| 			) | rpl::start_with_next([=] {
 | |
| 				const auto widget = _webview
 | |
| 					? _webview->window.widget()
 | |
| 					: nullptr;
 | |
| 				if (widget
 | |
| 					&& widget->isHidden()
 | |
| 					&& _webview->lastHidingBox == raw) {
 | |
| 					widget->show();
 | |
| 				}
 | |
| 			}, _webview->lifetime);
 | |
| 			if (hideNow) {
 | |
| 				widget->hide();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	_widget->showBox(
 | |
| 		std::move(box),
 | |
| 		LayerOption::KeepOther,
 | |
| 		anim::type::normal);
 | |
| }
 | |
| 
 | |
| void Panel::showToast(const TextWithEntities &text) {
 | |
| 	_widget->showToast(text);
 | |
| }
 | |
| 
 | |
| void Panel::showCriticalError(const TextWithEntities &text) {
 | |
| 	_progress = nullptr;
 | |
| 	_webviewProgress = false;
 | |
| 	auto error = base::make_unique_q<PaddingWrap<FlatLabel>>(
 | |
| 		_widget.get(),
 | |
| 		object_ptr<FlatLabel>(
 | |
| 			_widget.get(),
 | |
| 			rpl::single(text),
 | |
| 			st::paymentsCriticalError),
 | |
| 		st::paymentsCriticalErrorPadding);
 | |
| 	error->entity()->setClickHandlerFilter([=](
 | |
| 			const ClickHandlerPtr &handler,
 | |
| 			Qt::MouseButton) {
 | |
| 		const auto entity = handler->getTextEntity();
 | |
| 		if (entity.type != EntityType::CustomUrl) {
 | |
| 			return true;
 | |
| 		}
 | |
| 		File::OpenUrl(entity.data);
 | |
| 		return false;
 | |
| 	});
 | |
| 	_widget->showInner(std::move(error));
 | |
| }
 | |
| 
 | |
| void Panel::updateThemeParams(const QByteArray &json) {
 | |
| 	if (!_webview || !_webview->window.widget()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_webview->window.eval(R"(
 | |
| if (window.TelegramGameProxy) {
 | |
| 	window.TelegramGameProxy.receiveEvent(
 | |
| 		"theme_changed",
 | |
| 		{ "theme_params": )" + json + R"( });
 | |
| }
 | |
| )");
 | |
| }
 | |
| 
 | |
| void Panel::showWebviewError(
 | |
| 		const QString &text,
 | |
| 		const Webview::Available &information) {
 | |
| 	using Error = Webview::Available::Error;
 | |
| 	Expects(information.error != Error::None);
 | |
| 
 | |
| 	auto rich = TextWithEntities{ text };
 | |
| 	rich.append("\n\n");
 | |
| 	switch (information.error) {
 | |
| 	case Error::NoWebview2: {
 | |
| 		const auto command = QString(QChar(TextCommand));
 | |
| 		const auto text = tr::lng_payments_webview_install_edge(
 | |
| 			tr::now,
 | |
| 			lt_link,
 | |
| 			command);
 | |
| 		const auto parts = text.split(command);
 | |
| 		rich.append(parts.value(0))
 | |
| 			.append(Text::Link(
 | |
| 				"Microsoft Edge WebView2 Runtime",
 | |
| 				"https://go.microsoft.com/fwlink/p/?LinkId=2124703"))
 | |
| 			.append(parts.value(1));
 | |
| 	} break;
 | |
| 	case Error::NoGtkOrWebkit2Gtk:
 | |
| 		rich.append(tr::lng_payments_webview_install_webkit(tr::now));
 | |
| 		break;
 | |
| 	case Error::MutterWM:
 | |
| 		rich.append(tr::lng_payments_webview_switch_mutter(tr::now));
 | |
| 		break;
 | |
| 	case Error::Wayland:
 | |
| 		rich.append(tr::lng_payments_webview_switch_wayland(tr::now));
 | |
| 		break;
 | |
| 	default:
 | |
| 		rich.append(QString::fromStdString(information.details));
 | |
| 		break;
 | |
| 	}
 | |
| 	showCriticalError(rich);
 | |
| }
 | |
| 
 | |
| rpl::lifetime &Panel::lifetime() {
 | |
| 	return _widget->lifetime();
 | |
| }
 | |
| 
 | |
| std::unique_ptr<Panel> Show(Args &&args) {
 | |
| 	auto result = std::make_unique<Panel>(
 | |
| 		args.userDataPath,
 | |
| 		std::move(args.title),
 | |
| 		std::move(args.sendData),
 | |
| 		std::move(args.close),
 | |
| 		std::move(args.themeParams));
 | |
| 	if (!result->showWebview(args.url, std::move(args.bottom))) {
 | |
| 		const auto available = Webview::Availability();
 | |
| 		if (available.error != Webview::Available::Error::None) {
 | |
| 			result->showWebviewError(
 | |
| 				tr::lng_bot_no_webview(tr::now),
 | |
| 				available);
 | |
| 		} else {
 | |
| 			result->showCriticalError({
 | |
| 				"Error: Could not initialize WebView." });
 | |
| 		}
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| } // namespace Ui::BotWebView
 | 
