menu_sponsored.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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 "menu/menu_sponsored.h"
  8. #include "boxes/premium_preview_box.h"
  9. #include "chat_helpers/compose/compose_show.h"
  10. #include "core/ui_integration.h" // TextContext
  11. #include "data/components/sponsored_messages.h"
  12. #include "data/data_premium_limits.h"
  13. #include "data/data_session.h"
  14. #include "data/stickers/data_custom_emoji.h"
  15. #include "history/history.h"
  16. #include "lang/lang_keys.h"
  17. #include "main/main_session.h"
  18. #include "ui/boxes/report_box_graphics.h" // AddReportOptionButton.
  19. #include "ui/layers/generic_box.h"
  20. #include "ui/painter.h"
  21. #include "ui/rect.h"
  22. #include "ui/text/text_utilities.h"
  23. #include "ui/toast/toast.h"
  24. #include "ui/vertical_list.h"
  25. #include "ui/widgets/buttons.h"
  26. #include "ui/widgets/label_with_custom_emoji.h"
  27. #include "ui/widgets/menu/menu_add_action_callback.h"
  28. #include "ui/widgets/menu/menu_add_action_callback_factory.h"
  29. #include "ui/widgets/menu/menu_multiline_action.h"
  30. #include "ui/widgets/popup_menu.h"
  31. #include "styles/style_channel_earn.h"
  32. #include "styles/style_chat.h"
  33. #include "styles/style_info.h"
  34. #include "styles/style_layers.h"
  35. #include "styles/style_media_view.h"
  36. #include "styles/style_menu_icons.h"
  37. #include "styles/style_premium.h"
  38. #include "styles/style_settings.h"
  39. namespace Menu {
  40. namespace {
  41. void AboutBox(
  42. not_null<Ui::GenericBox*> box,
  43. std::shared_ptr<ChatHelpers::Show> show,
  44. const FullMsgId &fullId) {
  45. constexpr auto kUrl = "https://promote.telegram.org"_cs;
  46. box->setNoContentMargin(true);
  47. const auto isChannel = peerIsChannel(fullId.peer);
  48. const auto session = &show->session();
  49. const auto content = box->verticalLayout().get();
  50. const auto levels = Data::LevelLimits(session)
  51. .channelRestrictSponsoredLevelMin();
  52. Ui::AddSkip(content);
  53. Ui::AddSkip(content);
  54. Ui::AddSkip(content);
  55. {
  56. const auto &icon = st::sponsoredAboutTitleIcon;
  57. const auto rect = Rect(icon.size() * 1.4);
  58. auto owned = object_ptr<Ui::RpWidget>(content);
  59. owned->resize(rect.size());
  60. const auto widget = box->addRow(object_ptr<Ui::CenterWrap<>>(
  61. content,
  62. std::move(owned)))->entity();
  63. widget->paintRequest(
  64. ) | rpl::start_with_next([=] {
  65. auto p = Painter(widget);
  66. auto hq = PainterHighQualityEnabler(p);
  67. p.setPen(Qt::NoPen);
  68. p.setBrush(st::activeButtonBg);
  69. p.drawEllipse(rect);
  70. icon.paintInCenter(p, rect);
  71. }, widget->lifetime());
  72. }
  73. Ui::AddSkip(content);
  74. Ui::AddSkip(content);
  75. box->addRow(object_ptr<Ui::CenterWrap<>>(
  76. content,
  77. object_ptr<Ui::FlatLabel>(
  78. content,
  79. tr::lng_sponsored_menu_revenued_about(),
  80. st::boxTitle)));
  81. Ui::AddSkip(content);
  82. box->addRow(object_ptr<Ui::CenterWrap<>>(
  83. content,
  84. object_ptr<Ui::FlatLabel>(
  85. content,
  86. tr::lng_sponsored_revenued_subtitle(),
  87. st::channelEarnLearnDescription)));
  88. Ui::AddSkip(content);
  89. Ui::AddSkip(content);
  90. {
  91. const auto padding = QMargins(
  92. st::settingsButton.padding.left(),
  93. st::boxRowPadding.top(),
  94. st::boxRowPadding.right(),
  95. st::boxRowPadding.bottom());
  96. const auto addEntry = [&](
  97. rpl::producer<QString> title,
  98. rpl::producer<TextWithEntities> about,
  99. const style::icon &icon) {
  100. const auto top = content->add(
  101. object_ptr<Ui::FlatLabel>(
  102. content,
  103. std::move(title),
  104. st::channelEarnSemiboldLabel),
  105. padding);
  106. Ui::AddSkip(content, st::channelEarnHistoryThreeSkip);
  107. const auto label = content->add(
  108. object_ptr<Ui::FlatLabel>(
  109. content,
  110. std::move(about),
  111. st::channelEarnHistoryRecipientLabel),
  112. padding);
  113. const auto left = Ui::CreateChild<Ui::RpWidget>(
  114. box->verticalLayout().get());
  115. left->paintRequest(
  116. ) | rpl::start_with_next([=] {
  117. auto p = Painter(left);
  118. icon.paint(p, 0, 0, left->width());
  119. }, left->lifetime());
  120. left->resize(icon.size());
  121. top->geometryValue(
  122. ) | rpl::start_with_next([=](const QRect &g) {
  123. left->moveToLeft(
  124. (g.left() - left->width()) / 2,
  125. g.top() + st::channelEarnHistoryThreeSkip);
  126. }, left->lifetime());
  127. return label;
  128. };
  129. addEntry(
  130. tr::lng_sponsored_revenued_info1_title(),
  131. (isChannel
  132. ? tr::lng_sponsored_revenued_info1_description
  133. : tr::lng_sponsored_revenued_info1_bot_description)(
  134. Ui::Text::RichLangValue),
  135. st::sponsoredAboutPrivacyIcon);
  136. Ui::AddSkip(content);
  137. Ui::AddSkip(content);
  138. addEntry(
  139. (isChannel
  140. ? tr::lng_sponsored_revenued_info2_title
  141. : tr::lng_sponsored_revenued_info2_bot_title)(),
  142. (isChannel
  143. ? tr::lng_sponsored_revenued_info2_description
  144. : tr::lng_sponsored_revenued_info2_bot_description)(
  145. Ui::Text::RichLangValue),
  146. st::sponsoredAboutSplitIcon);
  147. Ui::AddSkip(content);
  148. Ui::AddSkip(content);
  149. auto link = tr::lng_settings_privacy_premium_link(
  150. ) | rpl::map([](QString t) {
  151. return Ui::Text::Link(std::move(t), u"internal:"_q);
  152. });
  153. addEntry(
  154. tr::lng_sponsored_revenued_info3_title(),
  155. isChannel
  156. ? tr::lng_sponsored_revenued_info3_description(
  157. lt_count,
  158. rpl::single(float64(levels)),
  159. lt_link,
  160. std::move(link),
  161. Ui::Text::RichLangValue)
  162. : tr::lng_sponsored_revenued_info3_bot_description(
  163. lt_link,
  164. std::move(link),
  165. Ui::Text::RichLangValue),
  166. st::sponsoredAboutRemoveIcon)->setClickHandlerFilter([=](
  167. const auto &...) {
  168. ShowPremiumPreviewBox(show, PremiumFeature::NoAds);
  169. return true;
  170. });
  171. Ui::AddSkip(content);
  172. Ui::AddSkip(content);
  173. }
  174. Ui::AddSkip(content);
  175. Ui::AddSkip(content);
  176. {
  177. box->addRow(
  178. object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
  179. content,
  180. object_ptr<Ui::FlatLabel>(
  181. content,
  182. tr::lng_sponsored_revenued_footer_title(),
  183. st::boxTitle)));
  184. }
  185. Ui::AddSkip(content);
  186. {
  187. const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji);
  188. const auto available = box->width()
  189. - rect::m::sum::h(st::boxRowPadding);
  190. box->addRow(
  191. Ui::CreateLabelWithCustomEmoji(
  192. content,
  193. (isChannel
  194. ? tr::lng_sponsored_revenued_footer_description
  195. : tr::lng_sponsored_revenued_footer_bot_description)(
  196. lt_link,
  197. tr::lng_channel_earn_about_link(
  198. lt_emoji,
  199. rpl::single(arrow),
  200. Ui::Text::RichLangValue
  201. ) | rpl::map([=](TextWithEntities t) {
  202. return Ui::Text::Link(std::move(t), kUrl.utf16());
  203. }),
  204. Ui::Text::RichLangValue),
  205. Core::TextContext({ .session = session }),
  206. st::channelEarnLearnDescription))->resizeToWidth(available);
  207. }
  208. Ui::AddSkip(content);
  209. Ui::AddSkip(content);
  210. {
  211. const auto &st = st::premiumPreviewDoubledLimitsBox;
  212. box->setStyle(st);
  213. auto button = object_ptr<Ui::RoundButton>(
  214. box,
  215. tr::lng_box_ok(),
  216. st::defaultActiveButton);
  217. button->resizeToWidth(box->width()
  218. - st.buttonPadding.left()
  219. - st.buttonPadding.left());
  220. button->setClickedCallback([=] { box->closeBox(); });
  221. box->addButton(std::move(button));
  222. }
  223. if (!isChannel) {
  224. const auto top = Ui::CreateChild<Ui::IconButton>(
  225. box,
  226. st::infoTopBarMenu);
  227. box->widthValue(
  228. ) | rpl::start_with_next([=](int width) {
  229. top->raise();
  230. top->moveToLeft(
  231. width - top->width() - st::defaultScrollArea.width,
  232. 0);
  233. }, top->lifetime());
  234. using MenuPtr = base::unique_qptr<Ui::PopupMenu>;
  235. const auto menu = top->lifetime().make_state<MenuPtr>();
  236. top->setClickedCallback([=] {
  237. *menu = base::make_unique_q<Ui::PopupMenu>(
  238. box->window(),
  239. st::popupMenuWithIcons);
  240. const auto raw = menu->get();
  241. raw->animatePhaseValue(
  242. ) | rpl::start_with_next([=](Ui::PopupMenu::AnimatePhase phase) {
  243. top->setForceRippled(phase == Ui::PopupMenu::AnimatePhase::Shown
  244. || phase == Ui::PopupMenu::AnimatePhase::StartShow);
  245. }, top->lifetime());
  246. FillSponsored(
  247. top,
  248. Ui::Menu::CreateAddActionCallback(menu->get()),
  249. show,
  250. fullId,
  251. false,
  252. true);
  253. const auto global = top->mapToGlobal(
  254. QPoint(top->width() / 4 * 3, top->height() / 2));
  255. raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
  256. raw->popup(
  257. QPoint(
  258. global.x(),
  259. std::max(global.y(), QCursor::pos().y())));
  260. return true;
  261. });
  262. }
  263. }
  264. void ShowReportSponsoredBox(
  265. std::shared_ptr<ChatHelpers::Show> show,
  266. const FullMsgId &fullId) {
  267. auto &sponsoredMessages = show->session().sponsoredMessages();
  268. const auto report = sponsoredMessages.createReportCallback(fullId);
  269. const auto guideLink = Ui::Text::Link(
  270. tr::lng_report_sponsored_reported_link(tr::now),
  271. u"https://promote.telegram.org/guidelines"_q);
  272. auto performRequest = [=](
  273. const auto &repeatRequest,
  274. Data::SponsoredReportResult::Id id) -> void {
  275. report(id, [=](const Data::SponsoredReportResult &result) {
  276. if (!result.error.isEmpty()) {
  277. show->showToast(result.error);
  278. }
  279. if (!result.options.empty()) {
  280. show->show(Box([=](not_null<Ui::GenericBox*> box) {
  281. box->setTitle(rpl::single(result.title));
  282. for (const auto &option : result.options) {
  283. const auto button = Ui::AddReportOptionButton(
  284. box->verticalLayout(),
  285. option.text,
  286. nullptr);
  287. button->setClickedCallback([=] {
  288. repeatRequest(repeatRequest, option.id);
  289. });
  290. }
  291. if (!id.isNull()) {
  292. box->addLeftButton(
  293. tr::lng_create_group_back(),
  294. [=] { box->closeBox(); });
  295. } else {
  296. const auto container = box->verticalLayout();
  297. Ui::AddSkip(container);
  298. container->add(object_ptr<Ui::DividerLabel>(
  299. container,
  300. object_ptr<Ui::FlatLabel>(
  301. container,
  302. tr::lng_report_sponsored_reported_learn(
  303. lt_link,
  304. rpl::single(guideLink),
  305. Ui::Text::WithEntities),
  306. st::boxDividerLabel),
  307. st::defaultBoxDividerLabelPadding,
  308. RectPart::Top | RectPart::Bottom));
  309. }
  310. box->addButton(
  311. tr::lng_close(),
  312. [=] { show->hideLayer(); });
  313. }));
  314. } else {
  315. constexpr auto kToastDuration = crl::time(4000);
  316. switch (result.result) {
  317. case Data::SponsoredReportResult::FinalStep::Hidden: {
  318. show->showToast(
  319. tr::lng_report_sponsored_hidden(tr::now),
  320. kToastDuration);
  321. } break;
  322. case Data::SponsoredReportResult::FinalStep::Reported: {
  323. auto text = tr::lng_report_sponsored_reported(
  324. tr::now,
  325. lt_link,
  326. guideLink,
  327. Ui::Text::WithEntities);
  328. show->showToast({
  329. .text = std::move(text),
  330. .duration = kToastDuration,
  331. });
  332. } break;
  333. case Data::SponsoredReportResult::FinalStep::Premium: {
  334. ShowPremiumPreviewBox(show, PremiumFeature::NoAds);
  335. } break;
  336. }
  337. show->hideLayer();
  338. }
  339. });
  340. };
  341. performRequest(performRequest, Data::SponsoredReportResult::Id());
  342. }
  343. } // namespace
  344. void FillSponsored(
  345. not_null<Ui::RpWidget*> parent,
  346. const Ui::Menu::MenuCallback &addAction,
  347. std::shared_ptr<ChatHelpers::Show> show,
  348. const FullMsgId &fullId,
  349. bool mediaViewer,
  350. bool skipAbout) {
  351. const auto session = &show->session();
  352. const auto details = session->sponsoredMessages().lookupDetails(fullId);
  353. const auto &info = details.info;
  354. if (!mediaViewer && !info.empty()) {
  355. auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {
  356. const auto allText = ranges::accumulate(
  357. info,
  358. TextWithEntities(),
  359. [](TextWithEntities a, TextWithEntities b) {
  360. return a.text.isEmpty() ? b : a.append('\n').append(b);
  361. }).text;
  362. const auto callback = [=] {
  363. TextUtilities::SetClipboardText({ allText });
  364. show->showToast(tr::lng_text_copied(tr::now));
  365. };
  366. for (const auto &i : info) {
  367. auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
  368. menu,
  369. st::defaultMenu,
  370. st::historySponsorInfoItem,
  371. st::historyHasCustomEmojiPosition,
  372. base::duplicate(i));
  373. item->clicks(
  374. ) | rpl::start_with_next(callback, menu->lifetime());
  375. menu->addAction(std::move(item));
  376. if (i != details.info.back()) {
  377. menu->addSeparator();
  378. }
  379. }
  380. };
  381. addAction({
  382. .text = tr::lng_sponsored_info_menu(tr::now),
  383. .handler = nullptr,
  384. .icon = &st::menuIconChannel,
  385. .fillSubmenu = std::move(fillSubmenu),
  386. });
  387. addAction({
  388. .separatorSt = &st::expandedMenuSeparator,
  389. .isSeparator = true,
  390. });
  391. }
  392. if (details.canReport) {
  393. if (!skipAbout) {
  394. addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] {
  395. show->show(Box(AboutBox, show, fullId));
  396. }, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo));
  397. }
  398. addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] {
  399. ShowReportSponsoredBox(show, fullId);
  400. }, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock));
  401. addAction({
  402. .separatorSt = (mediaViewer
  403. ? &st::mediaviewMenuSeparator
  404. : &st::expandedMenuSeparator),
  405. .isSeparator = true,
  406. });
  407. }
  408. addAction(tr::lng_sponsored_hide_ads(tr::now), [=] {
  409. if (session->premium()) {
  410. using Result = Data::SponsoredReportResult;
  411. session->sponsoredMessages().createReportCallback(
  412. fullId)(Result::Id("-1"), [](const auto &) {});
  413. } else {
  414. ShowPremiumPreviewBox(show, PremiumFeature::NoAds);
  415. }
  416. }, (mediaViewer ? &st::mediaMenuIconCancel : &st::menuIconCancel));
  417. }
  418. void ShowSponsored(
  419. not_null<Ui::RpWidget*> parent,
  420. std::shared_ptr<ChatHelpers::Show> show,
  421. const FullMsgId &fullId) {
  422. const auto menu = Ui::CreateChild<Ui::PopupMenu>(
  423. parent.get(),
  424. st::popupMenuWithIcons);
  425. FillSponsored(
  426. parent,
  427. Ui::Menu::CreateAddActionCallback(menu),
  428. show,
  429. fullId,
  430. false);
  431. menu->popup(QCursor::pos());
  432. }
  433. void ShowSponsoredAbout(
  434. std::shared_ptr<ChatHelpers::Show> show,
  435. const FullMsgId &fullId) {
  436. show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
  437. AboutBox(box, show, fullId);
  438. }));
  439. }
  440. } // namespace Menu