attach_bot_webview.cpp 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "ui/chat/attach/attach_bot_webview.h"
  8. #include "core/file_utilities.h"
  9. #include "ui/boxes/confirm_box.h"
  10. #include "ui/chat/attach/attach_bot_downloads.h"
  11. #include "ui/effects/radial_animation.h"
  12. #include "ui/effects/ripple_animation.h"
  13. #include "ui/layers/box_content.h"
  14. #include "ui/style/style_core_palette.h"
  15. #include "ui/text/text_utilities.h"
  16. #include "ui/widgets/separate_panel.h"
  17. #include "ui/widgets/buttons.h"
  18. #include "ui/widgets/labels.h"
  19. #include "ui/widgets/menu/menu_add_action_callback.h"
  20. #include "ui/wrap/fade_wrap.h"
  21. #include "ui/integration.h"
  22. #include "ui/painter.h"
  23. #include "ui/rect.h"
  24. #include "ui/ui_utility.h"
  25. #include "lang/lang_keys.h"
  26. #include "webview/webview_embed.h"
  27. #include "webview/webview_dialog.h"
  28. #include "webview/webview_interface.h"
  29. #include "base/debug_log.h"
  30. #include "base/invoke_queued.h"
  31. #include "base/qt_signal_producer.h"
  32. #include "styles/style_chat.h"
  33. #include "styles/style_payments.h"
  34. #include "styles/style_layers.h"
  35. #include "styles/style_menu_icons.h"
  36. #include <QtCore/QJsonDocument>
  37. #include <QtCore/QJsonObject>
  38. #include <QtCore/QJsonArray>
  39. #include <QtGui/QGuiApplication>
  40. #include <QtGui/QClipboard>
  41. #include <QtGui/QWindow>
  42. #include <QtGui/QScreen>
  43. #include <QtGui/qpa/qplatformscreen.h>
  44. namespace Ui::BotWebView {
  45. namespace {
  46. constexpr auto kProcessClickTimeout = crl::time(1000);
  47. constexpr auto kProgressDuration = crl::time(200);
  48. constexpr auto kProgressOpacity = 0.3;
  49. constexpr auto kLightnessThreshold = 128;
  50. constexpr auto kLightnessDelta = 32;
  51. struct ButtonArgs {
  52. bool isActive = false;
  53. bool isVisible = false;
  54. bool isProgressVisible = false;
  55. QString text;
  56. };
  57. [[nodiscard]] RectPart ParsePosition(const QString &position) {
  58. if (position == u"left"_q) {
  59. return RectPart::Left;
  60. } else if (position == u"top"_q) {
  61. return RectPart::Top;
  62. } else if (position == u"right"_q) {
  63. return RectPart::Right;
  64. } else if (position == u"bottom"_q) {
  65. return RectPart::Bottom;
  66. }
  67. return RectPart::Left;
  68. }
  69. [[nodiscard]] QJsonObject ParseMethodArgs(const QString &json) {
  70. if (json.isEmpty()) {
  71. return {};
  72. }
  73. auto error = QJsonParseError();
  74. const auto dictionary = QJsonDocument::fromJson(json.toUtf8(), &error);
  75. if (error.error != QJsonParseError::NoError) {
  76. LOG(("BotWebView Error: Could not parse \"%1\".").arg(json));
  77. return {};
  78. }
  79. return dictionary.object();
  80. }
  81. [[nodiscard]] std::optional<QColor> ParseColor(const QString &text) {
  82. if (!text.startsWith('#') || text.size() != 7) {
  83. return {};
  84. }
  85. const auto data = text.data() + 1;
  86. const auto hex = [&](int from) -> std::optional<int> {
  87. const auto parse = [](QChar ch) -> std::optional<int> {
  88. const auto code = ch.unicode();
  89. return (code >= 'a' && code <= 'f')
  90. ? std::make_optional(10 + (code - 'a'))
  91. : (code >= 'A' && code <= 'F')
  92. ? std::make_optional(10 + (code - 'A'))
  93. : (code >= '0' && code <= '9')
  94. ? std::make_optional(code - '0')
  95. : std::nullopt;
  96. };
  97. const auto h = parse(data[from]), l = parse(data[from + 1]);
  98. return (h && l) ? std::make_optional(*h * 16 + *l) : std::nullopt;
  99. };
  100. const auto r = hex(0), g = hex(2), b = hex(4);
  101. return (r && g && b) ? QColor(*r, *g, *b) : std::optional<QColor>();
  102. }
  103. [[nodiscard]] QColor ResolveRipple(QColor background) {
  104. auto hue = 0;
  105. auto saturation = 0;
  106. auto lightness = 0;
  107. auto alpha = 0;
  108. background.getHsv(&hue, &saturation, &lightness, &alpha);
  109. return QColor::fromHsv(
  110. hue,
  111. saturation,
  112. lightness - (lightness > kLightnessThreshold
  113. ? kLightnessDelta
  114. : -kLightnessDelta),
  115. alpha);
  116. }
  117. [[nodiscard]] const style::color *LookupNamedColor(const QString &key) {
  118. if (key == u"secondary_bg_color"_q) {
  119. return &st::boxDividerBg;
  120. } else if (key == u"bottom_bar_bg_color"_q) {
  121. return &st::windowBg;
  122. }
  123. return nullptr;
  124. }
  125. } // namespace
  126. class Panel::Button final : public RippleButton {
  127. public:
  128. Button(QWidget *parent, const style::RoundButton &st);
  129. ~Button();
  130. void updateBg(QColor bg);
  131. void updateBg(not_null<const style::color*> paletteBg);
  132. void updateFg(QColor fg);
  133. void updateFg(not_null<const style::color*> paletteFg);
  134. void updateArgs(ButtonArgs &&args);
  135. private:
  136. void paintEvent(QPaintEvent *e) override;
  137. QImage prepareRippleMask() const override;
  138. QPoint prepareRippleStartPosition() const override;
  139. void toggleProgress(bool shown);
  140. void setupProgressGeometry();
  141. std::unique_ptr<Progress> _progress;
  142. rpl::variable<QString> _textFull;
  143. Ui::Text::String _text;
  144. const style::RoundButton &_st;
  145. QColor _fg;
  146. style::owned_color _bg;
  147. RoundRect _roundRect;
  148. rpl::lifetime _bgLifetime;
  149. rpl::lifetime _fgLifetime;
  150. };
  151. struct Panel::Progress {
  152. Progress(QWidget *parent, Fn<QRect()> rect);
  153. RpWidget widget;
  154. InfiniteRadialAnimation animation;
  155. Animations::Simple shownAnimation;
  156. bool shown = true;
  157. rpl::lifetime geometryLifetime;
  158. };
  159. struct Panel::WebviewWithLifetime {
  160. WebviewWithLifetime(
  161. QWidget *parent = nullptr,
  162. Webview::WindowConfig config = Webview::WindowConfig());
  163. Webview::Window window;
  164. std::vector<QPointer<RpWidget>> boxes;
  165. rpl::lifetime boxesLifetime;
  166. rpl::lifetime lifetime;
  167. };
  168. Panel::Button::Button(QWidget *parent, const style::RoundButton &st)
  169. : RippleButton(parent, st.ripple)
  170. , _st(st)
  171. , _bg(st::windowBgActive->c)
  172. , _roundRect(st::callRadius, st::windowBgActive) {
  173. _textFull.value(
  174. ) | rpl::start_with_next([=](const QString &text) {
  175. _text.setText(st::semiboldTextStyle, text);
  176. update();
  177. }, lifetime());
  178. resize(
  179. _st.padding.left() + _text.maxWidth() + _st.padding.right(),
  180. _st.padding.top() + _st.height + _st.padding.bottom());
  181. }
  182. Panel::Button::~Button() = default;
  183. void Panel::Button::updateBg(QColor bg) {
  184. _bg.update(bg);
  185. _roundRect.setColor(_bg.color());
  186. _bgLifetime.destroy();
  187. update();
  188. }
  189. void Panel::Button::updateBg(not_null<const style::color*> paletteBg) {
  190. updateBg((*paletteBg)->c);
  191. _bgLifetime = style::PaletteChanged(
  192. ) | rpl::start_with_next([=] {
  193. updateBg((*paletteBg)->c);
  194. });
  195. }
  196. void Panel::Button::updateFg(QColor fg) {
  197. _fg = fg;
  198. _fgLifetime.destroy();
  199. update();
  200. }
  201. void Panel::Button::updateFg(not_null<const style::color*> paletteFg) {
  202. updateFg((*paletteFg)->c);
  203. _fgLifetime = style::PaletteChanged(
  204. ) | rpl::start_with_next([=] {
  205. updateFg((*paletteFg)->c);
  206. });
  207. }
  208. void Panel::Button::updateArgs(ButtonArgs &&args) {
  209. _textFull = std::move(args.text);
  210. setDisabled(!args.isActive);
  211. setPointerCursor(false);
  212. setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor);
  213. setVisible(args.isVisible);
  214. toggleProgress(args.isProgressVisible);
  215. update();
  216. }
  217. void Panel::Button::toggleProgress(bool shown) {
  218. if (!_progress) {
  219. if (!shown) {
  220. return;
  221. }
  222. _progress = std::make_unique<Progress>(
  223. this,
  224. [=] { return _progress->widget.rect(); });
  225. _progress->widget.paintRequest(
  226. ) | rpl::start_with_next([=](QRect clip) {
  227. auto p = QPainter(&_progress->widget);
  228. p.setOpacity(
  229. _progress->shownAnimation.value(_progress->shown ? 1. : 0.));
  230. auto thickness = st::paymentsLoading.thickness;
  231. const auto rect = _progress->widget.rect().marginsRemoved(
  232. { thickness, thickness, thickness, thickness });
  233. InfiniteRadialAnimation::Draw(
  234. p,
  235. _progress->animation.computeState(),
  236. rect.topLeft(),
  237. rect.size() - QSize(),
  238. _progress->widget.width(),
  239. _fg,
  240. thickness);
  241. }, _progress->widget.lifetime());
  242. _progress->widget.show();
  243. _progress->animation.start();
  244. } else if (_progress->shown == shown) {
  245. return;
  246. }
  247. const auto callback = [=] {
  248. if (!_progress->shownAnimation.animating() && !_progress->shown) {
  249. _progress = nullptr;
  250. } else {
  251. _progress->widget.update();
  252. }
  253. };
  254. _progress->shown = shown;
  255. _progress->shownAnimation.start(
  256. callback,
  257. shown ? 0. : 1.,
  258. shown ? 1. : 0.,
  259. kProgressDuration);
  260. if (shown) {
  261. setupProgressGeometry();
  262. }
  263. }
  264. void Panel::Button::setupProgressGeometry() {
  265. if (!_progress || !_progress->shown) {
  266. return;
  267. }
  268. _progress->geometryLifetime.destroy();
  269. sizeValue(
  270. ) | rpl::start_with_next([=](QSize outer) {
  271. const auto height = outer.height();
  272. const auto size = st::paymentsLoading.size;
  273. const auto skip = (height - size.height()) / 2;
  274. const auto right = outer.width();
  275. const auto top = outer.height() - height;
  276. _progress->widget.setGeometry(QRect{
  277. QPoint(right - skip - size.width(), top + skip),
  278. size });
  279. }, _progress->geometryLifetime);
  280. _progress->widget.show();
  281. _progress->widget.raise();
  282. if (_progress->shown
  283. && Ui::AppInFocus()
  284. && Ui::InFocusChain(_progress->widget.window())) {
  285. _progress->widget.setFocus();
  286. }
  287. }
  288. void Panel::Button::paintEvent(QPaintEvent *e) {
  289. Painter p(this);
  290. _roundRect.paint(p, rect());
  291. if (!isDisabled()) {
  292. const auto ripple = ResolveRipple(_bg.color()->c);
  293. paintRipple(p, rect().topLeft(), &ripple);
  294. }
  295. p.setFont(_st.style.font);
  296. const auto height = rect().height();
  297. const auto progress = st::paymentsLoading.size;
  298. const auto skip = (height - progress.height()) / 2;
  299. const auto padding = skip + progress.width() + skip;
  300. const auto space = width() - padding * 2;
  301. const auto textWidth = std::min(space, _text.maxWidth());
  302. const auto textTop = _st.padding.top() + _st.textTop;
  303. const auto textLeft = padding + (space - textWidth) / 2;
  304. p.setPen(_fg);
  305. _text.drawLeftElided(p, textLeft, textTop, textWidth, width());
  306. }
  307. QImage Panel::Button::prepareRippleMask() const {
  308. return RippleAnimation::RoundRectMask(size(), st::callRadius);
  309. }
  310. QPoint Panel::Button::prepareRippleStartPosition() const {
  311. return mapFromGlobal(QCursor::pos())
  312. - QPoint(_st.padding.left(), _st.padding.top());
  313. }
  314. Panel::WebviewWithLifetime::WebviewWithLifetime(
  315. QWidget *parent,
  316. Webview::WindowConfig config)
  317. : window(parent, std::move(config)) {
  318. }
  319. Panel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)
  320. : widget(parent)
  321. , animation(
  322. [=] { if (!anim::Disabled()) widget.update(rect()); },
  323. st::paymentsLoading) {
  324. }
  325. Panel::Panel(Args &&args)
  326. : _storageId(args.storageId)
  327. , _delegate(args.delegate)
  328. , _menuButtons(args.menuButtons)
  329. , _widget(std::make_unique<SeparatePanel>(Ui::SeparatePanelArgs{
  330. .menuSt = &st::botWebViewMenu,
  331. }))
  332. , _fullscreen(args.fullscreen)
  333. , _allowClipboardRead(args.allowClipboardRead) {
  334. _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
  335. _widget->setInnerSize(st::botWebViewPanelSize, true);
  336. const auto panel = _widget.get();
  337. rpl::duplicate(
  338. args.title
  339. ) | rpl::start_with_next([=](const QString &title) {
  340. const auto value = tr::lng_credits_box_history_entry_miniapp(tr::now)
  341. + u": "_q
  342. + title;
  343. panel->window()->setWindowTitle(value);
  344. }, panel->lifetime());
  345. const auto params = _delegate->botThemeParams();
  346. updateColorOverrides(params);
  347. _fullscreen.value(
  348. ) | rpl::start_with_next([=](bool fullscreen) {
  349. _widget->toggleFullScreen(fullscreen);
  350. layoutButtons();
  351. sendFullScreen();
  352. sendSafeArea();
  353. sendContentSafeArea();
  354. }, _widget->lifetime());
  355. _widget->fullScreenValue(
  356. ) | rpl::start_with_next([=](bool fullscreen) {
  357. _fullscreen = fullscreen;
  358. }, _widget->lifetime());
  359. _widget->closeRequests(
  360. ) | rpl::start_with_next([=] {
  361. if (_closeNeedConfirmation) {
  362. scheduleCloseWithConfirmation();
  363. } else {
  364. _delegate->botClose();
  365. }
  366. }, _widget->lifetime());
  367. _widget->closeEvents(
  368. ) | rpl::filter([=] {
  369. return !_hiddenForPayment;
  370. }) | rpl::start_with_next([=] {
  371. _delegate->botClose();
  372. }, _widget->lifetime());
  373. _widget->backRequests(
  374. ) | rpl::start_with_next([=] {
  375. postEvent("back_button_pressed");
  376. }, _widget->lifetime());
  377. rpl::merge(
  378. style::PaletteChanged(),
  379. _themeUpdateForced.events()
  380. ) | rpl::filter([=] {
  381. return !_themeUpdateScheduled;
  382. }) | rpl::start_with_next([=] {
  383. _themeUpdateScheduled = true;
  384. crl::on_main(_widget.get(), [=] {
  385. _themeUpdateScheduled = false;
  386. updateThemeParams(_delegate->botThemeParams());
  387. });
  388. }, _widget->lifetime());
  389. setTitle(std::move(args.title));
  390. _widget->setTitleBadge(std::move(args.titleBadge));
  391. if (!showWebview(std::move(args), params)) {
  392. const auto available = Webview::Availability();
  393. if (available.error != Webview::Available::Error::None) {
  394. showWebviewError(tr::lng_bot_no_webview(tr::now), available);
  395. } else {
  396. showCriticalError({ "Error: Could not initialize WebView." });
  397. }
  398. }
  399. }
  400. Panel::~Panel() {
  401. base::take(_webview);
  402. _progress = nullptr;
  403. _widget = nullptr;
  404. }
  405. void Panel::setupDownloadsProgress(
  406. not_null<RpWidget*> button,
  407. rpl::producer<DownloadsProgress> progress,
  408. bool fullscreen) {
  409. const auto widget = Ui::CreateChild<RpWidget>(button.get());
  410. widget->show();
  411. widget->setAttribute(Qt::WA_TransparentForMouseEvents);
  412. button->sizeValue() | rpl::start_with_next([=](QSize size) {
  413. widget->setGeometry(QRect(QPoint(), size));
  414. }, widget->lifetime());
  415. struct State {
  416. State(QWidget *parent)
  417. : animation([=](crl::time now) {
  418. const auto total = progress.total;
  419. const auto current = total
  420. ? (progress.ready / float64(total))
  421. : 0.;
  422. const auto updated = animation.update(current, false, now);
  423. if (!anim::Disabled() || updated) {
  424. parent->update();
  425. }
  426. }) {
  427. }
  428. DownloadsProgress progress;
  429. RadialAnimation animation;
  430. Animations::Simple fade;
  431. bool shown = false;
  432. };
  433. const auto state = widget->lifetime().make_state<State>(widget);
  434. std::move(
  435. progress
  436. ) | rpl::start_with_next([=](DownloadsProgress progress) {
  437. const auto toggle = [&](bool shown) {
  438. if (state->shown == shown) {
  439. return;
  440. }
  441. state->shown = shown;
  442. if (shown && !state->fade.animating()) {
  443. return;
  444. }
  445. state->fade.start([=] {
  446. widget->update();
  447. if (!state->shown
  448. && !state->fade.animating()
  449. && (!state->progress.total
  450. || (state->progress.ready
  451. == state->progress.total))) {
  452. state->animation.stop();
  453. }
  454. }, shown ? 0. : 2., shown ? 2. : 0., st::radialDuration * 2);
  455. };
  456. if (!state->shown && progress.loading) {
  457. if (!state->animation.animating()) {
  458. state->animation.start(0.);
  459. }
  460. toggle(true);
  461. } else if ((state->progress.total && !progress.total)
  462. || (state->progress.ready < state->progress.total
  463. && progress.ready == progress.total)) {
  464. state->animation.update(1., false, crl::now());
  465. toggle(false);
  466. }
  467. state->progress = progress;
  468. }, widget->lifetime());
  469. widget->paintRequest() | rpl::start_with_next([=] {
  470. const auto opacity = std::clamp(
  471. state->fade.value(state->shown ? 2. : 0.) - 1.,
  472. 0.,
  473. 1.);
  474. if (!opacity) {
  475. return;
  476. }
  477. auto p = QPainter(widget);
  478. p.setOpacity(opacity);
  479. const auto palette = _widget->titleOverridePalette();
  480. const auto color = fullscreen
  481. ? st::radialFg
  482. : palette
  483. ? palette->boxTitleCloseFg()
  484. : st::paymentsLoading.color;
  485. const auto &st = fullscreen
  486. ? st::fullScreenPanelMenu
  487. : st::separatePanelMenu;
  488. const auto size = st.rippleAreaSize;
  489. const auto rect = QRect(st.rippleAreaPosition, QSize(size, size));
  490. const auto stroke = st::botWebViewRadialStroke;
  491. const auto shift = stroke * 1.5;
  492. const auto inner = QRectF(rect).marginsRemoved(
  493. QMarginsF{ shift, shift, shift, shift });
  494. state->animation.draw(p, inner, stroke, color);
  495. }, widget->lifetime());
  496. }
  497. void Panel::requestActivate() {
  498. _widget->showAndActivate();
  499. if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
  500. InvokeQueued(widget, [=] {
  501. if (widget->isVisible()) {
  502. _webview->window.focus();
  503. }
  504. });
  505. }
  506. }
  507. void Panel::toggleProgress(bool shown) {
  508. if (!_progress) {
  509. if (!shown) {
  510. return;
  511. }
  512. _progress = std::make_unique<Progress>(
  513. _widget.get(),
  514. [=] { return progressRect(); });
  515. _progress->widget.paintRequest(
  516. ) | rpl::start_with_next([=](QRect clip) {
  517. auto p = QPainter(&_progress->widget);
  518. p.setOpacity(
  519. _progress->shownAnimation.value(_progress->shown ? 1. : 0.));
  520. const auto thickness = st::paymentsLoading.thickness;
  521. if (progressWithBackground()) {
  522. auto color = st::windowBg->c;
  523. color.setAlphaF(kProgressOpacity);
  524. p.fillRect(clip, color);
  525. }
  526. const auto rect = progressRect() - Margins(thickness);
  527. InfiniteRadialAnimation::Draw(
  528. p,
  529. _progress->animation.computeState(),
  530. rect.topLeft(),
  531. rect.size() - QSize(),
  532. _progress->widget.width(),
  533. st::paymentsLoading.color,
  534. anim::Disabled() ? (thickness / 2.) : thickness);
  535. }, _progress->widget.lifetime());
  536. _progress->widget.show();
  537. _progress->animation.start();
  538. } else if (_progress->shown == shown) {
  539. return;
  540. }
  541. const auto callback = [=] {
  542. if (!_progress->shownAnimation.animating() && !_progress->shown) {
  543. _progress = nullptr;
  544. } else {
  545. _progress->widget.update();
  546. }
  547. };
  548. _progress->shown = shown;
  549. _progress->shownAnimation.start(
  550. callback,
  551. shown ? 0. : 1.,
  552. shown ? 1. : 0.,
  553. kProgressDuration);
  554. if (shown) {
  555. setupProgressGeometry();
  556. }
  557. }
  558. bool Panel::progressWithBackground() const {
  559. return (_progress->widget.width() == _widget->innerGeometry().width());
  560. }
  561. QRect Panel::progressRect() const {
  562. const auto rect = _progress->widget.rect();
  563. if (!progressWithBackground()) {
  564. return rect;
  565. }
  566. const auto size = st::defaultBoxButton.height;
  567. return QRect(
  568. rect.x() + (rect.width() - size) / 2,
  569. rect.y() + (rect.height() - size) / 2,
  570. size,
  571. size);
  572. }
  573. void Panel::setupProgressGeometry() {
  574. if (!_progress || !_progress->shown) {
  575. return;
  576. }
  577. _progress->geometryLifetime.destroy();
  578. if (_webviewBottom) {
  579. _webviewBottom->geometryValue(
  580. ) | rpl::start_with_next([=](QRect bottom) {
  581. const auto height = bottom.height();
  582. const auto size = st::paymentsLoading.size;
  583. const auto skip = (height - size.height()) / 2;
  584. const auto inner = _widget->innerGeometry();
  585. const auto right = inner.x() + inner.width();
  586. const auto top = inner.y() + inner.height() - height;
  587. // This doesn't work, because first we get the correct bottom
  588. // geometry and after that we get the previous event (which
  589. // triggered the 'fire' of correct geometry before getting here).
  590. //const auto right = bottom.x() + bottom.width();
  591. //const auto top = bottom.y();
  592. _progress->widget.setGeometry(QRect{
  593. QPoint(right - skip - size.width(), top + skip),
  594. size });
  595. }, _progress->geometryLifetime);
  596. }
  597. _progress->widget.show();
  598. _progress->widget.raise();
  599. if (_progress->shown) {
  600. _progress->widget.setFocus();
  601. }
  602. }
  603. void Panel::showWebviewProgress() {
  604. if (_webviewProgress && _progress && _progress->shown) {
  605. return;
  606. }
  607. _webviewProgress = true;
  608. toggleProgress(true);
  609. }
  610. void Panel::hideWebviewProgress() {
  611. if (!_webviewProgress) {
  612. return;
  613. }
  614. _webviewProgress = false;
  615. toggleProgress(false);
  616. }
  617. bool Panel::showWebview(Args &&args, const Webview::ThemeParams &params) {
  618. _bottomText = std::move(args.bottom);
  619. if (!_webview && !createWebview(params)) {
  620. return false;
  621. }
  622. const auto allowBack = false;
  623. showWebviewProgress();
  624. _widget->hideLayer(anim::type::instant);
  625. updateThemeParams(params);
  626. const auto url = args.url;
  627. _webview->window.navigate(url);
  628. _widget->setBackAllowed(allowBack);
  629. rpl::duplicate(args.downloadsProgress) | rpl::start_with_next([=] {
  630. _downloadsUpdated.fire({});
  631. }, lifetime());
  632. _widget->setMenuAllowed([=](
  633. const Ui::Menu::MenuCallback &callback) {
  634. auto list = _delegate->botDownloads(true);
  635. if (!list.empty()) {
  636. auto value = rpl::single(
  637. std::move(list)
  638. ) | rpl::then(_downloadsUpdated.events(
  639. ) | rpl::map([=] {
  640. return _delegate->botDownloads();
  641. }));
  642. const auto action = [=](uint32 id, DownloadsAction type) {
  643. _delegate->botDownloadsAction(id, type);
  644. };
  645. callback(Ui::Menu::MenuCallback::Args{
  646. .text = tr::lng_downloads_section(tr::now),
  647. .icon = &st::menuIconDownload,
  648. .fillSubmenu = FillAttachBotDownloadsSubmenu(
  649. std::move(value),
  650. action),
  651. });
  652. callback({
  653. .separatorSt = &st::expandedMenuSeparator,
  654. .isSeparator = true,
  655. });
  656. }
  657. if (_webview && _webview->window.widget() && _hasSettingsButton) {
  658. callback(tr::lng_bot_settings(tr::now), [=] {
  659. postEvent("settings_button_pressed");
  660. }, &st::menuIconSettings);
  661. }
  662. if (_menuButtons & MenuButton::OpenBot) {
  663. callback(tr::lng_bot_open(tr::now), [=] {
  664. _delegate->botHandleMenuButton(MenuButton::OpenBot);
  665. }, &st::menuIconLeave);
  666. }
  667. callback(tr::lng_bot_reload_page(tr::now), [=] {
  668. if (_webview && _webview->window.widget()) {
  669. _webview->window.reload();
  670. } else if (const auto params = _delegate->botThemeParams()
  671. ; createWebview(params)) {
  672. showWebviewProgress();
  673. updateThemeParams(params);
  674. _webview->window.navigate(url);
  675. }
  676. }, &st::menuIconRestore);
  677. if (_menuButtons & MenuButton::ShareGame) {
  678. callback(tr::lng_iv_share(tr::now), [=] {
  679. _delegate->botHandleMenuButton(MenuButton::ShareGame);
  680. }, &st::menuIconShare);
  681. } else {
  682. callback(tr::lng_bot_terms(tr::now), [=] {
  683. File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));
  684. }, &st::menuIconGroupLog);
  685. callback(tr::lng_bot_privacy(tr::now), [=] {
  686. _delegate->botOpenPrivacyPolicy();
  687. }, &st::menuIconAntispam);
  688. }
  689. const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu);
  690. if (main || (_menuButtons & MenuButton::RemoveFromMenu)) {
  691. const auto handler = [=] {
  692. _delegate->botHandleMenuButton(main
  693. ? MenuButton::RemoveFromMainMenu
  694. : MenuButton::RemoveFromMenu);
  695. };
  696. callback({
  697. .text = (main
  698. ? tr::lng_bot_remove_from_side_menu
  699. : tr::lng_bot_remove_from_menu)(tr::now),
  700. .handler = handler,
  701. .icon = &st::menuIconDeleteAttention,
  702. .isAttention = true,
  703. });
  704. }
  705. }, [=, progress = std::move(args.downloadsProgress)](
  706. not_null<RpWidget*> button,
  707. bool fullscreen) {
  708. setupDownloadsProgress(
  709. button,
  710. rpl::duplicate(progress),
  711. fullscreen);
  712. });
  713. return true;
  714. }
  715. void Panel::createWebviewBottom() {
  716. _webviewBottom = std::make_unique<RpWidget>(_widget.get());
  717. const auto bottom = _webviewBottom.get();
  718. bottom->setVisible(!_fullscreen.current());
  719. const auto &padding = st::paymentsPanelPadding;
  720. const auto label = CreateChild<FlatLabel>(
  721. _webviewBottom.get(),
  722. _bottomText.value(),
  723. st::paymentsWebviewBottom);
  724. _webviewBottomLabel = label;
  725. const auto height = padding.top()
  726. + label->heightNoMargins()
  727. + padding.bottom();
  728. rpl::combine(
  729. _webviewBottom->widthValue(),
  730. label->widthValue()
  731. ) | rpl::start_with_next([=](int outerWidth, int width) {
  732. label->move((outerWidth - width) / 2, padding.top());
  733. }, label->lifetime());
  734. label->show();
  735. _webviewBottom->resize(_webviewBottom->width(), height);
  736. rpl::combine(
  737. _webviewParent->geometryValue() | rpl::map([=] {
  738. return _widget->innerGeometry();
  739. }),
  740. bottom->heightValue()
  741. ) | rpl::start_with_next([=](QRect inner, int height) {
  742. bottom->move(inner.x(), inner.y() + inner.height() - height);
  743. bottom->resizeToWidth(inner.width());
  744. layoutButtons();
  745. }, bottom->lifetime());
  746. }
  747. bool Panel::createWebview(const Webview::ThemeParams &params) {
  748. auto outer = base::make_unique_q<RpWidget>(_widget.get());
  749. const auto container = outer.get();
  750. _widget->showInner(std::move(outer));
  751. _webviewParent = container;
  752. _headerColorReceived = false;
  753. _bodyColorReceived = false;
  754. _bottomColorReceived = false;
  755. updateColorOverrides(params);
  756. createWebviewBottom();
  757. container->show();
  758. _webview = std::make_unique<WebviewWithLifetime>(
  759. container,
  760. Webview::WindowConfig{
  761. .opaqueBg = params.bodyBg,
  762. .storageId = _storageId,
  763. });
  764. const auto raw = &_webview->window;
  765. const auto bottom = _webviewBottom.get();
  766. QObject::connect(container, &QObject::destroyed, [=] {
  767. if (_webview && &_webview->window == raw) {
  768. base::take(_webview);
  769. if (_webviewProgress) {
  770. hideWebviewProgress();
  771. if (_progress && !_progress->shown) {
  772. _progress = nullptr;
  773. }
  774. }
  775. }
  776. if (_webviewBottom.get() == bottom) {
  777. _webviewBottomLabel = nullptr;
  778. _webviewBottom = nullptr;
  779. _secondaryButton = nullptr;
  780. _mainButton = nullptr;
  781. _bottomButtonsBg = nullptr;
  782. }
  783. });
  784. if (!raw->widget()) {
  785. return false;
  786. }
  787. #if !defined Q_OS_WIN && !defined Q_OS_MAC
  788. _widget->allowChildFullScreenControls(
  789. !raw->widget()->inherits("QWindowContainer"));
  790. #endif // !Q_OS_WIN && !Q_OS_MAC
  791. QObject::connect(raw->widget(), &QObject::destroyed, [=] {
  792. const auto parent = _webviewParent.data();
  793. if (!_webview
  794. || &_webview->window != raw
  795. || !parent
  796. || _widget->inner() != parent) {
  797. // If we destroyed _webview ourselves,
  798. // or if we changed _widget->inner ourselves,
  799. // we don't show any message, nothing crashed.
  800. return;
  801. }
  802. crl::on_main(this, [=] {
  803. showCriticalError({ "Error: WebView has crashed." });
  804. });
  805. });
  806. rpl::combine(
  807. container->geometryValue(),
  808. _footerHeight.value()
  809. ) | rpl::start_with_next([=](QRect geometry, int footer) {
  810. if (const auto view = raw->widget()) {
  811. view->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer }));
  812. crl::on_main(view, [=] {
  813. sendViewport();
  814. InvokeQueued(view, [=] { sendViewport(); });
  815. });
  816. }
  817. }, _webview->lifetime);
  818. raw->setMessageHandler([=](const QJsonDocument &message) {
  819. if (!message.isArray()) {
  820. LOG(("BotWebView Error: "
  821. "Not an array received in buy_callback arguments."));
  822. return;
  823. }
  824. const auto list = message.array();
  825. const auto command = list.at(0).toString();
  826. const auto arguments = ParseMethodArgs(list.at(1).toString());
  827. if (command == "web_app_close") {
  828. _delegate->botClose();
  829. } else if (command == "web_app_data_send") {
  830. sendDataMessage(arguments);
  831. } else if (command == "web_app_switch_inline_query") {
  832. switchInlineQueryMessage(arguments);
  833. } else if (command == "web_app_setup_main_button") {
  834. processButtonMessage(_mainButton, arguments);
  835. } else if (command == "web_app_setup_secondary_button") {
  836. processButtonMessage(_secondaryButton, arguments);
  837. } else if (command == "web_app_setup_back_button") {
  838. processBackButtonMessage(arguments);
  839. } else if (command == "web_app_setup_settings_button") {
  840. processSettingsButtonMessage(arguments);
  841. } else if (command == "web_app_request_theme") {
  842. _themeUpdateForced.fire({});
  843. } else if (command == "web_app_request_viewport") {
  844. sendViewport();
  845. } else if (command == "web_app_request_safe_area") {
  846. sendSafeArea();
  847. } else if (command == "web_app_request_content_safe_area") {
  848. sendContentSafeArea();
  849. } else if (command == "web_app_request_fullscreen") {
  850. if (!_fullscreen.current()) {
  851. _fullscreen = true;
  852. } else {
  853. sendFullScreen();
  854. }
  855. } else if (command == "web_app_request_file_download") {
  856. processDownloadRequest(arguments);
  857. } else if (command == "web_app_exit_fullscreen") {
  858. if (_fullscreen.current()) {
  859. _fullscreen = false;
  860. } else {
  861. sendFullScreen();
  862. }
  863. } else if (command == "web_app_check_home_screen") {
  864. postEvent("home_screen_checked", "{ status: \"unsupported\" }");
  865. } else if (command == "web_app_start_accelerometer") {
  866. postEvent("accelerometer_failed", "{ error: \"UNSUPPORTED\" }");
  867. } else if (command == "web_app_start_device_orientation") {
  868. postEvent(
  869. "device_orientation_failed",
  870. "{ error: \"UNSUPPORTED\" }");
  871. } else if (command == "web_app_start_gyroscope") {
  872. postEvent("gyroscope_failed", "{ error: \"UNSUPPORTED\" }");
  873. } else if (command == "web_app_check_location") {
  874. postEvent("location_checked", "{ available: false }");
  875. } else if (command == "web_app_request_location") {
  876. postEvent("location_requested", "{ available: false }");
  877. } else if (command == "web_app_biometry_get_info") {
  878. postEvent("biometry_info_received", "{ available: false }");
  879. } else if (command == "web_app_open_tg_link") {
  880. openTgLink(arguments);
  881. } else if (command == "web_app_open_link") {
  882. openExternalLink(arguments);
  883. } else if (command == "web_app_open_invoice") {
  884. openInvoice(arguments);
  885. } else if (command == "web_app_open_popup") {
  886. openPopup(arguments);
  887. } else if (command == "web_app_open_scan_qr_popup") {
  888. openScanQrPopup(arguments);
  889. } else if (command == "web_app_share_to_story") {
  890. openShareStory(arguments);
  891. } else if (command == "web_app_request_write_access") {
  892. requestWriteAccess();
  893. } else if (command == "web_app_request_phone") {
  894. requestPhone();
  895. } else if (command == "web_app_invoke_custom_method") {
  896. invokeCustomMethod(arguments);
  897. } else if (command == "web_app_setup_closing_behavior") {
  898. setupClosingBehaviour(arguments);
  899. } else if (command == "web_app_read_text_from_clipboard") {
  900. requestClipboardText(arguments);
  901. } else if (command == "web_app_set_header_color") {
  902. processHeaderColor(arguments);
  903. } else if (command == "web_app_set_background_color") {
  904. processBackgroundColor(arguments);
  905. } else if (command == "web_app_set_bottom_bar_color") {
  906. processBottomBarColor(arguments);
  907. } else if (command == "web_app_send_prepared_message") {
  908. processSendMessageRequest(arguments);
  909. } else if (command == "web_app_set_emoji_status") {
  910. processEmojiStatusRequest(arguments);
  911. } else if (command == "web_app_request_emoji_status_access") {
  912. processEmojiStatusAccessRequest();
  913. } else if (command == "share_score") {
  914. _delegate->botHandleMenuButton(MenuButton::ShareGame);
  915. }
  916. });
  917. raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
  918. if (_delegate->botHandleLocalUri(uri, false)) {
  919. return false;
  920. } else if (newWindow) {
  921. return true;
  922. }
  923. showWebviewProgress();
  924. return true;
  925. });
  926. raw->setNavigationDoneHandler([=](bool success) {
  927. hideWebviewProgress();
  928. });
  929. raw->init(R"(
  930. window.TelegramWebviewProxy = {
  931. postEvent: function(eventType, eventData) {
  932. if (window.external && window.external.invoke) {
  933. window.external.invoke(JSON.stringify([eventType, eventData]));
  934. }
  935. }
  936. };)");
  937. if (!_webview) {
  938. return false;
  939. }
  940. layoutButtons();
  941. setupProgressGeometry();
  942. base::qt_signal_producer(
  943. qApp,
  944. &QGuiApplication::focusWindowChanged
  945. ) | rpl::filter([=](QWindow *focused) {
  946. const auto handle = _widget->window()->windowHandle();
  947. const auto widget = _webview ? _webview->window.widget() : nullptr;
  948. return widget
  949. && !widget->isHidden()
  950. && handle
  951. && (focused == handle);
  952. }) | rpl::start_with_next([=] {
  953. _webview->window.focus();
  954. }, _webview->lifetime);
  955. return true;
  956. }
  957. void Panel::sendViewport() {
  958. postEvent("viewport_changed", "{ "
  959. "height: window.innerHeight, "
  960. "is_state_stable: true, "
  961. "is_expanded: true }");
  962. }
  963. void Panel::sendFullScreen() {
  964. postEvent("fullscreen_changed", _fullscreen.current()
  965. ? "{ is_fullscreen: true }"
  966. : "{ is_fullscreen: false }");
  967. }
  968. void Panel::sendSafeArea() {
  969. postEvent("safe_area_changed",
  970. "{ top: 0, right: 0, bottom: 0, left: 0 }");
  971. }
  972. void Panel::sendContentSafeArea() {
  973. const auto shift = st::separatePanelClose.rippleAreaPosition.y();
  974. const auto top = _fullscreen.current()
  975. ? (shift + st::fullScreenPanelClose.height + (shift / 2))
  976. : 0;
  977. const auto scaled = top * style::DevicePixelRatio();
  978. auto report = 0;
  979. if (const auto screen = QGuiApplication::primaryScreen()) {
  980. const auto dpi = screen->logicalDotsPerInch();
  981. const auto ratio = screen->devicePixelRatio();
  982. const auto basePair = screen->handle()->logicalBaseDpi();
  983. const auto base = (basePair.first + basePair.second) * 0.5;
  984. const auto systemScreenScale = dpi * ratio / base;
  985. report = int(base::SafeRound(scaled / systemScreenScale));
  986. }
  987. postEvent("content_safe_area_changed",
  988. u"{ top: %1, right: 0, bottom: 0, left: 0 }"_q.arg(report));
  989. }
  990. void Panel::setTitle(rpl::producer<QString> title) {
  991. _widget->setTitle(std::move(title));
  992. }
  993. void Panel::sendDataMessage(const QJsonObject &args) {
  994. if (args.isEmpty()) {
  995. _delegate->botClose();
  996. return;
  997. }
  998. const auto data = args["data"].toString();
  999. if (data.isEmpty()) {
  1000. LOG(("BotWebView Error: Bad 'data' in sendDataMessage."));
  1001. _delegate->botClose();
  1002. return;
  1003. }
  1004. _delegate->botSendData(data.toUtf8());
  1005. }
  1006. void Panel::switchInlineQueryMessage(const QJsonObject &args) {
  1007. if (args.isEmpty()) {
  1008. _delegate->botClose();
  1009. return;
  1010. }
  1011. const auto query = args["query"].toString();
  1012. if (query.isEmpty()) {
  1013. LOG(("BotWebView Error: Bad 'query' in switchInlineQueryMessage."));
  1014. _delegate->botClose();
  1015. return;
  1016. }
  1017. const auto valid = base::flat_set<QString>{
  1018. u"users"_q,
  1019. u"bots"_q,
  1020. u"groups"_q,
  1021. u"channels"_q,
  1022. };
  1023. const auto typeArray = args["chat_types"].toArray();
  1024. auto types = std::vector<QString>();
  1025. for (const auto &value : typeArray) {
  1026. const auto type = value.toString();
  1027. if (valid.contains(type)) {
  1028. types.push_back(type);
  1029. } else {
  1030. LOG(("BotWebView Error: "
  1031. "Bad chat type in switchInlineQueryMessage: %1.").arg(type));
  1032. types.clear();
  1033. break;
  1034. }
  1035. }
  1036. _delegate->botSwitchInlineQuery(types, query);
  1037. }
  1038. void Panel::processSendMessageRequest(const QJsonObject &args) {
  1039. if (args.isEmpty()) {
  1040. _delegate->botClose();
  1041. return;
  1042. }
  1043. const auto id = args["id"].toString();
  1044. auto callback = crl::guard(this, [=](QString error) {
  1045. if (error.isEmpty()) {
  1046. postEvent("prepared_message_sent");
  1047. } else {
  1048. postEvent(
  1049. "prepared_message_failed",
  1050. u"{ error: \"%1\" }"_q.arg(error));
  1051. }
  1052. });
  1053. _delegate->botSendPreparedMessage({
  1054. .id = id,
  1055. .callback = std::move(callback),
  1056. });
  1057. }
  1058. void Panel::processEmojiStatusRequest(const QJsonObject &args) {
  1059. if (args.isEmpty()) {
  1060. _delegate->botClose();
  1061. return;
  1062. }
  1063. const auto emojiId = args["custom_emoji_id"].toString().toULongLong();
  1064. const auto duration = TimeId(base::SafeRound(
  1065. args["duration"].toDouble()));
  1066. if (!emojiId) {
  1067. postEvent(
  1068. "emoji_status_failed",
  1069. "{ error: \"SUGGESTED_EMOJI_INVALID\" }");
  1070. return;
  1071. } else if (duration < 0) {
  1072. postEvent(
  1073. "emoji_status_failed",
  1074. "{ error: \"DURATION_INVALID\" }");
  1075. return;
  1076. }
  1077. auto callback = crl::guard(this, [=](QString error) {
  1078. if (error.isEmpty()) {
  1079. postEvent("emoji_status_set");
  1080. } else {
  1081. postEvent(
  1082. "emoji_status_failed",
  1083. u"{ error: \"%1\" }"_q.arg(error));
  1084. }
  1085. });
  1086. _delegate->botSetEmojiStatus({
  1087. .customEmojiId = emojiId,
  1088. .duration = duration,
  1089. .callback = std::move(callback),
  1090. });
  1091. }
  1092. void Panel::processEmojiStatusAccessRequest() {
  1093. auto callback = crl::guard(this, [=](bool allowed) {
  1094. postEvent("emoji_status_access_requested", allowed
  1095. ? "{ status: \"allowed\" }"
  1096. : "{ status: \"cancelled\" }");
  1097. });
  1098. _delegate->botRequestEmojiStatusAccess(std::move(callback));
  1099. }
  1100. void Panel::openTgLink(const QJsonObject &args) {
  1101. if (args.isEmpty()) {
  1102. LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'."));
  1103. _delegate->botClose();
  1104. return;
  1105. }
  1106. const auto path = args["path_full"].toString();
  1107. if (path.isEmpty()) {
  1108. LOG(("BotWebView Error: Bad 'path_full' in 'web_app_open_tg_link'."));
  1109. _delegate->botClose();
  1110. return;
  1111. }
  1112. _delegate->botHandleLocalUri("https://t.me" + path, true);
  1113. }
  1114. void Panel::openExternalLink(const QJsonObject &args) {
  1115. if (args.isEmpty()) {
  1116. _delegate->botClose();
  1117. return;
  1118. }
  1119. const auto iv = args["try_instant_view"].toBool();
  1120. const auto url = args["url"].toString();
  1121. if (!_delegate->botValidateExternalLink(url)) {
  1122. LOG(("BotWebView Error: Bad url in openExternalLink: %1").arg(url));
  1123. _delegate->botClose();
  1124. return;
  1125. } else if (!allowOpenLink()) {
  1126. return;
  1127. } else if (iv) {
  1128. _delegate->botOpenIvLink(url);
  1129. } else {
  1130. File::OpenUrl(url);
  1131. }
  1132. }
  1133. void Panel::openInvoice(const QJsonObject &args) {
  1134. if (args.isEmpty()) {
  1135. _delegate->botClose();
  1136. return;
  1137. }
  1138. const auto slug = args["slug"].toString();
  1139. if (slug.isEmpty()) {
  1140. LOG(("BotWebView Error: Bad 'slug' in openInvoice."));
  1141. _delegate->botClose();
  1142. return;
  1143. }
  1144. _delegate->botHandleInvoice(slug);
  1145. }
  1146. void Panel::openPopup(const QJsonObject &args) {
  1147. if (args.isEmpty()) {
  1148. _delegate->botClose();
  1149. return;
  1150. }
  1151. using Button = Webview::PopupArgs::Button;
  1152. using Type = Button::Type;
  1153. const auto message = args["message"].toString();
  1154. const auto types = base::flat_map<QString, Button::Type>{
  1155. { "default", Type::Default },
  1156. { "ok", Type::Ok },
  1157. { "close", Type::Close },
  1158. { "cancel", Type::Cancel },
  1159. { "destructive", Type::Destructive },
  1160. };
  1161. const auto buttonArray = args["buttons"].toArray();
  1162. auto buttons = std::vector<Webview::PopupArgs::Button>();
  1163. for (const auto button : buttonArray) {
  1164. const auto fields = button.toObject();
  1165. const auto i = types.find(fields["type"].toString());
  1166. if (i == end(types)) {
  1167. LOG(("BotWebView Error: Bad 'type' in openPopup buttons."));
  1168. _delegate->botClose();
  1169. return;
  1170. }
  1171. buttons.push_back({
  1172. .id = fields["id"].toString(),
  1173. .text = fields["text"].toString(),
  1174. .type = i->second,
  1175. });
  1176. }
  1177. if (message.isEmpty()) {
  1178. LOG(("BotWebView Error: Bad 'message' in openPopup."));
  1179. _delegate->botClose();
  1180. return;
  1181. } else if (buttons.empty()) {
  1182. LOG(("BotWebView Error: Bad 'buttons' in openPopup."));
  1183. _delegate->botClose();
  1184. return;
  1185. }
  1186. const auto widget = _webview->window.widget();
  1187. const auto weak = base::make_weak(this);
  1188. const auto result = Webview::ShowBlockingPopup({
  1189. .parent = widget ? widget->window() : nullptr,
  1190. .title = args["title"].toString(),
  1191. .text = message,
  1192. .buttons = std::move(buttons),
  1193. });
  1194. if (weak) {
  1195. postEvent("popup_closed", result.id
  1196. ? QJsonObject{ { u"button_id"_q, *result.id } }
  1197. : EventData());
  1198. }
  1199. }
  1200. void Panel::openScanQrPopup(const QJsonObject &args) {
  1201. const auto widget = _webview->window.widget();
  1202. [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({
  1203. .parent = widget ? widget->window() : nullptr,
  1204. .text = tr::lng_bot_no_scan_qr(tr::now),
  1205. .buttons = { {
  1206. .id = "ok",
  1207. .text = tr::lng_box_ok(tr::now),
  1208. .type = Webview::PopupArgs::Button::Type::Ok,
  1209. }},
  1210. });
  1211. }
  1212. void Panel::openShareStory(const QJsonObject &args) {
  1213. const auto widget = _webview->window.widget();
  1214. [[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({
  1215. .parent = widget ? widget->window() : nullptr,
  1216. .text = tr::lng_bot_no_share_story(tr::now),
  1217. .buttons = { {
  1218. .id = "ok",
  1219. .text = tr::lng_box_ok(tr::now),
  1220. .type = Webview::PopupArgs::Button::Type::Ok,
  1221. }},
  1222. });
  1223. }
  1224. void Panel::requestWriteAccess() {
  1225. if (_inBlockingRequest) {
  1226. replyRequestWriteAccess(false);
  1227. return;
  1228. }
  1229. _inBlockingRequest = true;
  1230. const auto finish = [=](bool allowed) {
  1231. _inBlockingRequest = false;
  1232. replyRequestWriteAccess(allowed);
  1233. };
  1234. const auto weak = base::make_weak(this);
  1235. _delegate->botCheckWriteAccess([=](bool allowed) {
  1236. if (!weak) {
  1237. return;
  1238. } else if (allowed) {
  1239. finish(true);
  1240. return;
  1241. }
  1242. using Button = Webview::PopupArgs::Button;
  1243. const auto widget = _webview->window.widget();
  1244. const auto integration = &Ui::Integration::Instance();
  1245. const auto result = Webview::ShowBlockingPopup({
  1246. .parent = widget ? widget->window() : nullptr,
  1247. .title = integration->phraseBotAllowWriteTitle(),
  1248. .text = integration->phraseBotAllowWrite(),
  1249. .buttons = {
  1250. {
  1251. .id = "allow",
  1252. .text = integration->phraseBotAllowWriteConfirm(),
  1253. },
  1254. { .id = "cancel", .type = Button::Type::Cancel },
  1255. },
  1256. });
  1257. if (!weak) {
  1258. return;
  1259. } else if (result.id == "allow") {
  1260. _delegate->botAllowWriteAccess(crl::guard(this, finish));
  1261. } else {
  1262. finish(false);
  1263. }
  1264. });
  1265. }
  1266. void Panel::replyRequestWriteAccess(bool allowed) {
  1267. postEvent("write_access_requested", QJsonObject{
  1268. { u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q }
  1269. });
  1270. }
  1271. void Panel::requestPhone() {
  1272. if (_inBlockingRequest) {
  1273. replyRequestPhone(false);
  1274. return;
  1275. }
  1276. _inBlockingRequest = true;
  1277. const auto finish = [=](bool shared) {
  1278. _inBlockingRequest = false;
  1279. replyRequestPhone(shared);
  1280. };
  1281. using Button = Webview::PopupArgs::Button;
  1282. const auto widget = _webview->window.widget();
  1283. const auto weak = base::make_weak(this);
  1284. const auto integration = &Ui::Integration::Instance();
  1285. const auto result = Webview::ShowBlockingPopup({
  1286. .parent = widget ? widget->window() : nullptr,
  1287. .title = integration->phraseBotSharePhoneTitle(),
  1288. .text = integration->phraseBotSharePhone(),
  1289. .buttons = {
  1290. {
  1291. .id = "share",
  1292. .text = integration->phraseBotSharePhoneConfirm(),
  1293. },
  1294. { .id = "cancel", .type = Button::Type::Cancel },
  1295. },
  1296. });
  1297. if (!weak) {
  1298. return;
  1299. } else if (result.id == "share") {
  1300. _delegate->botSharePhone(crl::guard(this, finish));
  1301. } else {
  1302. finish(false);
  1303. }
  1304. }
  1305. void Panel::replyRequestPhone(bool shared) {
  1306. postEvent("phone_requested", QJsonObject{
  1307. { u"status"_q, shared ? u"sent"_q : u"cancelled"_q }
  1308. });
  1309. }
  1310. void Panel::invokeCustomMethod(const QJsonObject &args) {
  1311. const auto requestId = args["req_id"];
  1312. if (requestId.isUndefined()) {
  1313. return;
  1314. }
  1315. const auto finish = [=](QJsonObject response) {
  1316. replyCustomMethod(requestId, std::move(response));
  1317. };
  1318. auto callback = crl::guard(this, [=](CustomMethodResult result) {
  1319. if (result) {
  1320. auto error = QJsonParseError();
  1321. const auto parsed = QJsonDocument::fromJson(
  1322. "{ \"result\": " + *result + '}',
  1323. &error);
  1324. if (error.error != QJsonParseError::NoError
  1325. || !parsed.isObject()
  1326. || parsed.object().size() != 1) {
  1327. finish({ { u"error"_q, u"Could not parse response."_q } });
  1328. } else {
  1329. finish(parsed.object());
  1330. }
  1331. } else {
  1332. finish({ { u"error"_q, result.error() } });
  1333. }
  1334. });
  1335. const auto params = QJsonDocument(
  1336. args["params"].toObject()
  1337. ).toJson(QJsonDocument::Compact);
  1338. _delegate->botInvokeCustomMethod({
  1339. .method = args["method"].toString(),
  1340. .params = params,
  1341. .callback = std::move(callback),
  1342. });
  1343. }
  1344. void Panel::replyCustomMethod(QJsonValue requestId, QJsonObject response) {
  1345. response["req_id"] = requestId;
  1346. postEvent(u"custom_method_invoked"_q, response);
  1347. }
  1348. void Panel::requestClipboardText(const QJsonObject &args) {
  1349. const auto requestId = args["req_id"];
  1350. if (requestId.isUndefined()) {
  1351. return;
  1352. }
  1353. auto result = QJsonObject();
  1354. result["req_id"] = requestId;
  1355. if (allowClipboardQuery()) {
  1356. result["data"] = QGuiApplication::clipboard()->text();
  1357. }
  1358. postEvent(u"clipboard_text_received"_q, result);
  1359. }
  1360. bool Panel::allowOpenLink() const {
  1361. //const auto now = crl::now();
  1362. //if (_mainButtonLastClick
  1363. // && _mainButtonLastClick + kProcessClickTimeout >= now) {
  1364. // _mainButtonLastClick = 0;
  1365. // return true;
  1366. //}
  1367. return true;
  1368. }
  1369. bool Panel::allowClipboardQuery() const {
  1370. if (!_allowClipboardRead) {
  1371. return false;
  1372. }
  1373. //const auto now = crl::now();
  1374. //if (_mainButtonLastClick
  1375. // && _mainButtonLastClick + kProcessClickTimeout >= now) {
  1376. // _mainButtonLastClick = 0;
  1377. // return true;
  1378. //}
  1379. return true;
  1380. }
  1381. void Panel::scheduleCloseWithConfirmation() {
  1382. if (!_closeWithConfirmationScheduled) {
  1383. _closeWithConfirmationScheduled = true;
  1384. InvokeQueued(_widget.get(), [=] { closeWithConfirmation(); });
  1385. }
  1386. }
  1387. void Panel::closeWithConfirmation() {
  1388. using Button = Webview::PopupArgs::Button;
  1389. const auto widget = _webview->window.widget();
  1390. const auto weak = base::make_weak(this);
  1391. const auto integration = &Ui::Integration::Instance();
  1392. const auto result = Webview::ShowBlockingPopup({
  1393. .parent = widget ? widget->window() : nullptr,
  1394. .title = integration->phrasePanelCloseWarning(),
  1395. .text = integration->phrasePanelCloseUnsaved(),
  1396. .buttons = {
  1397. {
  1398. .id = "close",
  1399. .text = integration->phrasePanelCloseAnyway(),
  1400. .type = Button::Type::Destructive,
  1401. },
  1402. { .id = "cancel", .type = Button::Type::Cancel },
  1403. },
  1404. .ignoreFloodCheck = true,
  1405. });
  1406. if (!weak) {
  1407. return;
  1408. } else if (result.id == "close") {
  1409. _delegate->botClose();
  1410. } else {
  1411. _closeWithConfirmationScheduled = false;
  1412. }
  1413. }
  1414. void Panel::setupClosingBehaviour(const QJsonObject &args) {
  1415. _closeNeedConfirmation = args["need_confirmation"].toBool();
  1416. }
  1417. void Panel::processButtonMessage(
  1418. std::unique_ptr<Button> &button,
  1419. const QJsonObject &args) {
  1420. if (args.isEmpty()) {
  1421. _delegate->botClose();
  1422. return;
  1423. }
  1424. const auto shown = [&] {
  1425. return button && !button->isHidden();
  1426. };
  1427. const auto wasShown = shown();
  1428. const auto guard = gsl::finally([&] {
  1429. if (shown() != wasShown) {
  1430. crl::on_main(this, [=] {
  1431. sendViewport();
  1432. });
  1433. }
  1434. });
  1435. const auto text = args["text"].toString().trimmed();
  1436. const auto visible = args["is_visible"].toBool() && !text.isEmpty();
  1437. if (!button) {
  1438. if (visible) {
  1439. createButton(button);
  1440. _bottomButtonsBg->show();
  1441. } else {
  1442. return;
  1443. }
  1444. }
  1445. if (const auto bg = ParseColor(args["color"].toString())) {
  1446. button->updateBg(*bg);
  1447. } else {
  1448. button->updateBg(&st::windowBgActive);
  1449. }
  1450. if (const auto fg = ParseColor(args["text_color"].toString())) {
  1451. button->updateFg(*fg);
  1452. } else {
  1453. button->updateFg(&st::windowFgActive);
  1454. }
  1455. button->updateArgs({
  1456. .isActive = args["is_active"].toBool(),
  1457. .isVisible = visible,
  1458. .isProgressVisible = args["is_progress_visible"].toBool(),
  1459. .text = args["text"].toString(),
  1460. });
  1461. if (button.get() == _secondaryButton.get()) {
  1462. const auto position = ParsePosition(args["position"].toString());
  1463. if (_secondaryPosition != position) {
  1464. _secondaryPosition = position;
  1465. layoutButtons();
  1466. }
  1467. }
  1468. }
  1469. void Panel::processBackButtonMessage(const QJsonObject &args) {
  1470. _widget->setBackAllowed(args["is_visible"].toBool());
  1471. }
  1472. void Panel::processSettingsButtonMessage(const QJsonObject &args) {
  1473. _hasSettingsButton = args["is_visible"].toBool();
  1474. }
  1475. void Panel::processHeaderColor(const QJsonObject &args) {
  1476. _headerColorReceived = true;
  1477. if (const auto color = ParseColor(args["color"].toString())) {
  1478. _widget->overrideTitleColor(color);
  1479. _headerColorLifetime.destroy();
  1480. } else if (const auto color = LookupNamedColor(
  1481. args["color_key"].toString())) {
  1482. _widget->overrideTitleColor((*color)->c);
  1483. _headerColorLifetime = style::PaletteChanged(
  1484. ) | rpl::start_with_next([=] {
  1485. _widget->overrideTitleColor((*color)->c);
  1486. });
  1487. } else {
  1488. _widget->overrideTitleColor(std::nullopt);
  1489. _headerColorLifetime.destroy();
  1490. }
  1491. }
  1492. void Panel::overrideBodyColor(std::optional<QColor> color) {
  1493. _widget->overrideBodyColor(color);
  1494. const auto raw = _webviewBottomLabel.data();
  1495. if (!raw) {
  1496. return;
  1497. } else if (!color) {
  1498. raw->setTextColorOverride(std::nullopt);
  1499. return;
  1500. }
  1501. const auto contrast = 2.5;
  1502. const auto luminance = 0.2126 * color->redF()
  1503. + 0.7152 * color->greenF()
  1504. + 0.0722 * color->blueF();
  1505. const auto textColor = (luminance > 0.5)
  1506. ? QColor(0, 0, 0)
  1507. : QColor(255, 255, 255);
  1508. const auto textLuminance = (luminance > 0.5) ? 0 : 1;
  1509. const auto adaptiveOpacity = (luminance - textLuminance + contrast)
  1510. / contrast;
  1511. const auto opacity = std::clamp(adaptiveOpacity, 0.5, 0.64);
  1512. auto buttonColor = textColor;
  1513. buttonColor.setAlphaF(opacity);
  1514. raw->setTextColorOverride(buttonColor);
  1515. }
  1516. void Panel::processBackgroundColor(const QJsonObject &args) {
  1517. _bodyColorReceived = true;
  1518. if (const auto color = ParseColor(args["color"].toString())) {
  1519. overrideBodyColor(*color);
  1520. _bodyColorLifetime.destroy();
  1521. } else if (const auto color = LookupNamedColor(
  1522. args["color_key"].toString())) {
  1523. overrideBodyColor((*color)->c);
  1524. _bodyColorLifetime = style::PaletteChanged(
  1525. ) | rpl::start_with_next([=] {
  1526. overrideBodyColor((*color)->c);
  1527. });
  1528. } else {
  1529. overrideBodyColor(std::nullopt);
  1530. _bodyColorLifetime.destroy();
  1531. }
  1532. if (const auto raw = _bottomButtonsBg.get()) {
  1533. raw->update();
  1534. }
  1535. if (const auto raw = _webviewBottom.get()) {
  1536. raw->update();
  1537. }
  1538. }
  1539. void Panel::processBottomBarColor(const QJsonObject &args) {
  1540. _bottomColorReceived = true;
  1541. if (const auto color = ParseColor(args["color"].toString())) {
  1542. _widget->overrideBottomBarColor(color);
  1543. _bottomBarColor = color;
  1544. _bottomBarColorLifetime.destroy();
  1545. } else if (const auto color = LookupNamedColor(
  1546. args["color_key"].toString())) {
  1547. _widget->overrideBottomBarColor((*color)->c);
  1548. _bottomBarColor = (*color)->c;
  1549. _bottomBarColorLifetime = style::PaletteChanged(
  1550. ) | rpl::start_with_next([=] {
  1551. _widget->overrideBottomBarColor((*color)->c);
  1552. _bottomBarColor = (*color)->c;
  1553. });
  1554. } else {
  1555. _widget->overrideBottomBarColor(std::nullopt);
  1556. _bottomBarColor = std::nullopt;
  1557. _bottomBarColorLifetime.destroy();
  1558. }
  1559. if (const auto raw = _bottomButtonsBg.get()) {
  1560. raw->update();
  1561. }
  1562. }
  1563. void Panel::processDownloadRequest(const QJsonObject &args) {
  1564. if (args.isEmpty()) {
  1565. _delegate->botClose();
  1566. return;
  1567. }
  1568. const auto url = args["url"].toString();
  1569. const auto name = args["file_name"].toString();
  1570. if (url.isEmpty()) {
  1571. LOG(("BotWebView Error: Bad 'url' in download request."));
  1572. _delegate->botClose();
  1573. return;
  1574. } else if (name.isEmpty()) {
  1575. LOG(("BotWebView Error: Bad 'file_name' in download request."));
  1576. _delegate->botClose();
  1577. return;
  1578. }
  1579. const auto done = crl::guard(this, [=](bool started) {
  1580. postEvent("file_download_requested", started
  1581. ? "{ status: \"downloading\" }"
  1582. : "{ status: \"cancelled\" }");
  1583. });
  1584. _delegate->botDownloadFile({
  1585. .url = url,
  1586. .name = name,
  1587. .callback = done,
  1588. });
  1589. }
  1590. void Panel::createButton(std::unique_ptr<Button> &button) {
  1591. if (!_bottomButtonsBg) {
  1592. _bottomButtonsBg = std::make_unique<RpWidget>(_widget.get());
  1593. const auto raw = _bottomButtonsBg.get();
  1594. raw->paintRequest() | rpl::start_with_next([=] {
  1595. auto p = QPainter(raw);
  1596. auto hq = PainterHighQualityEnabler(p);
  1597. p.setPen(Qt::NoPen);
  1598. p.setBrush(_bottomBarColor.value_or(st::windowBg->c));
  1599. p.drawRoundedRect(
  1600. raw->rect().marginsAdded({ 0, 2 * st::callRadius, 0, 0 }),
  1601. st::callRadius,
  1602. st::callRadius);
  1603. }, raw->lifetime());
  1604. }
  1605. button = std::make_unique<Button>(
  1606. _bottomButtonsBg.get(),
  1607. st::botWebViewBottomButton);
  1608. const auto raw = button.get();
  1609. raw->setClickedCallback([=] {
  1610. if (!raw->isDisabled()) {
  1611. if (raw == _mainButton.get()) {
  1612. postEvent("main_button_pressed");
  1613. } else if (raw == _secondaryButton.get()) {
  1614. postEvent("secondary_button_pressed");
  1615. }
  1616. }
  1617. });
  1618. raw->hide();
  1619. rpl::combine(
  1620. raw->shownValue(),
  1621. raw->heightValue()
  1622. ) | rpl::start_with_next([=] {
  1623. layoutButtons();
  1624. }, raw->lifetime());
  1625. }
  1626. void Panel::layoutButtons() {
  1627. if (!_webviewBottom) {
  1628. return;
  1629. }
  1630. const auto inner = _widget->innerGeometry();
  1631. const auto shown = [](std::unique_ptr<Button> &button) {
  1632. return button && !button->isHidden();
  1633. };
  1634. const auto any = shown(_mainButton) || shown(_secondaryButton);
  1635. _webviewBottom->setVisible(!any
  1636. && !_fullscreen.current()
  1637. && !_layerShown);
  1638. if (any) {
  1639. _bottomButtonsBg->setVisible(!_layerShown);
  1640. const auto one = shown(_mainButton)
  1641. ? _mainButton.get()
  1642. : _secondaryButton.get();
  1643. const auto both = shown(_mainButton) && shown(_secondaryButton);
  1644. const auto vertical = both
  1645. && ((_secondaryPosition == RectPart::Top)
  1646. || (_secondaryPosition == RectPart::Bottom));
  1647. const auto padding = st::botWebViewBottomPadding;
  1648. const auto height = padding.top()
  1649. + (vertical
  1650. ? (_mainButton->height()
  1651. + st::botWebViewBottomSkip.y()
  1652. + _secondaryButton->height())
  1653. : one->height())
  1654. + padding.bottom();
  1655. _bottomButtonsBg->setGeometry(
  1656. inner.x(),
  1657. inner.y() + inner.height() - height,
  1658. inner.width(),
  1659. height);
  1660. auto left = padding.left();
  1661. auto bottom = height - padding.bottom();
  1662. auto available = inner.width() - padding.left() - padding.right();
  1663. if (!both) {
  1664. one->resizeToWidth(available);
  1665. one->move(left, bottom - one->height());
  1666. } else if (_secondaryPosition == RectPart::Top) {
  1667. _mainButton->resizeToWidth(available);
  1668. bottom -= _mainButton->height();
  1669. _mainButton->move(left, bottom);
  1670. bottom -= st::botWebViewBottomSkip.y();
  1671. _secondaryButton->resizeToWidth(available);
  1672. bottom -= _secondaryButton->height();
  1673. _secondaryButton->move(left, bottom);
  1674. } else if (_secondaryPosition == RectPart::Bottom) {
  1675. _secondaryButton->resizeToWidth(available);
  1676. bottom -= _secondaryButton->height();
  1677. _secondaryButton->move(left, bottom);
  1678. bottom -= st::botWebViewBottomSkip.y();
  1679. _mainButton->resizeToWidth(available);
  1680. bottom -= _mainButton->height();
  1681. _mainButton->move(left, bottom);
  1682. } else if (_secondaryPosition == RectPart::Left) {
  1683. available = (available - st::botWebViewBottomSkip.x()) / 2;
  1684. _secondaryButton->resizeToWidth(available);
  1685. bottom -= _secondaryButton->height();
  1686. _secondaryButton->move(left, bottom);
  1687. _mainButton->resizeToWidth(available);
  1688. _mainButton->move(
  1689. inner.width() - padding.right() - available,
  1690. bottom);
  1691. } else {
  1692. available = (available - st::botWebViewBottomSkip.x()) / 2;
  1693. _mainButton->resizeToWidth(available);
  1694. bottom -= _mainButton->height();
  1695. _mainButton->move(left, bottom);
  1696. _secondaryButton->resizeToWidth(available);
  1697. _secondaryButton->move(
  1698. inner.width() - padding.right() - available,
  1699. bottom);
  1700. }
  1701. } else if (_bottomButtonsBg) {
  1702. _bottomButtonsBg->hide();
  1703. }
  1704. const auto footer = _layerShown
  1705. ? 0
  1706. : any
  1707. ? _bottomButtonsBg->height()
  1708. : _fullscreen.current()
  1709. ? 0
  1710. : _webviewBottom->height();
  1711. _widget->setBottomBarHeight((!_layerShown && any) ? footer : 0);
  1712. _footerHeight = footer;
  1713. }
  1714. void Panel::showBox(object_ptr<BoxContent> box) {
  1715. showBox(std::move(box), LayerOption::KeepOther, anim::type::normal);
  1716. }
  1717. void Panel::showBox(
  1718. object_ptr<BoxContent> box,
  1719. LayerOptions options,
  1720. anim::type animated) {
  1721. if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
  1722. _layerShown = true;
  1723. const auto hideNow = !widget->isHidden();
  1724. const auto raw = box.data();
  1725. _webview->boxes.push_back(raw);
  1726. raw->boxClosing(
  1727. ) | rpl::filter([=] {
  1728. return _webview != nullptr;
  1729. }) | rpl::start_with_next([=] {
  1730. auto &list = _webview->boxes;
  1731. list.erase(ranges::remove_if(list, [&](QPointer<RpWidget> b) {
  1732. return !b || (b == raw);
  1733. }), end(list));
  1734. if (list.empty()) {
  1735. _webview->boxesLifetime.destroy();
  1736. _layerShown = false;
  1737. const auto widget = _webview
  1738. ? _webview->window.widget()
  1739. : nullptr;
  1740. if (widget && widget->isHidden()) {
  1741. widget->show();
  1742. layoutButtons();
  1743. }
  1744. }
  1745. }, _webview->boxesLifetime);
  1746. if (hideNow) {
  1747. widget->hide();
  1748. layoutButtons();
  1749. }
  1750. }
  1751. const auto raw = box.data();
  1752. InvokeQueued(raw, [=] {
  1753. if (raw->window()->isActiveWindow()) {
  1754. // In case focus is somewhat in a native child window,
  1755. // like a webview, Qt glitches here with input fields showing
  1756. // focused state, but not receiving any keyboard input:
  1757. //
  1758. // window()->windowHandle()->isActive() == false.
  1759. //
  1760. // Steps were: SeparatePanel with a WebView2 child,
  1761. // some interaction with mouse inside the WebView2,
  1762. // so that WebView2 gets focus and active window state,
  1763. // then we call setSearchAllowed() and after animation
  1764. // is finished try typing -> nothing happens.
  1765. //
  1766. // With this workaround it works fine.
  1767. _widget->activateWindow();
  1768. }
  1769. });
  1770. _widget->showBox(
  1771. std::move(box),
  1772. LayerOption::KeepOther,
  1773. anim::type::normal);
  1774. }
  1775. void Panel::showToast(TextWithEntities &&text) {
  1776. _widget->showToast(std::move(text));
  1777. }
  1778. not_null<QWidget*> Panel::toastParent() const {
  1779. return _widget->uiShow()->toastParent();
  1780. }
  1781. void Panel::hideLayer(anim::type animated) {
  1782. _widget->hideLayer(animated);
  1783. }
  1784. void Panel::showCriticalError(const TextWithEntities &text) {
  1785. _progress = nullptr;
  1786. _webviewProgress = false;
  1787. auto error = base::make_unique_q<PaddingWrap<FlatLabel>>(
  1788. _widget.get(),
  1789. object_ptr<FlatLabel>(
  1790. _widget.get(),
  1791. rpl::single(text),
  1792. st::paymentsCriticalError),
  1793. st::paymentsCriticalErrorPadding);
  1794. error->entity()->setClickHandlerFilter([=](
  1795. const ClickHandlerPtr &handler,
  1796. Qt::MouseButton) {
  1797. const auto entity = handler->getTextEntity();
  1798. if (entity.type != EntityType::CustomUrl) {
  1799. return true;
  1800. }
  1801. File::OpenUrl(entity.data);
  1802. return false;
  1803. });
  1804. _widget->showInner(std::move(error));
  1805. }
  1806. void Panel::updateThemeParams(const Webview::ThemeParams &params) {
  1807. updateColorOverrides(params);
  1808. if (!_webview || !_webview->window.widget()) {
  1809. return;
  1810. }
  1811. _webview->window.updateTheme(
  1812. params.bodyBg,
  1813. params.scrollBg,
  1814. params.scrollBgOver,
  1815. params.scrollBarBg,
  1816. params.scrollBarBgOver);
  1817. postEvent("theme_changed", "{\"theme_params\": " + params.json + "}");
  1818. }
  1819. void Panel::updateColorOverrides(const Webview::ThemeParams &params) {
  1820. if (!_headerColorReceived && params.titleBg.alpha() == 255) {
  1821. _widget->overrideTitleColor(params.titleBg);
  1822. }
  1823. if (!_bodyColorReceived && params.bodyBg.alpha() == 255) {
  1824. overrideBodyColor(params.bodyBg);
  1825. }
  1826. }
  1827. void Panel::invoiceClosed(const QString &slug, const QString &status) {
  1828. if (!_webview || !_webview->window.widget()) {
  1829. return;
  1830. }
  1831. postEvent("invoice_closed", QJsonObject{
  1832. { u"slug"_q, slug },
  1833. { u"status"_q, status },
  1834. });
  1835. if (_hiddenForPayment) {
  1836. _hiddenForPayment = false;
  1837. _widget->showAndActivate();
  1838. }
  1839. }
  1840. void Panel::hideForPayment() {
  1841. _hiddenForPayment = true;
  1842. _widget->hideGetDuration();
  1843. }
  1844. void Panel::postEvent(const QString &event) {
  1845. postEvent(event, {});
  1846. }
  1847. void Panel::postEvent(const QString &event, EventData data) {
  1848. if (!_webview) {
  1849. LOG(("BotWebView Error: Post event \"%1\" on crashed webview."
  1850. ).arg(event));
  1851. return;
  1852. }
  1853. auto written = v::is<QString>(data)
  1854. ? v::get<QString>(data).toUtf8()
  1855. : QJsonDocument(
  1856. v::get<QJsonObject>(data)).toJson(QJsonDocument::Compact);
  1857. _webview->window.eval(R"(
  1858. if (window.TelegramGameProxy) {
  1859. window.TelegramGameProxy.receiveEvent(
  1860. ")"
  1861. + event.toUtf8()
  1862. + '"' + (written.isEmpty() ? QByteArray() : ", " + written)
  1863. + R"();
  1864. }
  1865. )");
  1866. }
  1867. TextWithEntities ErrorText(const Webview::Available &info) {
  1868. Expects(info.error != Webview::Available::Error::None);
  1869. using Error = Webview::Available::Error;
  1870. switch (info.error) {
  1871. case Error::NoWebview2:
  1872. return tr::lng_payments_webview_install_edge(
  1873. tr::now,
  1874. lt_link,
  1875. Text::Link(
  1876. "Microsoft Edge WebView2 Runtime",
  1877. "https://go.microsoft.com/fwlink/p/?LinkId=2124703"),
  1878. Ui::Text::WithEntities);
  1879. case Error::NoWebKitGTK:
  1880. return { tr::lng_payments_webview_install_webkit(tr::now) };
  1881. case Error::NoOpenGL:
  1882. return { tr::lng_payments_webview_enable_opengl(tr::now) };
  1883. case Error::NonX11:
  1884. return { tr::lng_payments_webview_switch_x11(tr::now) };
  1885. case Error::OldWindows:
  1886. return { tr::lng_payments_webview_update_windows(tr::now) };
  1887. default:
  1888. return { QString::fromStdString(info.details) };
  1889. }
  1890. }
  1891. void Panel::showWebviewError(
  1892. const QString &text,
  1893. const Webview::Available &information) {
  1894. showCriticalError(TextWithEntities{ text }.append(
  1895. "\n\n"
  1896. ).append(ErrorText(information)));
  1897. }
  1898. rpl::lifetime &Panel::lifetime() {
  1899. return _widget->lifetime();
  1900. }
  1901. std::unique_ptr<Panel> Show(Args &&args) {
  1902. return std::make_unique<Panel>(std::move(args));
  1903. }
  1904. } // namespace Ui::BotWebView