send_credits_box.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  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 "boxes/send_credits_box.h"
  8. #include "api/api_credits.h"
  9. #include "apiwrap.h"
  10. #include "core/ui_integration.h" // TextContext
  11. #include "data/components/credits.h"
  12. #include "data/data_credits.h"
  13. #include "data/data_photo.h"
  14. #include "data/data_session.h"
  15. #include "data/data_user.h"
  16. #include "data/stickers/data_custom_emoji.h"
  17. #include "history/history.h"
  18. #include "history/history_item.h"
  19. #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
  20. #include "lang/lang_keys.h"
  21. #include "main/main_session.h"
  22. #include "payments/payments_checkout_process.h"
  23. #include "payments/payments_form.h"
  24. #include "settings/settings_credits_graphics.h"
  25. #include "ui/boxes/confirm_box.h"
  26. #include "ui/controls/userpic_button.h"
  27. #include "ui/effects/credits_graphics.h"
  28. #include "ui/effects/premium_graphics.h"
  29. #include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg.
  30. #include "ui/image/image_prepare.h"
  31. #include "ui/layers/generic_box.h"
  32. #include "ui/painter.h"
  33. #include "ui/rect.h"
  34. #include "ui/text/text_utilities.h"
  35. #include "ui/vertical_list.h"
  36. #include "ui/widgets/buttons.h"
  37. #include "ui/widgets/peer_bubble.h"
  38. #include "styles/style_boxes.h"
  39. #include "styles/style_chat.h"
  40. #include "styles/style_credits.h"
  41. #include "styles/style_giveaway.h"
  42. #include "styles/style_info.h" // inviteLinkSubscribeBoxTerms
  43. #include "styles/style_layers.h"
  44. #include "styles/style_premium.h"
  45. #include "styles/style_settings.h"
  46. namespace Ui {
  47. namespace {
  48. struct PaidMediaData {
  49. const Data::Invoice *invoice = nullptr;
  50. HistoryItem *item = nullptr;
  51. PeerData *peer = nullptr;
  52. int photos = 0;
  53. int videos = 0;
  54. explicit operator bool() const {
  55. return invoice && item && peer && (photos || videos);
  56. }
  57. };
  58. [[nodiscard]] PaidMediaData LookupPaidMediaData(
  59. not_null<Main::Session*> session,
  60. not_null<Payments::CreditsFormData*> form) {
  61. using namespace Payments;
  62. const auto message = std::get_if<InvoiceMessage>(&form->id.value);
  63. const auto item = message
  64. ? session->data().message(message->peer, message->itemId)
  65. : nullptr;
  66. const auto media = item ? item->media() : nullptr;
  67. const auto invoice = media ? media->invoice() : nullptr;
  68. if (!invoice || !invoice->isPaidMedia) {
  69. return {};
  70. }
  71. auto photos = 0;
  72. auto videos = 0;
  73. for (const auto &media : invoice->extendedMedia) {
  74. const auto photo = media->photo();
  75. if (photo && !photo->extendedMediaVideoDuration().has_value()) {
  76. ++photos;
  77. } else {
  78. ++videos;
  79. }
  80. }
  81. const auto bot = item->viaBot();
  82. const auto sender = item->originalSender();
  83. return {
  84. .invoice = invoice,
  85. .item = item,
  86. .peer = (bot ? bot : sender ? sender : message->peer.get()),
  87. .photos = photos,
  88. .videos = videos,
  89. };
  90. }
  91. void AddTerms(
  92. not_null<Ui::BoxContent*> box,
  93. not_null<Ui::RpWidget*> button,
  94. const style::Box &stBox) {
  95. const auto terms = Ui::CreateChild<Ui::FlatLabel>(
  96. button->parentWidget(),
  97. tr::lng_channel_invite_subscription_terms(
  98. lt_link,
  99. rpl::combine(
  100. tr::lng_paid_react_agree_link(),
  101. tr::lng_group_invite_subscription_about_url()
  102. ) | rpl::map([](const QString &text, const QString &url) {
  103. return Ui::Text::Link(text, url);
  104. }),
  105. Ui::Text::RichLangValue),
  106. st::inviteLinkSubscribeBoxTerms);
  107. const auto &buttonPadding = stBox.buttonPadding;
  108. const auto style = box->lifetime().make_state<style::Box>(style::Box{
  109. .buttonPadding = buttonPadding + QMargins(0, 0, 0, terms->height()),
  110. .buttonHeight = stBox.buttonHeight,
  111. .button = stBox.button,
  112. .margin = stBox.margin,
  113. .title = stBox.title,
  114. .bg = stBox.bg,
  115. .titleAdditionalFg = stBox.titleAdditionalFg,
  116. .shadowIgnoreTopSkip = stBox.shadowIgnoreTopSkip,
  117. .shadowIgnoreBottomSkip = stBox.shadowIgnoreBottomSkip,
  118. });
  119. button->geometryValue() | rpl::start_with_next([=](const QRect &rect) {
  120. terms->resizeToWidth(box->width()
  121. - rect::m::sum::h(st::boxRowPadding));
  122. terms->moveToLeft(
  123. rect.x() + (rect.width() - terms->width()) / 2,
  124. rect::bottom(rect) + buttonPadding.bottom() / 2);
  125. }, terms->lifetime());
  126. box->setStyle(*style);
  127. }
  128. [[nodiscard]] rpl::producer<TextWithEntities> SendCreditsConfirmText(
  129. not_null<Main::Session*> session,
  130. not_null<Payments::CreditsFormData*> form) {
  131. if (const auto data = LookupPaidMediaData(session, form)) {
  132. auto photos = 0;
  133. auto videos = 0;
  134. for (const auto &media : data.invoice->extendedMedia) {
  135. const auto photo = media->photo();
  136. if (photo && !photo->extendedMediaVideoDuration().has_value()) {
  137. ++photos;
  138. } else {
  139. ++videos;
  140. }
  141. }
  142. auto photosBold = tr::lng_credits_box_out_photos(
  143. lt_count,
  144. rpl::single(photos) | tr::to_count(),
  145. Ui::Text::Bold);
  146. auto videosBold = tr::lng_credits_box_out_videos(
  147. lt_count,
  148. rpl::single(videos) | tr::to_count(),
  149. Ui::Text::Bold);
  150. auto media = (!videos)
  151. ? ((photos > 1)
  152. ? std::move(photosBold)
  153. : tr::lng_credits_box_out_photo(Ui::Text::WithEntities))
  154. : (!photos)
  155. ? ((videos > 1)
  156. ? std::move(videosBold)
  157. : tr::lng_credits_box_out_video(Ui::Text::WithEntities))
  158. : tr::lng_credits_box_out_both(
  159. lt_photo,
  160. std::move(photosBold),
  161. lt_video,
  162. std::move(videosBold),
  163. Ui::Text::WithEntities);
  164. if (const auto user = data.peer->asUser()) {
  165. return tr::lng_credits_box_out_media_user(
  166. lt_count,
  167. rpl::single(form->invoice.amount) | tr::to_count(),
  168. lt_media,
  169. std::move(media),
  170. lt_user,
  171. rpl::single(Ui::Text::Bold(user->shortName())),
  172. Ui::Text::RichLangValue);
  173. }
  174. return tr::lng_credits_box_out_media(
  175. lt_count,
  176. rpl::single(form->invoice.amount) | tr::to_count(),
  177. lt_media,
  178. std::move(media),
  179. lt_chat,
  180. rpl::single(Ui::Text::Bold(data.peer->name())),
  181. Ui::Text::RichLangValue);
  182. }
  183. const auto bot = session->data().user(form->botId);
  184. if (form->invoice.subscriptionPeriod) {
  185. return (bot->botInfo
  186. ? tr::lng_credits_box_out_subscription_bot
  187. : tr::lng_credits_box_out_subscription_business)(
  188. lt_count,
  189. rpl::single(form->invoice.amount) | tr::to_count(),
  190. lt_title,
  191. rpl::single(TextWithEntities{ form->title }),
  192. lt_recipient,
  193. rpl::single(TextWithEntities{ bot->name() }),
  194. Ui::Text::RichLangValue);
  195. }
  196. return tr::lng_credits_box_out_sure(
  197. lt_count,
  198. rpl::single(form->invoice.amount) | tr::to_count(),
  199. lt_text,
  200. rpl::single(TextWithEntities{ form->title }),
  201. lt_bot,
  202. rpl::single(TextWithEntities{ bot->name() }),
  203. Ui::Text::RichLangValue);
  204. }
  205. [[nodiscard]] object_ptr<Ui::RpWidget> SendCreditsThumbnail(
  206. not_null<Ui::RpWidget*> parent,
  207. not_null<Main::Session*> session,
  208. not_null<Payments::CreditsFormData*> form,
  209. int photoSize) {
  210. if (const auto data = LookupPaidMediaData(session, form)) {
  211. const auto first = data.invoice->extendedMedia[0]->photo();
  212. const auto second = (data.photos > 1)
  213. ? data.invoice->extendedMedia[1]->photo()
  214. : nullptr;
  215. const auto totalCount = int(data.invoice->extendedMedia.size());
  216. if (first && first->extendedMediaPreview()) {
  217. return Settings::PaidMediaThumbnail(
  218. parent,
  219. first,
  220. second,
  221. totalCount,
  222. photoSize);
  223. }
  224. }
  225. if (form->photo) {
  226. return Settings::HistoryEntryPhoto(parent, form->photo, photoSize);
  227. }
  228. const auto bot = session->data().user(form->botId);
  229. return object_ptr<Ui::UserpicButton>(
  230. parent,
  231. bot,
  232. st::defaultUserpicButton);
  233. }
  234. [[nodiscard]] not_null<Ui::RpWidget*> SendCreditsBadge(
  235. not_null<Ui::RpWidget*> parent,
  236. int credits) {
  237. const auto widget = Ui::CreateChild<Ui::RpWidget>(parent);
  238. const auto &font = st::chatGiveawayBadgeFont;
  239. const auto text = QString::number(credits);
  240. const auto iconHeight = font->ascent - font->descent;
  241. const auto iconWidth = iconHeight + st::lineWidth;
  242. const auto width = font->width(text) + iconWidth + st::lineWidth;
  243. const auto inner = QRect(0, 0, width, font->height);
  244. const auto rect = inner + st::subscriptionCreditsBadgePadding;
  245. const auto size = rect.size();
  246. const auto svg = widget->lifetime().make_state<QSvgRenderer>(
  247. Ui::Premium::Svg());
  248. const auto half = st::chatGiveawayBadgeStroke / 2.;
  249. const auto left = st::subscriptionCreditsBadgePadding.left();
  250. const auto smaller = QRectF(rect.translated(-rect.topLeft()))
  251. - Margins(half);
  252. const auto radius = smaller.height() / 2.;
  253. widget->resize(size);
  254. widget->paintRequest() | rpl::start_with_next([=] {
  255. auto p = QPainter(widget);
  256. auto hq = PainterHighQualityEnabler(p);
  257. p.setPen(QPen(st::premiumButtonFg, st::chatGiveawayBadgeStroke * 1.));
  258. p.setBrush(st::creditsBg3);
  259. p.drawRoundedRect(smaller, radius, radius);
  260. p.translate(0, font->descent / 2);
  261. p.setPen(st::premiumButtonFg);
  262. p.setBrush(st::premiumButtonFg);
  263. svg->render(
  264. &p,
  265. QRect(
  266. left,
  267. half + (inner.height() - iconHeight) / 2,
  268. iconHeight,
  269. iconHeight));
  270. p.setFont(font);
  271. p.drawText(
  272. left + iconWidth,
  273. st::subscriptionCreditsBadgePadding.top() + font->ascent,
  274. text);
  275. }, widget->lifetime());
  276. return widget;
  277. }
  278. } // namespace
  279. void SendCreditsBox(
  280. not_null<Ui::GenericBox*> box,
  281. std::shared_ptr<Payments::CreditsFormData> form,
  282. Fn<void()> sent) {
  283. if (!form) {
  284. return;
  285. }
  286. struct State {
  287. rpl::variable<bool> confirmButtonBusy = false;
  288. };
  289. const auto state = box->lifetime().make_state<State>();
  290. const auto &stBox = st::giveawayGiftCodeBox;
  291. box->setStyle(stBox);
  292. box->setNoContentMargin(true);
  293. const auto session = form->invoice.session;
  294. const auto photoSize = st::defaultUserpicButton.photoSize;
  295. const auto content = box->verticalLayout();
  296. Ui::AddSkip(content, photoSize / 2);
  297. {
  298. const auto ministarsContainer = Ui::CreateChild<Ui::RpWidget>(box);
  299. const auto fullHeight = photoSize * 2;
  300. using MiniStars = Ui::Premium::ColoredMiniStars;
  301. const auto ministars = box->lifetime().make_state<MiniStars>(
  302. ministarsContainer,
  303. false,
  304. Ui::Premium::MiniStars::Type::BiStars);
  305. ministars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
  306. ministarsContainer->paintRequest(
  307. ) | rpl::start_with_next([=] {
  308. auto p = QPainter(ministarsContainer);
  309. ministars->paint(p);
  310. }, ministarsContainer->lifetime());
  311. box->widthValue(
  312. ) | rpl::start_with_next([=](int width) {
  313. ministarsContainer->resize(width, fullHeight);
  314. const auto w = fullHeight / 3 * 2;
  315. ministars->setCenter(QRect(
  316. (width - w) / 2,
  317. (fullHeight - w) / 2,
  318. w,
  319. w));
  320. }, ministarsContainer->lifetime());
  321. }
  322. const auto thumb = box->addRow(object_ptr<Ui::CenterWrap<>>(
  323. content,
  324. SendCreditsThumbnail(content, session, form.get(), photoSize)));
  325. thumb->setAttribute(Qt::WA_TransparentForMouseEvents);
  326. if (form->invoice.subscriptionPeriod) {
  327. const auto badge = SendCreditsBadge(content, form->invoice.amount);
  328. thumb->geometryValue() | rpl::start_with_next([=](const QRect &r) {
  329. badge->moveToLeft(
  330. r.x() + (r.width() - badge->width()) / 2,
  331. rect::bottom(r) - badge->height() / 2);
  332. }, badge->lifetime());
  333. Ui::AddSkip(content);
  334. Ui::AddSkip(content);
  335. }
  336. Ui::AddSkip(content);
  337. box->addRow(object_ptr<Ui::CenterWrap<>>(
  338. box,
  339. object_ptr<Ui::FlatLabel>(
  340. box,
  341. form->invoice.subscriptionPeriod
  342. ? rpl::single(form->title)
  343. : tr::lng_credits_box_out_title(),
  344. st::settingsPremiumUserTitle)));
  345. if (form->invoice.subscriptionPeriod && form->botId && form->photo) {
  346. Ui::AddSkip(content);
  347. Ui::AddSkip(content);
  348. const auto bot = session->data().user(form->botId);
  349. box->addRow(
  350. object_ptr<Ui::CenterWrap<>>(
  351. box,
  352. Ui::CreatePeerBubble(box, bot)));
  353. Ui::AddSkip(content);
  354. }
  355. Ui::AddSkip(content);
  356. box->addRow(object_ptr<Ui::CenterWrap<>>(
  357. box,
  358. object_ptr<Ui::FlatLabel>(
  359. box,
  360. SendCreditsConfirmText(session, form.get()),
  361. st::creditsBoxAbout)));
  362. Ui::AddSkip(content);
  363. Ui::AddSkip(content);
  364. const auto button = box->addButton(rpl::single(QString()), [=] {
  365. if (state->confirmButtonBusy.current()) {
  366. return;
  367. }
  368. const auto show = box->uiShow();
  369. const auto weak = MakeWeak(box.get());
  370. state->confirmButtonBusy = true;
  371. session->api().request(
  372. MTPpayments_SendStarsForm(
  373. MTP_long(form->formId),
  374. form->inputInvoice)
  375. ).done([=](const MTPpayments_PaymentResult &result) {
  376. result.match([&](const MTPDpayments_paymentResult &data) {
  377. session->api().applyUpdates(data.vupdates());
  378. }, [](const MTPDpayments_paymentVerificationNeeded &data) {
  379. });
  380. if (weak) {
  381. state->confirmButtonBusy = false;
  382. box->closeBox();
  383. }
  384. sent();
  385. }).fail([=](const MTP::Error &error) {
  386. if (weak) {
  387. state->confirmButtonBusy = false;
  388. }
  389. const auto id = error.type();
  390. if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
  391. auto error = ::Ui::MakeInformBox(
  392. tr::lng_payments_precheckout_stars_failed(tr::now));
  393. error->boxClosing() | rpl::start_with_next([=] {
  394. if (const auto paybox = weak.data()) {
  395. paybox->closeBox();
  396. }
  397. }, error->lifetime());
  398. show->showBox(std::move(error));
  399. } else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) {
  400. show->showToast(
  401. tr::lng_payments_precheckout_stars_timeout(tr::now));
  402. } else {
  403. show->showToast(id);
  404. }
  405. }).send();
  406. });
  407. if (form->invoice.subscriptionPeriod) {
  408. AddTerms(box, button, stBox);
  409. }
  410. {
  411. using namespace Info::Statistics;
  412. const auto loadingAnimation = InfiniteRadialAnimationWidget(
  413. button,
  414. st::giveawayGiftCodeStartButton.height / 2);
  415. AddChildToWidgetCenter(button.data(), loadingAnimation);
  416. loadingAnimation->showOn(state->confirmButtonBusy.value());
  417. }
  418. SetButtonMarkedLabel(
  419. button,
  420. rpl::combine(
  421. (form->invoice.subscriptionPeriod
  422. ? tr::lng_credits_box_out_subscription_confirm
  423. : tr::lng_credits_box_out_confirm)(
  424. lt_count,
  425. rpl::single(form->invoice.amount) | tr::to_count(),
  426. lt_emoji,
  427. rpl::single(CreditsEmojiSmall(session)),
  428. Ui::Text::RichLangValue),
  429. state->confirmButtonBusy.value()
  430. ) | rpl::map([](TextWithEntities &&text, bool busy) {
  431. return busy ? TextWithEntities() : std::move(text);
  432. }),
  433. session,
  434. st::creditsBoxButtonLabel,
  435. &box->getDelegate()->style().button.textFg);
  436. const auto buttonWidth = st::boxWidth
  437. - rect::m::sum::h(stBox.buttonPadding);
  438. button->widthValue() | rpl::filter([=] {
  439. return (button->widthNoMargins() != buttonWidth);
  440. }) | rpl::start_with_next([=] {
  441. button->resizeToWidth(buttonWidth);
  442. }, button->lifetime());
  443. {
  444. const auto close = Ui::CreateChild<Ui::IconButton>(
  445. content,
  446. st::boxTitleClose);
  447. close->setClickedCallback([=] { box->closeBox(); });
  448. content->widthValue() | rpl::start_with_next([=](int) {
  449. close->moveToRight(0, 0);
  450. }, close->lifetime());
  451. }
  452. {
  453. session->credits().load(true);
  454. const auto balance = Settings::AddBalanceWidget(
  455. content,
  456. session->credits().balanceValue(),
  457. false);
  458. rpl::combine(
  459. balance->sizeValue(),
  460. content->sizeValue()
  461. ) | rpl::start_with_next([=](const QSize &, const QSize &) {
  462. balance->moveToLeft(
  463. st::creditsHistoryRightSkip * 2,
  464. st::creditsHistoryRightSkip);
  465. balance->update();
  466. }, balance->lifetime());
  467. }
  468. }
  469. TextWithEntities CreditsEmoji(not_null<Main::Session*> session) {
  470. return Ui::Text::SingleCustomEmoji(
  471. session->data().customEmojiManager().registerInternalEmoji(
  472. st::settingsPremiumIconStar,
  473. QMargins{ 0, -st::moderateBoxExpandInnerSkip, 0, 0 },
  474. true),
  475. QString(QChar(0x2B50)));
  476. }
  477. TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
  478. return Ui::Text::IconEmoji(
  479. &st::starIconEmoji,
  480. QString(QChar(0x2B50)));
  481. }
  482. not_null<FlatLabel*> SetButtonMarkedLabel(
  483. not_null<RpWidget*> button,
  484. rpl::producer<TextWithEntities> text,
  485. Text::MarkedContext context,
  486. const style::FlatLabel &st,
  487. const style::color *textFg) {
  488. const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
  489. button,
  490. rpl::single(QString()),
  491. st);
  492. context.repaint = [=] { buttonLabel->update(); };
  493. rpl::duplicate(
  494. text
  495. ) | rpl::filter([=](const TextWithEntities &text) {
  496. return !text.text.isEmpty();
  497. }) | rpl::start_with_next([=](const TextWithEntities &text) {
  498. buttonLabel->setMarkedText(text, context);
  499. }, buttonLabel->lifetime());
  500. if (textFg) {
  501. buttonLabel->setTextColorOverride((*textFg)->c);
  502. style::PaletteChanged() | rpl::start_with_next([=] {
  503. buttonLabel->setTextColorOverride((*textFg)->c);
  504. }, buttonLabel->lifetime());
  505. }
  506. button->sizeValue(
  507. ) | rpl::start_with_next([=](const QSize &size) {
  508. buttonLabel->moveToLeft(
  509. (size.width() - buttonLabel->width()) / 2,
  510. (size.height() - buttonLabel->height()) / 2);
  511. }, buttonLabel->lifetime());
  512. buttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
  513. buttonLabel->showOn(std::move(
  514. text
  515. ) | rpl::map([=](const TextWithEntities &text) {
  516. return !text.text.isEmpty();
  517. }));
  518. return buttonLabel;
  519. }
  520. not_null<FlatLabel*> SetButtonMarkedLabel(
  521. not_null<RpWidget*> button,
  522. rpl::producer<TextWithEntities> text,
  523. not_null<Main::Session*> session,
  524. const style::FlatLabel &st,
  525. const style::color *textFg) {
  526. return SetButtonMarkedLabel(button, text, Core::TextContext({
  527. .session = session,
  528. }), st, textFg);
  529. }
  530. void SendStarsForm(
  531. not_null<Main::Session*> session,
  532. std::shared_ptr<Payments::CreditsFormData> data,
  533. Fn<void(std::optional<QString>)> done) {
  534. session->api().request(MTPpayments_SendStarsForm(
  535. MTP_long(data->formId),
  536. data->inputInvoice
  537. )).done([=](const MTPpayments_PaymentResult &result) {
  538. result.match([&](const MTPDpayments_paymentResult &data) {
  539. session->api().applyUpdates(data.vupdates());
  540. }, [](const MTPDpayments_paymentVerificationNeeded &data) {
  541. });
  542. done(std::nullopt);
  543. }).fail([=](const MTP::Error &error) {
  544. done(error.type());
  545. }).send();
  546. }
  547. } // namespace Ui