reactions_settings_box.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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/reactions_settings_box.h"
  8. #include "base/unixtime.h"
  9. #include "data/data_user.h"
  10. #include "data/data_document.h"
  11. #include "data/data_document_media.h"
  12. #include "data/data_message_reactions.h"
  13. #include "data/data_session.h"
  14. #include "history/admin_log/history_admin_log_item.h"
  15. #include "history/history.h"
  16. #include "history/history_item.h"
  17. #include "history/view/reactions/history_view_reactions_strip.h"
  18. #include "history/view/history_view_element.h"
  19. #include "history/view/history_view_fake_items.h"
  20. #include "lang/lang_keys.h"
  21. #include "boxes/premium_preview_box.h"
  22. #include "main/main_session.h"
  23. #include "settings/settings_premium.h"
  24. #include "ui/chat/chat_style.h"
  25. #include "ui/chat/chat_theme.h"
  26. #include "ui/effects/scroll_content_shadow.h"
  27. #include "ui/layers/generic_box.h"
  28. #include "ui/widgets/buttons.h"
  29. #include "ui/widgets/labels.h"
  30. #include "ui/widgets/scroll_area.h"
  31. #include "ui/wrap/vertical_layout.h"
  32. #include "ui/animated_icon.h"
  33. #include "ui/painter.h"
  34. #include "ui/vertical_list.h"
  35. #include "window/section_widget.h"
  36. #include "window/window_session_controller.h"
  37. #include "styles/style_boxes.h"
  38. #include "styles/style_chat.h"
  39. #include "styles/style_layers.h"
  40. #include "styles/style_media_player.h" // mediaPlayerMenuCheck
  41. #include "styles/style_settings.h"
  42. namespace {
  43. void AddMessage(
  44. not_null<Ui::VerticalLayout*> container,
  45. not_null<Window::SessionController*> controller,
  46. rpl::producer<Data::ReactionId> &&idValue,
  47. int width) {
  48. const auto widget = container->add(
  49. object_ptr<Ui::RpWidget>(container),
  50. style::margins(
  51. 0,
  52. st::defaultVerticalListSkip,
  53. 0,
  54. st::settingsPrivacySkipTop));
  55. class Delegate final : public HistoryView::SimpleElementDelegate {
  56. public:
  57. using HistoryView::SimpleElementDelegate::SimpleElementDelegate;
  58. private:
  59. HistoryView::Context elementContext() override {
  60. return HistoryView::Context::ContactPreview;
  61. }
  62. };
  63. struct State {
  64. AdminLog::OwnedItem reply;
  65. AdminLog::OwnedItem item;
  66. std::unique_ptr<Delegate> delegate;
  67. std::unique_ptr<Ui::ChatStyle> style;
  68. struct {
  69. std::vector<rpl::lifetime> lifetimes;
  70. bool flag = false;
  71. } icons;
  72. };
  73. const auto state = container->lifetime().make_state<State>();
  74. state->delegate = std::make_unique<Delegate>(
  75. controller,
  76. crl::guard(widget, [=] { widget->update(); }));
  77. state->style = std::make_unique<Ui::ChatStyle>(
  78. controller->session().colorIndicesValue());
  79. state->style->apply(controller->defaultChatTheme().get());
  80. state->icons.lifetimes = std::vector<rpl::lifetime>(2);
  81. const auto history = controller->session().data().history(
  82. PeerData::kServiceNotificationsId);
  83. state->reply = HistoryView::GenerateItem(
  84. state->delegate.get(),
  85. history,
  86. HistoryView::GenerateUser(
  87. history,
  88. tr::lng_settings_chat_message_reply_from(tr::now)),
  89. FullMsgId(),
  90. tr::lng_settings_chat_message_reply(tr::now));
  91. auto message = HistoryView::GenerateItem(
  92. state->delegate.get(),
  93. history,
  94. history->peer->id,
  95. state->reply->data()->fullId(),
  96. tr::lng_settings_chat_message(tr::now));
  97. const auto view = message.get();
  98. state->item = std::move(message);
  99. const auto padding = st::settingsForwardPrivacyPadding;
  100. const auto updateWidgetSize = [=](int width) {
  101. const auto height = view->resizeGetHeight(width);
  102. const auto top = view->marginTop();
  103. const auto bottom = view->marginBottom();
  104. const auto full = padding + top + height + bottom + padding;
  105. widget->resize(width, full);
  106. };
  107. widget->widthValue(
  108. ) | rpl::filter(
  109. rpl::mappers::_1 >= (st::historyMinimalWidth / 2)
  110. ) | rpl::start_with_next(updateWidgetSize, widget->lifetime());
  111. updateWidgetSize(width);
  112. const auto rightSize = st::settingsReactionCornerSize;
  113. const auto rightRect = [=] {
  114. const auto viewInner = view->innerGeometry();
  115. return QRect(
  116. viewInner.x() + viewInner.width(),
  117. padding
  118. + view->marginTop()
  119. + view->resizeGetHeight(widget->width())
  120. - rightSize.height(),
  121. rightSize.width(),
  122. rightSize.height()).translated(st::settingsReactionCornerSkip);
  123. };
  124. widget->paintRequest(
  125. ) | rpl::start_with_next([=](const QRect &rect) {
  126. Window::SectionWidget::PaintBackground(
  127. controller,
  128. controller->defaultChatTheme().get(), // #TODO themes
  129. widget,
  130. rect);
  131. Painter p(widget);
  132. auto hq = PainterHighQualityEnabler(p);
  133. const auto theme = controller->defaultChatTheme().get();
  134. auto context = theme->preparePaintContext(
  135. state->style.get(),
  136. widget->rect(),
  137. widget->rect(),
  138. controller->isGifPausedAtLeastFor(
  139. Window::GifPauseReason::Layer));
  140. context.outbg = view->hasOutLayout();
  141. {
  142. const auto radius = rightSize.height() / 2;
  143. const auto r = rightRect();
  144. const auto &st = context.st->messageStyle(
  145. context.outbg,
  146. context.selected());
  147. p.setPen(Qt::NoPen);
  148. p.setBrush(st.msgShadow);
  149. p.drawRoundedRect(r.translated(0, st::msgShadow), radius, radius);
  150. p.setBrush(st.msgBg);
  151. p.drawRoundedRect(r, radius, radius);
  152. }
  153. p.translate(padding / 2, padding + view->marginBottom());
  154. view->draw(p, context);
  155. }, widget->lifetime());
  156. auto selectedId = rpl::duplicate(idValue);
  157. std::move(
  158. selectedId
  159. ) | rpl::start_with_next([
  160. =,
  161. idValue = std::move(idValue),
  162. iconSize = st::settingsReactionMessageSize
  163. ](const Data::ReactionId &id) {
  164. const auto index = state->icons.flag ? 1 : 0;
  165. state->icons.flag = !state->icons.flag;
  166. state->icons.lifetimes[index] = rpl::lifetime();
  167. const auto &reactions = controller->session().data().reactions();
  168. auto iconPositionValue = widget->geometryValue(
  169. ) | rpl::map([=](const QRect &r) {
  170. return widget->pos()
  171. + rightRect().topLeft()
  172. + QPoint(
  173. (rightSize.width() - iconSize) / 2,
  174. (rightSize.height() - iconSize) / 2);
  175. });
  176. auto destroys = rpl::duplicate(
  177. idValue
  178. ) | rpl::skip(1) | rpl::to_empty;
  179. if (const auto customId = id.custom()) {
  180. AddReactionCustomIcon(
  181. container,
  182. std::move(iconPositionValue),
  183. iconSize,
  184. controller,
  185. customId,
  186. std::move(destroys),
  187. &state->icons.lifetimes[index]);
  188. return;
  189. }
  190. for (const auto &r : reactions.list(Data::Reactions::Type::Active)) {
  191. if (r.id != id) {
  192. continue;
  193. }
  194. AddReactionAnimatedIcon(
  195. container,
  196. std::move(iconPositionValue),
  197. iconSize,
  198. r,
  199. rpl::never<>(),
  200. std::move(destroys),
  201. &state->icons.lifetimes[index]);
  202. return;
  203. }
  204. }, widget->lifetime());
  205. }
  206. not_null<Ui::RpWidget*> AddReactionIconWrap(
  207. not_null<Ui::RpWidget*> parent,
  208. rpl::producer<QPoint> iconPositionValue,
  209. int iconSize,
  210. Fn<void(not_null<QWidget*>, QPainter&)> paintCallback,
  211. rpl::producer<> &&destroys,
  212. not_null<rpl::lifetime*> stateLifetime) {
  213. struct State {
  214. base::unique_qptr<Ui::RpWidget> widget;
  215. Ui::Animations::Simple finalAnimation;
  216. };
  217. const auto state = stateLifetime->make_state<State>();
  218. state->widget = base::make_unique_q<Ui::RpWidget>(parent);
  219. const auto widget = state->widget.get();
  220. widget->resize(iconSize, iconSize);
  221. widget->setAttribute(Qt::WA_TransparentForMouseEvents);
  222. std::move(
  223. iconPositionValue
  224. ) | rpl::start_with_next([=](const QPoint &point) {
  225. widget->moveToLeft(point.x(), point.y());
  226. }, widget->lifetime());
  227. const auto update = crl::guard(widget, [=] { widget->update(); });
  228. widget->paintRequest(
  229. ) | rpl::start_with_next([=] {
  230. auto p = QPainter(widget);
  231. if (state->finalAnimation.animating()) {
  232. const auto progress = 1. - state->finalAnimation.value(0.);
  233. const auto size = widget->size();
  234. const auto scaledSize = size * progress;
  235. const auto scaledCenter = QPoint(
  236. (size.width() - scaledSize.width()) / 2.,
  237. (size.height() - scaledSize.height()) / 2.);
  238. p.setOpacity(progress);
  239. p.translate(scaledCenter);
  240. p.scale(progress, progress);
  241. }
  242. paintCallback(widget, p);
  243. }, widget->lifetime());
  244. std::move(
  245. destroys
  246. ) | rpl::take(1) | rpl::start_with_next([=, from = 0., to = 1.] {
  247. state->finalAnimation.start(
  248. [=](float64 value) {
  249. update();
  250. if (value == to) {
  251. stateLifetime->destroy();
  252. }
  253. },
  254. from,
  255. to,
  256. st::defaultPopupMenu.showDuration);
  257. }, widget->lifetime());
  258. widget->raise();
  259. widget->show();
  260. return widget;
  261. }
  262. } // namespace
  263. void AddReactionAnimatedIcon(
  264. not_null<Ui::RpWidget*> parent,
  265. rpl::producer<QPoint> iconPositionValue,
  266. int iconSize,
  267. const Data::Reaction &reaction,
  268. rpl::producer<> &&selects,
  269. rpl::producer<> &&destroys,
  270. not_null<rpl::lifetime*> stateLifetime) {
  271. struct State {
  272. struct Entry {
  273. std::shared_ptr<Data::DocumentMedia> media;
  274. std::shared_ptr<Ui::AnimatedIcon> icon;
  275. };
  276. Entry appear;
  277. Entry select;
  278. bool appearAnimated = false;
  279. rpl::lifetime loadingLifetime;
  280. };
  281. const auto state = stateLifetime->make_state<State>();
  282. state->appear.media = reaction.appearAnimation->createMediaView();
  283. state->select.media = reaction.selectAnimation->createMediaView();
  284. state->appear.media->checkStickerLarge();
  285. state->select.media->checkStickerLarge();
  286. rpl::single() | rpl::then(
  287. reaction.appearAnimation->session().downloaderTaskFinished()
  288. ) | rpl::start_with_next([=] {
  289. const auto check = [&](State::Entry &entry) {
  290. if (!entry.media) {
  291. return true;
  292. } else if (!entry.media->loaded()) {
  293. return false;
  294. }
  295. entry.icon = HistoryView::Reactions::DefaultIconFactory(
  296. entry.media.get(),
  297. iconSize);
  298. entry.media = nullptr;
  299. return true;
  300. };
  301. if (check(state->select) && check(state->appear)) {
  302. state->loadingLifetime.destroy();
  303. }
  304. }, state->loadingLifetime);
  305. const auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {
  306. const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
  307. const auto frame = animation->frame(st::windowFg->c);
  308. p.drawImage(
  309. QRect(
  310. (widget->width() - iconSize) / 2,
  311. (widget->height() - iconSize) / 2,
  312. iconSize,
  313. iconSize),
  314. frame);
  315. };
  316. const auto appear = state->appear.icon.get();
  317. if (appear && !state->appearAnimated) {
  318. state->appearAnimated = true;
  319. appear->animate(crl::guard(widget, [=] { widget->update(); }));
  320. }
  321. if (appear && appear->animating()) {
  322. paintFrame(appear);
  323. } else if (const auto select = state->select.icon.get()) {
  324. paintFrame(select);
  325. }
  326. };
  327. const auto widget = AddReactionIconWrap(
  328. parent,
  329. std::move(iconPositionValue),
  330. iconSize,
  331. paintCallback,
  332. std::move(destroys),
  333. stateLifetime);
  334. std::move(
  335. selects
  336. ) | rpl::start_with_next([=] {
  337. const auto select = state->select.icon.get();
  338. if (select && !select->animating()) {
  339. select->animate(crl::guard(widget, [=] { widget->update(); }));
  340. }
  341. }, widget->lifetime());
  342. }
  343. void AddReactionCustomIcon(
  344. not_null<Ui::RpWidget*> parent,
  345. rpl::producer<QPoint> iconPositionValue,
  346. int iconSize,
  347. not_null<Window::SessionController*> controller,
  348. DocumentId customId,
  349. rpl::producer<> &&destroys,
  350. not_null<rpl::lifetime*> stateLifetime) {
  351. struct State {
  352. std::unique_ptr<Ui::Text::CustomEmoji> custom;
  353. Fn<void()> repaint;
  354. };
  355. const auto state = stateLifetime->make_state<State>();
  356. static constexpr auto tag = Data::CustomEmojiManager::SizeTag::Normal;
  357. state->custom = controller->session().data().customEmojiManager().create(
  358. customId,
  359. [=] { state->repaint(); },
  360. tag);
  361. const auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {
  362. const auto ratio = style::DevicePixelRatio();
  363. const auto size = Data::FrameSizeFromTag(tag) / ratio;
  364. state->custom->paint(p, {
  365. .textColor = st::windowFg->c,
  366. .now = crl::now(),
  367. .position = QPoint(
  368. (widget->width() - size) / 2,
  369. (widget->height() - size) / 2),
  370. .paused = controller->isGifPausedAtLeastFor(
  371. Window::GifPauseReason::Layer),
  372. });
  373. };
  374. const auto widget = AddReactionIconWrap(
  375. parent,
  376. std::move(iconPositionValue),
  377. iconSize,
  378. paintCallback,
  379. std::move(destroys),
  380. stateLifetime);
  381. state->repaint = crl::guard(widget, [=] { widget->update(); });
  382. }
  383. void ReactionsSettingsBox(
  384. not_null<Ui::GenericBox*> box,
  385. not_null<Window::SessionController*> controller) {
  386. struct State {
  387. rpl::variable<Data::ReactionId> selectedId;
  388. };
  389. const auto &reactions = controller->session().data().reactions();
  390. const auto state = box->lifetime().make_state<State>();
  391. state->selectedId = reactions.favoriteId();
  392. const auto pinnedToTop = box->setPinnedToTopContent(
  393. object_ptr<Ui::VerticalLayout>(box));
  394. auto idValue = state->selectedId.value();
  395. AddMessage(pinnedToTop, controller, std::move(idValue), box->width());
  396. Ui::AddSubsectionTitle(
  397. pinnedToTop,
  398. tr::lng_settings_chat_reactions_subtitle());
  399. const auto container = box->verticalLayout();
  400. const auto check = Ui::CreateChild<Ui::RpWidget>(container.get());
  401. check->resize(st::settingsReactionCornerSize);
  402. check->setAttribute(Qt::WA_TransparentForMouseEvents);
  403. check->paintRequest(
  404. ) | rpl::start_with_next([=] {
  405. Painter p(check);
  406. st::mediaPlayerMenuCheck.paintInCenter(p, check->rect());
  407. }, check->lifetime());
  408. const auto checkButton = [=](not_null<const Ui::RpWidget*> button) {
  409. check->moveToRight(
  410. st::settingsButtonRightSkip,
  411. button->y() + (button->height() - check->height()) / 2);
  412. };
  413. auto firstCheckedButton = (Ui::RpWidget*)(nullptr);
  414. auto list = reactions.list(Data::Reactions::Type::Active);
  415. if (const auto favorite = reactions.favorite()) {
  416. if (favorite->id.custom()) {
  417. list.insert(begin(list), *favorite);
  418. }
  419. }
  420. for (const auto &r : list) {
  421. const auto button = container->add(object_ptr<Ui::SettingsButton>(
  422. container,
  423. rpl::single<QString>(base::duplicate(r.title)),
  424. st::settingsButton));
  425. const auto iconSize = st::settingsReactionSize;
  426. const auto left = button->st().iconLeft;
  427. auto iconPositionValue = button->sizeValue(
  428. ) | rpl::map([=](const QSize &s) {
  429. return QPoint(
  430. left + st::settingsReactionRightSkip,
  431. (s.height() - iconSize) / 2);
  432. });
  433. if (const auto customId = r.id.custom()) {
  434. AddReactionCustomIcon(
  435. button,
  436. std::move(iconPositionValue),
  437. iconSize,
  438. controller,
  439. customId,
  440. rpl::never<>(),
  441. &button->lifetime());
  442. } else {
  443. AddReactionAnimatedIcon(
  444. button,
  445. std::move(iconPositionValue),
  446. iconSize,
  447. r,
  448. button->events(
  449. ) | rpl::filter([=](not_null<QEvent*> event) {
  450. return event->type() == QEvent::Enter;
  451. }) | rpl::to_empty,
  452. rpl::never<>(),
  453. &button->lifetime());
  454. }
  455. button->setClickedCallback([=, id = r.id] {
  456. checkButton(button);
  457. state->selectedId = id;
  458. });
  459. if (r.id == state->selectedId.current()) {
  460. firstCheckedButton = button;
  461. }
  462. }
  463. if (firstCheckedButton) {
  464. firstCheckedButton->geometryValue(
  465. ) | rpl::filter([=](const QRect &r) {
  466. return r.isValid();
  467. }) | rpl::take(1) | rpl::start_with_next([=] {
  468. checkButton(firstCheckedButton);
  469. }, firstCheckedButton->lifetime());
  470. }
  471. check->raise();
  472. box->setTitle(tr::lng_settings_chat_reactions_title());
  473. box->setWidth(st::boxWideWidth);
  474. box->addButton(tr::lng_settings_save(), [=] {
  475. const auto &data = controller->session().data();
  476. const auto selectedId = state->selectedId.current();
  477. if (data.reactions().favoriteId() != selectedId) {
  478. data.reactions().setFavorite(selectedId);
  479. }
  480. box->closeBox();
  481. });
  482. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  483. }