sponsored_message_bar.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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/sponsored_message_bar.h"
  8. #include "core/application.h"
  9. #include "core/click_handler_types.h"
  10. #include "core/ui_integration.h" // TextContext
  11. #include "data/components/sponsored_messages.h"
  12. #include "data/data_session.h"
  13. #include "history/history_item_helpers.h"
  14. #include "lang/lang_keys.h"
  15. #include "main/main_session.h"
  16. #include "menu/menu_sponsored.h"
  17. #include "ui/chat/chat_style.h"
  18. #include "ui/chat/chat_theme.h"
  19. #include "ui/dynamic_image.h"
  20. #include "ui/dynamic_thumbnails.h"
  21. #include "ui/effects/animation_value.h"
  22. #include "ui/effects/ripple_animation.h"
  23. #include "ui/image/image_prepare.h"
  24. #include "ui/rect.h"
  25. #include "ui/widgets/buttons.h"
  26. #include "ui/widgets/shadow.h"
  27. #include "window/section_widget.h"
  28. #include "window/window_controller.h"
  29. #include "window/window_session_controller.h"
  30. #include "styles/style_chat.h"
  31. #include "styles/style_chat_helpers.h"
  32. #include "styles/style_dialogs.h"
  33. namespace Ui {
  34. namespace {
  35. struct Colors final {
  36. QColor bg;
  37. QColor fg;
  38. };
  39. using ColorFactory = Fn<Colors()>;
  40. class BadgeButton final : public Ui::RippleButton {
  41. public:
  42. BadgeButton(
  43. not_null<Ui::RpWidget*> parent,
  44. tr::phrase<> text,
  45. ColorFactory cache)
  46. : Ui::RippleButton(parent, st::defaultRippleAnimation) {
  47. text(
  48. ) | rpl::start_with_next([this](const QString &t) {
  49. const auto height = st::stickersHeaderBadgeFont->height;
  50. resize(
  51. st::stickersHeaderBadgeFont->width(t) + height,
  52. height);
  53. update();
  54. }, lifetime());
  55. paintRequest() | rpl::start_with_next([this, cache, text] {
  56. auto p = QPainter(this);
  57. const auto colors = cache();
  58. const auto r = rect();
  59. const auto rippleColor = anim::with_alpha(colors.fg, .15);
  60. Ui::RippleButton::paintRipple(
  61. p,
  62. QPoint(),
  63. &rippleColor);
  64. p.setBrush(colors.bg);
  65. p.setPen(Qt::NoPen);
  66. p.drawRoundedRect(r, r.height() / 2, r.height() / 2);
  67. p.setFont(st::stickersHeaderBadgeFont);
  68. p.setPen(colors.fg);
  69. p.drawText(r, text(tr::now), style::al_center);
  70. }, lifetime());
  71. }
  72. QImage prepareRippleMask() const override {
  73. return Ui::RippleAnimation::RoundRectMask(size(), height() / 2);
  74. }
  75. };
  76. [[nodiscard]] Window::SessionController *FindSessionController(
  77. not_null<RpWidget*> widget) {
  78. const auto window = Core::App().findWindow(widget);
  79. return window ? window->sessionController() : nullptr;
  80. }
  81. [[nodiscard]] ColorFactory GenerateReplyColorCallback(
  82. not_null<Window::SessionController*> controller,
  83. not_null<RpWidget*> widget,
  84. FullMsgId fullId,
  85. int colorIndex) {
  86. const auto peer = controller->session().data().peer(fullId.peer);
  87. struct State final {
  88. std::shared_ptr<Ui::ChatTheme> theme;
  89. };
  90. const auto state = widget->lifetime().make_state<State>();
  91. Window::ChatThemeValueFromPeer(
  92. controller,
  93. peer
  94. ) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {
  95. state->theme = std::move(theme);
  96. }, widget->lifetime());
  97. return [=]() -> Colors {
  98. if (!state->theme) {
  99. return {
  100. anim::with_alpha(st::windowBgActive->c, .15),
  101. st::windowActiveTextFg->c,
  102. };
  103. }
  104. const auto context = controller->preparePaintContext({
  105. .theme = state->theme.get(),
  106. });
  107. const auto selected = false;
  108. const auto cache = context.st->coloredReplyCache(
  109. selected,
  110. colorIndex);
  111. return { cache->bg, cache->icon };
  112. };
  113. }
  114. [[nodiscard]] ColorFactory GenerateReplyColorCallback(
  115. not_null<RpWidget*> widget,
  116. FullMsgId fullId,
  117. int colorIndex) {
  118. if (const auto window = FindSessionController(widget)) {
  119. return GenerateReplyColorCallback(window, widget, fullId, colorIndex);
  120. }
  121. const auto window
  122. = widget->lifetime().make_state<Window::SessionController*>();
  123. const auto callback = widget->lifetime().make_state<ColorFactory>();
  124. return [=, color = colorIndex]() -> Colors {
  125. if (*callback) {
  126. return (*callback)();
  127. }
  128. *window = FindSessionController(widget);
  129. if (const auto w = (*window)) {
  130. *callback = GenerateReplyColorCallback(w, widget, fullId, color);
  131. return (*callback)();
  132. } else {
  133. return {
  134. anim::with_alpha(st::windowBgActive->c, .15),
  135. st::windowActiveTextFg->c,
  136. };
  137. }
  138. };
  139. }
  140. } // namespace
  141. void FillSponsoredMessageBar(
  142. not_null<RpWidget*> container,
  143. not_null<Main::Session*> session,
  144. FullMsgId fullId,
  145. Data::SponsoredFrom from,
  146. const TextWithEntities &textWithEntities) {
  147. const auto widget = CreateSimpleRectButton(
  148. container,
  149. st::defaultRippleAnimationBgOver);
  150. widget->show();
  151. container->sizeValue() | rpl::start_with_next([=](const QSize &s) {
  152. widget->resize(s);
  153. }, widget->lifetime());
  154. widget->setAcceptBoth();
  155. widget->addClickHandler([=](Qt::MouseButton button) {
  156. if (button == Qt::RightButton) {
  157. if (const auto controller = FindSessionController(widget)) {
  158. ::Menu::ShowSponsored(widget, controller->uiShow(), fullId);
  159. }
  160. } else if (button == Qt::LeftButton) {
  161. session->sponsoredMessages().clicked(fullId, false, false);
  162. UrlClickHandler::Open(from.link);
  163. }
  164. });
  165. struct State final {
  166. Ui::Text::String title;
  167. Ui::Text::String contentTitle;
  168. Ui::Text::String contentText;
  169. rpl::variable<int> lastPaintedContentLineAmount = 0;
  170. rpl::variable<int> lastPaintedContentTop = 0;
  171. std::shared_ptr<Ui::DynamicImage> rightPhoto;
  172. QImage rightPhotoImage;
  173. };
  174. const auto state = widget->lifetime().make_state<State>();
  175. const auto &titleSt = st::semiboldTextStyle;
  176. const auto &contentTitleSt = st::semiboldTextStyle;
  177. const auto &contentTextSt = st::defaultTextStyle;
  178. state->title.setText(
  179. titleSt,
  180. from.isRecommended
  181. ? tr::lng_recommended_message_title(tr::now)
  182. : tr::lng_sponsored_message_title(tr::now));
  183. state->contentTitle.setText(contentTitleSt, from.title);
  184. state->contentText.setMarkedText(
  185. contentTextSt,
  186. textWithEntities,
  187. kMarkupTextOptions,
  188. Core::TextContext({
  189. .session = session,
  190. .repaint = [=] { widget->update(); },
  191. }));
  192. const auto hostedClick = [=](ClickHandlerPtr handler) {
  193. return [=] {
  194. if (const auto controller = FindSessionController(widget)) {
  195. ActivateClickHandler(widget, handler, {
  196. .other = QVariant::fromValue(ClickHandlerContext{
  197. .itemId = fullId,
  198. .sessionWindow = base::make_weak(controller),
  199. .show = controller->uiShow(),
  200. })
  201. });
  202. }
  203. };
  204. };
  205. const auto kLinesForPhoto = 3;
  206. const auto rightPhotoSize = titleSt.font->ascent * kLinesForPhoto;
  207. const auto rightPhotoPlaceholder = titleSt.font->height * kLinesForPhoto;
  208. const auto hasRightPhoto = from.photoId > 0;
  209. if (hasRightPhoto) {
  210. state->rightPhoto = Ui::MakePhotoThumbnail(
  211. session->data().photo(from.photoId),
  212. fullId);
  213. const auto callback = [=] {
  214. state->rightPhotoImage = Images::Round(
  215. state->rightPhoto->image(rightPhotoSize),
  216. ImageRoundRadius::Small);
  217. widget->update();
  218. };
  219. state->rightPhoto->subscribeToUpdates(callback);
  220. callback();
  221. }
  222. const auto rightHide = hasRightPhoto
  223. ? nullptr
  224. : Ui::CreateChild<Ui::IconButton>(
  225. container,
  226. st::dialogsCancelSearchInPeer);
  227. if (rightHide) {
  228. container->sizeValue(
  229. ) | rpl::start_with_next([=](const QSize &s) {
  230. rightHide->moveToRight(st::buttonRadius, st::lineWidth);
  231. }, rightHide->lifetime());
  232. rightHide->setClickedCallback(
  233. hostedClick(HideSponsoredClickHandler()));
  234. }
  235. const auto badgeButton = Ui::CreateChild<BadgeButton>(
  236. widget,
  237. from.canReport
  238. ? tr::lng_sponsored_message_revenue_button
  239. : tr::lng_sponsored_top_bar_hide,
  240. GenerateReplyColorCallback(
  241. widget,
  242. fullId,
  243. from.colorIndex ? from.colorIndex : 4/*blue*/));
  244. badgeButton->setClickedCallback(
  245. hostedClick(from.canReport
  246. ? AboutSponsoredClickHandler()
  247. : HideSponsoredClickHandler()));
  248. badgeButton->show();
  249. const auto draw = [=](QPainter &p) {
  250. const auto r = widget->rect();
  251. p.fillRect(r, st::historyPinnedBg);
  252. widget->paintRipple(p, 0, 0);
  253. const auto leftPadding = st::msgReplyBarSkip + st::msgReplyBarSkip;
  254. const auto rightPadding = st::msgReplyBarSkip;
  255. const auto topPadding = st::msgReplyPadding.top();
  256. const auto availableWidthNoPhoto = r.width()
  257. - leftPadding
  258. - rightPadding;
  259. const auto availableWidth = availableWidthNoPhoto
  260. - (hasRightPhoto ? (rightPadding + rightPhotoSize) : 0)
  261. - (rightHide ? rightHide->width() : 0);
  262. const auto titleRight = leftPadding
  263. + state->title.maxWidth()
  264. + titleSt.font->spacew * 2;
  265. const auto hasSecondLineTitle = (titleRight
  266. > (availableWidth
  267. - state->contentTitle.maxWidth()
  268. - badgeButton->width()));
  269. p.setPen(st::windowActiveTextFg);
  270. state->title.draw(p, {
  271. .position = QPoint(leftPadding, topPadding),
  272. .outerWidth = availableWidth,
  273. .availableWidth = availableWidth,
  274. });
  275. badgeButton->moveToLeft(
  276. hasSecondLineTitle
  277. ? titleRight
  278. : std::min(
  279. titleRight
  280. + state->contentTitle.maxWidth()
  281. + titleSt.font->spacew * 2,
  282. r.width()
  283. - (hasRightPhoto
  284. ? (rightPadding + rightPhotoSize)
  285. : 0)
  286. - (rightHide ? rightHide->width() : 0)
  287. - rightPadding),
  288. topPadding
  289. + (titleSt.font->height - badgeButton->height()) / 2);
  290. p.setPen(st::windowFg);
  291. {
  292. const auto left = hasSecondLineTitle ? leftPadding : titleRight;
  293. const auto top = hasSecondLineTitle
  294. ? (topPadding + titleSt.font->height)
  295. : topPadding;
  296. state->contentTitle.draw(p, {
  297. .position = QPoint(left, top),
  298. .outerWidth = hasSecondLineTitle
  299. ? availableWidth
  300. : (availableWidth - titleRight),
  301. .availableWidth = availableWidth,
  302. .elisionLines = 1,
  303. });
  304. }
  305. {
  306. const auto left = leftPadding;
  307. const auto top = hasSecondLineTitle
  308. ? (topPadding
  309. + titleSt.font->height
  310. + contentTitleSt.font->height)
  311. : topPadding + titleSt.font->height;
  312. auto lastContentLineAmount = 0;
  313. const auto lineHeight = contentTextSt.font->height;
  314. const auto lineLayout = [&](int line) -> Ui::Text::LineGeometry {
  315. line++;
  316. lastContentLineAmount = line;
  317. const auto diff = (st::sponsoredMessageBarMaxHeight)
  318. - line * lineHeight;
  319. if (diff < 3 * lineHeight) {
  320. return {
  321. .width = availableWidthNoPhoto,
  322. .elided = true,
  323. };
  324. } else if (diff < 2 * lineHeight) {
  325. return {};
  326. }
  327. line += (hasSecondLineTitle ? 2 : 1)
  328. + (hasRightPhoto ? 0 : 1);
  329. return {
  330. .width = (line > kLinesForPhoto)
  331. ? availableWidthNoPhoto
  332. : availableWidth,
  333. };
  334. };
  335. state->contentText.draw(p, {
  336. .position = QPoint(left, top),
  337. .outerWidth = availableWidth,
  338. .availableWidth = availableWidth,
  339. .geometry = Ui::Text::GeometryDescriptor{
  340. .layout = std::move(lineLayout),
  341. },
  342. });
  343. state->lastPaintedContentTop = top;
  344. state->lastPaintedContentLineAmount = lastContentLineAmount;
  345. }
  346. if (hasRightPhoto) {
  347. p.drawImage(
  348. r.width() - rightPadding - rightPhotoSize,
  349. topPadding + (rightPhotoPlaceholder - rightPhotoSize) / 2,
  350. state->rightPhotoImage);
  351. }
  352. };
  353. widget->paintRequest() | rpl::start_with_next([=] {
  354. auto p = QPainter(widget);
  355. draw(p);
  356. }, widget->lifetime());
  357. rpl::combine(
  358. state->lastPaintedContentTop.value(),
  359. state->lastPaintedContentLineAmount.value()
  360. ) | rpl::distinct_until_changed() | rpl::start_with_next([=](
  361. int lastTop,
  362. int lastLines) {
  363. const auto bottomPadding = st::msgReplyPadding.top();
  364. const auto desiredHeight = lastTop
  365. + (lastLines * contentTextSt.font->height)
  366. + bottomPadding;
  367. const auto minHeight = hasRightPhoto
  368. ? (rightPhotoPlaceholder + bottomPadding * 2)
  369. : desiredHeight;
  370. container->resize(
  371. widget->width(),
  372. std::clamp(
  373. desiredHeight,
  374. minHeight,
  375. st::sponsoredMessageBarMaxHeight));
  376. }, widget->lifetime());
  377. { // Calculate a good size for container.
  378. auto dummy = QImage(1, 1, QImage::Format_ARGB32);
  379. auto p = QPainter(&dummy);
  380. draw(p);
  381. }
  382. {
  383. const auto top = Ui::CreateChild<PlainShadow>(widget);
  384. const auto bottom = Ui::CreateChild<PlainShadow>(widget);
  385. widget->sizeValue() | rpl::start_with_next([=] (const QSize &s) {
  386. top->show();
  387. top->raise();
  388. top->resizeToWidth(s.width());
  389. bottom->show();
  390. bottom->raise();
  391. bottom->resizeToWidth(s.width());
  392. bottom->moveToLeft(0, s.height() - bottom->height());
  393. }, top->lifetime());
  394. }
  395. }
  396. } // namespace Ui