who_reacted_context_action.cpp 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  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/controls/who_reacted_context_action.h"
  8. #include "base/call_delayed.h"
  9. #include "ui/widgets/menu/menu_action.h"
  10. #include "ui/widgets/popup_menu.h"
  11. #include "ui/effects/ripple_animation.h"
  12. #include "ui/chat/group_call_userpics.h"
  13. #include "ui/text/text_custom_emoji.h"
  14. #include "ui/emoji_config.h"
  15. #include "ui/painter.h"
  16. #include "ui/ui_utility.h"
  17. #include "lang/lang_keys.h"
  18. #include "styles/style_chat.h"
  19. #include "styles/style_chat_helpers.h"
  20. #include "styles/style_menu_icons.h"
  21. #include <QtCore/QLocale>
  22. namespace Lang {
  23. namespace {
  24. struct StringWithReacted {
  25. QString text;
  26. int seen = 0;
  27. };
  28. } // namespace
  29. template <typename ResultString>
  30. struct StartReplacements;
  31. template <>
  32. struct StartReplacements<StringWithReacted> {
  33. static inline StringWithReacted Call(QString &&langString) {
  34. return { std::move(langString) };
  35. }
  36. };
  37. template <typename ResultString>
  38. struct ReplaceTag;
  39. template <>
  40. struct ReplaceTag<StringWithReacted> {
  41. static StringWithReacted Call(
  42. StringWithReacted &&original,
  43. ushort tag,
  44. const StringWithReacted &replacement);
  45. };
  46. StringWithReacted ReplaceTag<StringWithReacted>::Call(
  47. StringWithReacted &&original,
  48. ushort tag,
  49. const StringWithReacted &replacement) {
  50. const auto offset = FindTagReplacementPosition(original.text, tag);
  51. if (offset < 0) {
  52. return std::move(original);
  53. }
  54. original.text = ReplaceTag<QString>::Call(
  55. std::move(original.text),
  56. tag,
  57. replacement.text + '/' + QString::number(original.seen));
  58. return std::move(original);
  59. }
  60. } // namespace Lang
  61. namespace Ui {
  62. namespace {
  63. constexpr auto kPreloaderAlpha = 0.2;
  64. using Text::CustomEmojiFactory;
  65. class Action final : public Menu::ItemBase {
  66. public:
  67. Action(
  68. not_null<PopupMenu*> parentMenu,
  69. rpl::producer<WhoReadContent> content,
  70. CustomEmojiFactory factory,
  71. Fn<void(WhoReadParticipant)> participantChosen,
  72. Fn<void()> showAllChosen);
  73. bool isEnabled() const override;
  74. not_null<QAction*> action() const override;
  75. void handleKeyPress(not_null<QKeyEvent*> e) override;
  76. protected:
  77. QPoint prepareRippleStartPosition() const override;
  78. QImage prepareRippleMask() const override;
  79. int contentHeight() const override;
  80. private:
  81. void paint(Painter &p);
  82. void updateUserpicsFromContent();
  83. void resolveMinWidth();
  84. void refreshText();
  85. void refreshDimensions();
  86. void populateSubmenu();
  87. const not_null<PopupMenu*> _parentMenu;
  88. const not_null<QAction*> _dummyAction;
  89. const Fn<void(WhoReadParticipant)> _participantChosen;
  90. const Fn<void()> _showAllChosen;
  91. const std::unique_ptr<GroupCallUserpics> _userpics;
  92. const style::Menu &_st;
  93. const CustomEmojiFactory _customEmojiFactory;
  94. WhoReactedListMenu _submenu;
  95. Text::String _text;
  96. std::unique_ptr<Ui::Text::CustomEmoji> _custom;
  97. int _textWidth = 0;
  98. const int _height = 0;
  99. int _userpicsWidth = 0;
  100. bool _appeared = false;
  101. WhoReadContent _content;
  102. };
  103. class WhenAction final : public Menu::ItemBase {
  104. public:
  105. WhenAction(
  106. not_null<PopupMenu*> parentMenu,
  107. rpl::producer<WhoReadContent> content,
  108. Fn<void()> showOrPremium);
  109. bool isEnabled() const override;
  110. not_null<QAction*> action() const override;
  111. protected:
  112. QPoint prepareRippleStartPosition() const override;
  113. QImage prepareRippleMask() const override;
  114. int contentHeight() const override;
  115. private:
  116. void paint(Painter &p);
  117. void resizeEvent(QResizeEvent *e) override;
  118. void resolveMinWidth();
  119. void refreshText();
  120. void refreshDimensions();
  121. const not_null<PopupMenu*> _parentMenu;
  122. const not_null<QAction*> _dummyAction;
  123. const Fn<void()> _showOrPremium;
  124. const style::Menu &_st;
  125. Text::String _text;
  126. Text::String _show;
  127. QRect _showRect;
  128. int _textWidth = 0;
  129. const int _height = 0;
  130. WhoReadContent _content;
  131. };
  132. TextParseOptions MenuTextOptions = {
  133. TextParseLinks, // flags
  134. 0, // maxw
  135. 0, // maxh
  136. Qt::LayoutDirectionAuto, // dir
  137. };
  138. [[nodiscard]] QString FormatReactedString(int reacted, int seen) {
  139. const auto projection = [&](const QString &text) {
  140. return Lang::StringWithReacted{ text, seen };
  141. };
  142. return tr::lng_context_seen_reacted(
  143. tr::now,
  144. lt_count_short,
  145. reacted,
  146. projection
  147. ).text;
  148. }
  149. Action::Action(
  150. not_null<PopupMenu*> parentMenu,
  151. rpl::producer<WhoReadContent> content,
  152. Text::CustomEmojiFactory factory,
  153. Fn<void(WhoReadParticipant)> participantChosen,
  154. Fn<void()> showAllChosen)
  155. : ItemBase(parentMenu->menu(), parentMenu->menu()->st())
  156. , _parentMenu(parentMenu)
  157. , _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
  158. , _participantChosen(std::move(participantChosen))
  159. , _showAllChosen(std::move(showAllChosen))
  160. , _userpics(std::make_unique<GroupCallUserpics>(
  161. st::defaultWhoRead.userpics,
  162. rpl::never<bool>(),
  163. [=] { update(); }))
  164. , _st(parentMenu->menu()->st())
  165. , _customEmojiFactory(std::move(factory))
  166. , _submenu(_customEmojiFactory, _participantChosen, _showAllChosen)
  167. , _height(st::defaultWhoRead.itemPadding.top()
  168. + _st.itemStyle.font->height
  169. + st::defaultWhoRead.itemPadding.bottom()) {
  170. const auto parent = parentMenu->menu();
  171. const auto delay = anim::Disabled() ? 0 : parentMenu->st().duration;
  172. const auto checkAppeared = [=, now = crl::now()](bool force = false) {
  173. _appeared = force || ((crl::now() - now) >= delay);
  174. };
  175. setAcceptBoth(true);
  176. initResizeHook(parent->sizeValue());
  177. std::move(
  178. content
  179. ) | rpl::start_with_next([=](WhoReadContent &&content) {
  180. checkAppeared();
  181. const auto changed = (_content.participants != content.participants)
  182. || (_content.state != content.state);
  183. _content = content;
  184. if (changed) {
  185. PostponeCall(this, [=] { populateSubmenu(); });
  186. }
  187. updateUserpicsFromContent();
  188. refreshText();
  189. refreshDimensions();
  190. setPointerCursor(isEnabled());
  191. _dummyAction->setEnabled(isEnabled());
  192. if (!isEnabled()) {
  193. setSelected(false);
  194. }
  195. update();
  196. }, lifetime());
  197. resolveMinWidth();
  198. _userpics->widthValue(
  199. ) | rpl::start_with_next([=](int width) {
  200. _userpicsWidth = width;
  201. refreshDimensions();
  202. update();
  203. }, lifetime());
  204. paintRequest(
  205. ) | rpl::start_with_next([=] {
  206. Painter p(this);
  207. paint(p);
  208. }, lifetime());
  209. clicks(
  210. ) | rpl::start_with_next([=] {
  211. if (_content.participants.size() == 1) {
  212. if (const auto onstack = _participantChosen) {
  213. onstack(_content.participants.front());
  214. }
  215. } else if (_content.fullReactionsCount > 0) {
  216. if (const auto onstack = _showAllChosen) {
  217. onstack();
  218. }
  219. }
  220. }, lifetime());
  221. enableMouseSelecting();
  222. base::call_delayed(parentMenu->st().duration, this, [=] {
  223. if (!_appeared) {
  224. checkAppeared(true);
  225. updateUserpicsFromContent();
  226. }
  227. });
  228. }
  229. void Action::resolveMinWidth() {
  230. const auto maxIconWidth = 0;
  231. const auto width = [&](const QString &text) {
  232. return _st.itemStyle.font->width(text);
  233. };
  234. const auto maxText = (_content.type == WhoReadType::Listened)
  235. ? tr::lng_context_seen_listened(tr::now, lt_count, 999)
  236. : (_content.type == WhoReadType::Watched)
  237. ? tr::lng_context_seen_watched(tr::now, lt_count, 999)
  238. : (_content.type == WhoReadType::Seen)
  239. ? tr::lng_context_seen_text(tr::now, lt_count, 999)
  240. : QString();
  241. const auto maxReacted = (_content.fullReactionsCount > 0)
  242. ? (!maxText.isEmpty()
  243. ? FormatReactedString(_content.fullReactionsCount, 999)
  244. : tr::lng_context_seen_reacted(
  245. tr::now,
  246. lt_count_short,
  247. _content.fullReactionsCount))
  248. : QString();
  249. const auto maxTextWidth = std::max(width(maxText), width(maxReacted));
  250. const auto maxWidth = st::defaultWhoRead.itemPadding.left()
  251. + maxIconWidth
  252. + maxTextWidth
  253. + _userpics->maxWidth()
  254. + st::defaultWhoRead.itemPadding.right();
  255. setMinWidth(maxWidth);
  256. }
  257. void Action::updateUserpicsFromContent() {
  258. if (!_appeared) {
  259. return;
  260. }
  261. auto users = std::vector<GroupCallUser>();
  262. if (!_content.participants.empty()) {
  263. const auto count = std::min(
  264. int(_content.participants.size()),
  265. WhoReadParticipant::kMaxSmallUserpics);
  266. const auto factor = style::DevicePixelRatio();
  267. users.reserve(count);
  268. for (auto i = 0; i != count; ++i) {
  269. auto &participant = _content.participants[i];
  270. participant.userpicSmall.setDevicePixelRatio(factor);
  271. users.push_back({
  272. .userpic = participant.userpicSmall,
  273. .userpicKey = participant.userpicKey,
  274. .id = participant.id,
  275. });
  276. }
  277. }
  278. _userpics->update(users, true);
  279. }
  280. void Action::populateSubmenu() {
  281. if (_content.participants.size() < 1) {
  282. _submenu.clear();
  283. _parentMenu->removeSubmenu(action());
  284. if (!isEnabled()) {
  285. setSelected(false);
  286. }
  287. return;
  288. }
  289. const auto submenu = _parentMenu->ensureSubmenu(
  290. action(),
  291. st::whoReadMenu);
  292. _submenu.populate(submenu, _content);
  293. _parentMenu->checkSubmenuShow();
  294. }
  295. void Action::paint(Painter &p) {
  296. const auto enabled = isEnabled();
  297. const auto selected = isSelected();
  298. if (selected && _st.itemBgOver->c.alpha() < 255) {
  299. p.fillRect(0, 0, width(), _height, _st.itemBg);
  300. }
  301. const auto &bg = selected ? _st.itemBgOver : _st.itemBg;
  302. p.fillRect(0, 0, width(), _height, bg);
  303. if (enabled) {
  304. paintRipple(p, 0, 0);
  305. }
  306. if (!_custom && !_content.singleCustomEntityData.isEmpty()) {
  307. _custom = _customEmojiFactory(
  308. _content.singleCustomEntityData,
  309. { .repaint = [=] { update(); } });
  310. }
  311. if (_custom) {
  312. const auto ratio = style::DevicePixelRatio();
  313. const auto size = Emoji::GetSizeNormal() / ratio;
  314. const auto adjusted = Text::AdjustCustomEmojiSize(size);
  315. const auto x = st::defaultWhoRead.iconPosition.x()
  316. + (st::whoReadChecks.width() - adjusted) / 2;
  317. const auto y = (_height - adjusted) / 2;
  318. _custom->paint(p, {
  319. .textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,
  320. .now = crl::now(),
  321. .position = { x, y },
  322. });
  323. } else {
  324. const auto &icon = (_content.fullReactionsCount)
  325. ? (!enabled
  326. ? st::whoReadReactionsDisabled
  327. : selected
  328. ? st::whoReadReactionsOver
  329. : st::whoReadReactions)
  330. : (_content.type == WhoReadType::Seen)
  331. ? (!enabled
  332. ? st::whoReadChecksDisabled
  333. : selected
  334. ? st::whoReadChecksOver
  335. : st::whoReadChecks)
  336. : (!enabled
  337. ? st::whoReadPlayedDisabled
  338. : selected
  339. ? st::whoReadPlayedOver
  340. : st::whoReadPlayed);
  341. icon.paint(p, st::defaultWhoRead.iconPosition, width());
  342. }
  343. p.setPen(!enabled
  344. ? _st.itemFgDisabled
  345. : selected
  346. ? _st.itemFgOver
  347. : _st.itemFg);
  348. _text.drawLeftElided(
  349. p,
  350. st::defaultWhoRead.itemPadding.left(),
  351. st::defaultWhoRead.itemPadding.top(),
  352. _textWidth,
  353. width());
  354. if (_appeared) {
  355. _userpics->paint(
  356. p,
  357. width() - st::defaultWhoRead.itemPadding.right(),
  358. (height() - st::defaultWhoRead.userpics.size) / 2,
  359. st::defaultWhoRead.userpics.size);
  360. }
  361. }
  362. void Action::refreshText() {
  363. const auto usersCount = int(_content.participants.size());
  364. const auto onlySeenCount = ranges::count(
  365. _content.participants,
  366. QString(),
  367. &WhoReadParticipant::customEntityData);
  368. const auto count = std::max(_content.fullReactionsCount, usersCount);
  369. _text.setMarkedText(
  370. _st.itemStyle,
  371. { ((_content.state == WhoReadState::Unknown)
  372. ? tr::lng_context_seen_loading(tr::now)
  373. : (usersCount == 1)
  374. ? _content.participants.front().name
  375. : (_content.fullReactionsCount > 0
  376. && _content.fullReactionsCount <= _content.fullReadCount)
  377. ? FormatReactedString(
  378. _content.fullReactionsCount,
  379. _content.fullReadCount)
  380. : (_content.type == WhoReadType::Reacted
  381. || (count > 0 && _content.fullReactionsCount > usersCount)
  382. || (count > 0 && onlySeenCount == 0))
  383. ? (count
  384. ? tr::lng_context_seen_reacted(
  385. tr::now,
  386. lt_count_short,
  387. count)
  388. : tr::lng_context_seen_reacted_none(tr::now))
  389. : (_content.type == WhoReadType::Watched)
  390. ? (count
  391. ? tr::lng_context_seen_watched(tr::now, lt_count, count)
  392. : tr::lng_context_seen_watched_none(tr::now))
  393. : (_content.type == WhoReadType::Listened)
  394. ? (count
  395. ? tr::lng_context_seen_listened(tr::now, lt_count, count)
  396. : tr::lng_context_seen_listened_none(tr::now))
  397. : (count
  398. ? tr::lng_context_seen_text(tr::now, lt_count, count)
  399. : tr::lng_context_seen_text_none(tr::now))) },
  400. MenuTextOptions);
  401. }
  402. void Action::refreshDimensions() {
  403. if (!minWidth()) {
  404. return;
  405. }
  406. const auto textWidth = _text.maxWidth();
  407. const auto &padding = st::defaultWhoRead.itemPadding;
  408. const auto goodWidth = padding.left()
  409. + textWidth
  410. + (_userpicsWidth ? (_st.itemStyle.font->spacew + _userpicsWidth) : 0)
  411. + padding.right();
  412. const auto w = std::clamp(
  413. goodWidth,
  414. _st.widthMin,
  415. std::max(minWidth(), _st.widthMin));
  416. _textWidth = w - (goodWidth - textWidth);
  417. }
  418. bool Action::isEnabled() const {
  419. return !_content.participants.empty()
  420. || (_content.state == WhoReadState::MyHidden);
  421. }
  422. not_null<QAction*> Action::action() const {
  423. return _dummyAction;
  424. }
  425. QPoint Action::prepareRippleStartPosition() const {
  426. return mapFromGlobal(QCursor::pos());
  427. }
  428. QImage Action::prepareRippleMask() const {
  429. return Ui::RippleAnimation::RectMask(size());
  430. }
  431. int Action::contentHeight() const {
  432. return _height;
  433. }
  434. void Action::handleKeyPress(not_null<QKeyEvent*> e) {
  435. if (!isSelected()) {
  436. return;
  437. }
  438. const auto key = e->key();
  439. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  440. setClicked(Menu::TriggeredSource::Keyboard);
  441. }
  442. }
  443. WhenAction::WhenAction(
  444. not_null<PopupMenu*> parentMenu,
  445. rpl::producer<WhoReadContent> content,
  446. Fn<void()> showOrPremium)
  447. : ItemBase(parentMenu->menu(), parentMenu->menu()->st())
  448. , _parentMenu(parentMenu)
  449. , _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
  450. , _showOrPremium(std::move(showOrPremium))
  451. , _st(parentMenu->menu()->st())
  452. , _height(st::whenReadPadding.top()
  453. + st::whenReadStyle.font->height
  454. + st::whenReadPadding.bottom()) {
  455. const auto parent = parentMenu->menu();
  456. setAcceptBoth(true);
  457. initResizeHook(parent->sizeValue());
  458. std::move(
  459. content
  460. ) | rpl::start_with_next([=](WhoReadContent &&content) {
  461. _content = content;
  462. refreshText();
  463. refreshDimensions();
  464. setPointerCursor(isEnabled());
  465. _dummyAction->setEnabled(isEnabled());
  466. if (!isEnabled()) {
  467. setSelected(false);
  468. }
  469. update();
  470. }, lifetime());
  471. resolveMinWidth();
  472. refreshDimensions();
  473. paintRequest(
  474. ) | rpl::start_with_next([=] {
  475. Painter p(this);
  476. paint(p);
  477. }, lifetime());
  478. clicks(
  479. ) | rpl::start_with_next([=] {
  480. if (_content.state == WhoReadState::MyHidden) {
  481. if (const auto onstack = _showOrPremium) {
  482. onstack();
  483. }
  484. }
  485. }, lifetime());
  486. enableMouseSelecting();
  487. }
  488. void WhenAction::resolveMinWidth() {
  489. const auto width = [&](const QString &text) {
  490. return st::whenReadStyle.font->width(text);
  491. };
  492. const auto added = st::whenReadShowPadding.left()
  493. + st::whenReadShowPadding.right();
  494. const auto sampleDate = QDate::currentDate();
  495. const auto sampleTime = QLocale().toString(
  496. QTime::currentTime(),
  497. QLocale::ShortFormat);
  498. const auto maxTextWidth = added + std::max({
  499. width(tr::lng_contacts_loading(tr::now)),
  500. (width(tr::lng_context_read_hidden(tr::now))
  501. + st::whenReadSkip
  502. + width(tr::lng_context_read_show(tr::now))),
  503. width(tr::lng_mediaview_today(tr::now, lt_time, sampleTime)),
  504. width(tr::lng_mediaview_yesterday(tr::now, lt_time, sampleTime)),
  505. width(tr::lng_mediaview_date_time(
  506. tr::now,
  507. lt_date,
  508. tr::lng_month_day(
  509. tr::now,
  510. lt_month,
  511. Lang::MonthDay(sampleDate.month())(tr::now),
  512. lt_day,
  513. QString::number(sampleDate.day())),
  514. lt_time,
  515. sampleTime)),
  516. });
  517. const auto maxWidth = st::whenReadPadding.left()
  518. + maxTextWidth
  519. + st::whenReadPadding.right();
  520. setMinWidth(maxWidth);
  521. }
  522. void WhenAction::paint(Painter &p) {
  523. const auto loading = !isEnabled() && _content.participants.empty();
  524. const auto selected = isSelected();
  525. if (selected && _st.itemBgOver->c.alpha() < 255) {
  526. p.fillRect(0, 0, width(), _height, _st.itemBg);
  527. }
  528. p.fillRect(0, 0, width(), _height, _st.itemBg);
  529. const auto &icon = (_content.type == WhoReadType::Edited)
  530. ? (selected ? st::whenEditedOver : st::whenEdited)
  531. : (_content.type == WhoReadType::Original)
  532. ? (selected ? st::whenOriginalOver : st::whenOriginal)
  533. : loading
  534. ? st::whoReadChecksDisabled
  535. : selected
  536. ? st::whoReadChecksOver
  537. : st::whoReadChecks;
  538. icon.paint(p, st::whenReadIconPosition, width());
  539. p.setPen(loading ? _st.itemFgDisabled : _st.itemFg);
  540. _text.drawLeftElided(
  541. p,
  542. st::whenReadPadding.left(),
  543. st::whenReadPadding.top(),
  544. _textWidth,
  545. width());
  546. if (!_show.isEmpty()) {
  547. auto hq = PainterHighQualityEnabler(p);
  548. p.setPen(Qt::NoPen);
  549. p.setBrush(_st.itemBgOver);
  550. const auto radius = _showRect.height() / 2.;
  551. p.drawRoundedRect(_showRect, radius, radius);
  552. paintRipple(p, 0, 0);
  553. const auto inner = _showRect.marginsRemoved(st::whenReadShowPadding);
  554. p.setPen(_st.itemFgOver);
  555. _show.drawLeftElided(
  556. p,
  557. inner.x(),
  558. inner.y(),
  559. inner.width(),
  560. width());
  561. }
  562. }
  563. void WhenAction::refreshText() {
  564. _text.setMarkedText(
  565. st::whenReadStyle,
  566. { ((_content.state == WhoReadState::Unknown)
  567. ? tr::lng_context_seen_loading(tr::now)
  568. : _content.participants.empty()
  569. ? tr::lng_context_read_hidden(tr::now)
  570. : _content.participants.front().date) },
  571. MenuTextOptions);
  572. if (_content.state == WhoReadState::MyHidden) {
  573. _show.setMarkedText(
  574. st::whenReadStyle,
  575. { tr::lng_context_read_show(tr::now) },
  576. MenuTextOptions);
  577. } else {
  578. _show = Text::String();
  579. }
  580. }
  581. void WhenAction::resizeEvent(QResizeEvent *e) {
  582. ItemBase::resizeEvent(e);
  583. refreshDimensions();
  584. }
  585. void WhenAction::refreshDimensions() {
  586. if (!minWidth()) {
  587. return;
  588. }
  589. const auto textWidth = _text.maxWidth();
  590. const auto showWidth = _show.isEmpty() ? 0 : _show.maxWidth();
  591. const auto &padding = st::whenReadPadding;
  592. const auto goodWidth = padding.left()
  593. + textWidth
  594. + (showWidth
  595. ? (st::whenReadSkip
  596. + st::whenReadShowPadding.left()
  597. + showWidth
  598. + st::whenReadShowPadding.right())
  599. : 0)
  600. + padding.right();
  601. const auto w = std::clamp(
  602. goodWidth,
  603. _st.widthMin,
  604. std::max(width(), _st.widthMin));
  605. _textWidth = std::min(w - (goodWidth - textWidth), textWidth);
  606. if (showWidth) {
  607. _showRect = QRect(
  608. padding.left() + _textWidth + st::whenReadSkip,
  609. padding.top() - st::whenReadShowPadding.top(),
  610. (st::whenReadShowPadding.left()
  611. + showWidth
  612. + st::whenReadShowPadding.right()),
  613. (st::whenReadShowPadding.top()
  614. + st::whenReadStyle.font->height
  615. + st::whenReadShowPadding.bottom()));
  616. }
  617. }
  618. bool WhenAction::isEnabled() const {
  619. return (_content.state == WhoReadState::MyHidden);
  620. }
  621. not_null<QAction*> WhenAction::action() const {
  622. return _dummyAction;
  623. }
  624. QPoint WhenAction::prepareRippleStartPosition() const {
  625. const auto result = mapFromGlobal(QCursor::pos());
  626. return _showRect.contains(result)
  627. ? result
  628. : Ui::RippleButton::DisabledRippleStartPosition();
  629. }
  630. QImage WhenAction::prepareRippleMask() const {
  631. return Ui::RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) {
  632. const auto radius = _showRect.height() / 2.;
  633. p.drawRoundedRect(_showRect, radius, radius);
  634. });
  635. }
  636. int WhenAction::contentHeight() const {
  637. return _height;
  638. }
  639. } // namespace
  640. WhoReactedEntryAction::WhoReactedEntryAction(
  641. not_null<RpWidget*> parent,
  642. CustomEmojiFactory customEmojiFactory,
  643. const style::Menu &st,
  644. Data &&data)
  645. : ItemBase(parent, st)
  646. , _dummyAction(CreateChild<QAction>(parent.get()))
  647. , _customEmojiFactory(std::move(customEmojiFactory))
  648. , _st(st)
  649. , _height(st::defaultWhoRead.photoSkip * 2 + st::defaultWhoRead.photoSize) {
  650. setAcceptBoth(true);
  651. initResizeHook(parent->sizeValue());
  652. setData(std::move(data));
  653. paintRequest(
  654. ) | rpl::start_with_next([=] {
  655. paint(Painter(this));
  656. }, lifetime());
  657. enableMouseSelecting();
  658. }
  659. not_null<QAction*> WhoReactedEntryAction::action() const {
  660. return _dummyAction.get();
  661. }
  662. bool WhoReactedEntryAction::isEnabled() const {
  663. return true;
  664. }
  665. int WhoReactedEntryAction::contentHeight() const {
  666. return _height;
  667. }
  668. void WhoReactedEntryAction::setData(Data &&data) {
  669. setClickedCallback(std::move(data.callback));
  670. _userpic = std::move(data.userpic);
  671. _text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);
  672. if (data.date.isEmpty()) {
  673. _date = Text::String();
  674. } else {
  675. _date.setMarkedText(
  676. st::whoReadDateStyle,
  677. { data.date },
  678. MenuTextOptions);
  679. }
  680. _type = data.type;
  681. _custom = _customEmojiFactory
  682. ? _customEmojiFactory(
  683. data.customEntityData,
  684. { .repaint = [=] { update(); } })
  685. : nullptr;
  686. const auto ratio = style::DevicePixelRatio();
  687. const auto size = Emoji::GetSizeNormal() / ratio;
  688. _customSize = Text::AdjustCustomEmojiSize(size);
  689. const auto textWidth = std::max(
  690. _text.maxWidth(),
  691. st::whoReadDateSkip + _date.maxWidth());
  692. const auto &padding = _st.itemPadding;
  693. const auto rightSkip = padding.right()
  694. + (_custom ? (size + padding.right()) : 0);
  695. const auto goodWidth = st::defaultWhoRead.nameLeft
  696. + textWidth
  697. + rightSkip;
  698. const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);
  699. _textWidth = w - (goodWidth - textWidth);
  700. setMinWidth(w);
  701. update();
  702. }
  703. void WhoReactedEntryAction::paint(Painter &&p) {
  704. const auto enabled = isEnabled();
  705. const auto selected = isSelected();
  706. if (selected && _st.itemBgOver->c.alpha() < 255) {
  707. p.fillRect(0, 0, width(), _height, _st.itemBg);
  708. }
  709. const auto bg = selected ? _st.itemBgOver : _st.itemBg;
  710. p.fillRect(0, 0, width(), _height, bg);
  711. if (enabled) {
  712. paintRipple(p, 0, 0);
  713. }
  714. const auto photoSize = st::defaultWhoRead.photoSize;
  715. const auto photoLeft = st::defaultWhoRead.photoLeft;
  716. const auto photoTop = (height() - photoSize) / 2;
  717. const auto preloader = (_type == WhoReactedType::Preloader);
  718. const auto preloaderBrush = preloader
  719. ? [&] {
  720. auto color = _st.itemFg->c;
  721. color.setAlphaF(color.alphaF() * kPreloaderAlpha);
  722. return QBrush(color);
  723. }() : QBrush();
  724. if (preloader) {
  725. auto hq = PainterHighQualityEnabler(p);
  726. p.setPen(Qt::NoPen);
  727. p.setBrush(preloaderBrush);
  728. p.drawEllipse(photoLeft, photoTop, photoSize, photoSize);
  729. } else if (!_userpic.isNull()) {
  730. p.drawImage(photoLeft, photoTop, _userpic);
  731. if (_type == WhoReactedType::RefRecipientNow) {
  732. auto hq = PainterHighQualityEnabler(p);
  733. p.setBrush(Qt::NoBrush);
  734. auto bgPen = bg->p;
  735. bgPen.setWidthF(st::lineWidth * 6.);
  736. p.setPen(bgPen);
  737. p.drawEllipse(photoLeft, photoTop, photoSize, photoSize);
  738. auto fgPen = st::windowBgActive->p;
  739. fgPen.setWidthF(st::lineWidth * 2.);
  740. p.setPen(fgPen);
  741. p.drawEllipse(photoLeft, photoTop, photoSize, photoSize);
  742. }
  743. } else if (!_custom) {
  744. st::menuIconReactions.paintInCenter(
  745. p,
  746. QRect(photoLeft, photoTop, photoSize, photoSize));
  747. }
  748. const auto withDate = !_date.isEmpty();
  749. const auto textTop = withDate
  750. ? st::whoReadNameWithDateTop
  751. : (height() - _st.itemStyle.font->height) / 2;
  752. if (_type == WhoReactedType::Preloader) {
  753. auto hq = PainterHighQualityEnabler(p);
  754. p.setPen(Qt::NoPen);
  755. p.setBrush(preloaderBrush);
  756. const auto height = _st.itemStyle.font->height / 2;
  757. p.drawRoundedRect(
  758. st::defaultWhoRead.nameLeft,
  759. textTop + (_st.itemStyle.font->height - height) / 2,
  760. _textWidth,
  761. height,
  762. height / 2.,
  763. height / 2.);
  764. } else {
  765. p.setPen(selected
  766. ? _st.itemFgOver
  767. : enabled
  768. ? _st.itemFg
  769. : _st.itemFgDisabled);
  770. _text.drawLeftElided(
  771. p,
  772. st::defaultWhoRead.nameLeft,
  773. textTop,
  774. _textWidth,
  775. width());
  776. }
  777. if (_type == WhoReactedType::RefRecipient
  778. || _type == WhoReactedType::RefRecipientNow) {
  779. p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
  780. _date.drawLeftElided(
  781. p,
  782. st::defaultWhoRead.nameLeft,
  783. st::whoReadDateTop,
  784. _textWidth,
  785. width());
  786. } else if (withDate) {
  787. const auto iconPosition = QPoint(
  788. st::defaultWhoRead.nameLeft,
  789. st::whoReadDateTop) + st::whoReadDateChecksPosition;
  790. const auto icon = [&] {
  791. switch (_type) {
  792. case WhoReactedType::Viewed:
  793. return &(selected
  794. ? st::whoReadDateChecksOver
  795. : st::whoReadDateChecks);
  796. case WhoReactedType::Reacted:
  797. return &(selected
  798. ? st::whoLikedDateHeartOver
  799. : st::whoLikedDateHeart);
  800. case WhoReactedType::Reposted:
  801. return &(selected
  802. ? st::whoRepostedDateHeartOver
  803. : st::whoRepostedDateHeart);
  804. case WhoReactedType::Forwarded:
  805. return &(selected
  806. ? st::whoForwardedDateHeartOver
  807. : st::whoForwardedDateHeart);
  808. }
  809. Unexpected("Type in WhoReactedEntryAction::paint.");
  810. }();
  811. icon->paint(p, iconPosition, width());
  812. p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
  813. _date.drawLeftElided(
  814. p,
  815. st::defaultWhoRead.nameLeft + st::whoReadDateSkip,
  816. st::whoReadDateTop,
  817. _textWidth - st::whoReadDateSkip,
  818. width());
  819. }
  820. if (_custom) {
  821. const auto ratio = style::DevicePixelRatio();
  822. const auto size = Emoji::GetSizeNormal() / ratio;
  823. const auto skip = (size - _customSize) / 2;
  824. _custom->paint(p, {
  825. .textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,
  826. .now = crl::now(),
  827. .position = QPoint(
  828. width() - _st.itemPadding.right() - size + skip,
  829. (height() - _customSize) / 2),
  830. });
  831. }
  832. }
  833. bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {
  834. return (a.id == b.id)
  835. && (a.name == b.name)
  836. && (a.date == b.date)
  837. && (a.userpicKey == b.userpicKey);
  838. }
  839. bool operator!=(const WhoReadParticipant &a, const WhoReadParticipant &b) {
  840. return !(a == b);
  841. }
  842. base::unique_qptr<Menu::ItemBase> WhoReactedContextAction(
  843. not_null<PopupMenu*> menu,
  844. rpl::producer<WhoReadContent> content,
  845. CustomEmojiFactory factory,
  846. Fn<void(WhoReadParticipant)> participantChosen,
  847. Fn<void()> showAllChosen) {
  848. return base::make_unique_q<Action>(
  849. menu,
  850. std::move(content),
  851. std::move(factory),
  852. std::move(participantChosen),
  853. std::move(showAllChosen));
  854. }
  855. base::unique_qptr<Menu::ItemBase> WhenReadContextAction(
  856. not_null<PopupMenu*> menu,
  857. rpl::producer<WhoReadContent> content,
  858. Fn<void()> showOrPremium) {
  859. return base::make_unique_q<WhenAction>(
  860. menu,
  861. std::move(content),
  862. std::move(showOrPremium));
  863. }
  864. WhoReactedListMenu::WhoReactedListMenu(
  865. CustomEmojiFactory factory,
  866. Fn<void(WhoReadParticipant)> participantChosen,
  867. Fn<void()> showAllChosen)
  868. : _customEmojiFactory(std::move(factory))
  869. , _participantChosen(std::move(participantChosen))
  870. , _showAllChosen(std::move(showAllChosen)) {
  871. }
  872. void WhoReactedListMenu::clear() {
  873. _actions.clear();
  874. }
  875. void WhoReactedListMenu::populate(
  876. not_null<PopupMenu*> menu,
  877. const WhoReadContent &content,
  878. Fn<void()> refillTopActions,
  879. int addedToBottom,
  880. Fn<void()> appendBottomActions) {
  881. const auto reactions = ranges::count_if(
  882. content.participants,
  883. [](const auto &p) { return !p.customEntityData.isEmpty(); });
  884. const auto addShowAll = (content.fullReactionsCount > reactions);
  885. const auto actionsCount = int(content.participants.size())
  886. + (addShowAll ? 1 : 0);
  887. if (_actions.size() > actionsCount) {
  888. _actions.clear();
  889. menu->clearActions();
  890. if (refillTopActions) {
  891. refillTopActions();
  892. }
  893. addedToBottom = 0;
  894. }
  895. auto index = 0;
  896. const auto append = [&](WhoReactedEntryData &&data) {
  897. if (index < _actions.size()) {
  898. _actions[index]->setData(std::move(data));
  899. } else {
  900. auto item = base::make_unique_q<WhoReactedEntryAction>(
  901. menu->menu(),
  902. _customEmojiFactory,
  903. menu->menu()->st(),
  904. std::move(data));
  905. _actions.push_back(item.get());
  906. const auto count = int(menu->actions().size());
  907. if (addedToBottom > 0 && addedToBottom <= count) {
  908. menu->insertAction(count - addedToBottom, std::move(item));
  909. } else {
  910. menu->addAction(std::move(item));
  911. }
  912. }
  913. ++index;
  914. };
  915. for (const auto &participant : content.participants) {
  916. const auto chosen = [call = _participantChosen, participant] {
  917. call(participant);
  918. };
  919. append({
  920. .text = participant.name,
  921. .date = participant.date,
  922. .type = (participant.dateReacted
  923. ? WhoReactedType::Reacted
  924. : WhoReactedType::Viewed),
  925. .customEntityData = participant.customEntityData,
  926. .userpic = participant.userpicLarge,
  927. .callback = chosen,
  928. });
  929. }
  930. if (addShowAll) {
  931. append({
  932. .text = tr::lng_context_seen_reacted_all(tr::now),
  933. .callback = _showAllChosen,
  934. });
  935. }
  936. if (!addedToBottom && appendBottomActions) {
  937. appendBottomActions();
  938. }
  939. }
  940. } // namespace Ui