menu_send.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  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_send.h"
  8. #include "api/api_common.h"
  9. #include "base/event_filter.h"
  10. #include "base/unixtime.h"
  11. #include "boxes/abstract_box.h"
  12. #include "boxes/premium_preview_box.h"
  13. #include "chat_helpers/compose/compose_show.h"
  14. #include "chat_helpers/stickers_emoji_pack.h"
  15. #include "core/shortcuts.h"
  16. #include "history/admin_log/history_admin_log_item.h"
  17. #include "history/view/media/history_view_sticker.h"
  18. #include "history/view/reactions/history_view_reactions_selector.h"
  19. #include "history/view/history_view_element.h"
  20. #include "history/view/history_view_fake_items.h"
  21. #include "history/view/history_view_schedule_box.h"
  22. #include "history/history.h"
  23. #include "history/history_item.h"
  24. #include "history/history_unread_things.h"
  25. #include "lang/lang_keys.h"
  26. #include "lottie/lottie_single_player.h"
  27. #include "ui/chat/chat_style.h"
  28. #include "ui/chat/chat_theme.h"
  29. #include "ui/effects/path_shift_gradient.h"
  30. #include "ui/effects/radial_animation.h"
  31. #include "ui/effects/ripple_animation.h"
  32. #include "ui/text/text_utilities.h"
  33. #include "ui/widgets/buttons.h"
  34. #include "ui/widgets/labels.h"
  35. #include "ui/widgets/popup_menu.h"
  36. #include "ui/widgets/shadow.h"
  37. #include "ui/wrap/padding_wrap.h"
  38. #include "ui/painter.h"
  39. #include "ui/ui_utility.h"
  40. #include "data/data_document.h"
  41. #include "data/data_document_media.h"
  42. #include "data/data_peer.h"
  43. #include "data/data_forum.h"
  44. #include "data/data_forum_topic.h"
  45. #include "data/data_message_reactions.h"
  46. #include "data/data_session.h"
  47. #include "main/main_session.h"
  48. #include "apiwrap.h"
  49. #include "settings/settings_premium.h"
  50. #include "window/themes/window_theme.h"
  51. #include "window/section_widget.h"
  52. #include "styles/style_chat.h"
  53. #include "styles/style_chat_helpers.h"
  54. #include "styles/style_menu_icons.h"
  55. #include "styles/style_window.h"
  56. #include <QtWidgets/QApplication>
  57. namespace SendMenu {
  58. namespace {
  59. constexpr auto kToggleDuration = crl::time(400);
  60. class Delegate final : public HistoryView::DefaultElementDelegate {
  61. public:
  62. Delegate(not_null<Ui::PathShiftGradient*> pathGradient)
  63. : _pathGradient(pathGradient) {
  64. }
  65. private:
  66. bool elementAnimationsPaused() override {
  67. return false;
  68. }
  69. not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {
  70. return _pathGradient;
  71. }
  72. HistoryView::Context elementContext() override {
  73. return HistoryView::Context::ContactPreview;
  74. }
  75. const not_null<Ui::PathShiftGradient*> _pathGradient;
  76. };
  77. class EffectPreview final : public Ui::RpWidget {
  78. public:
  79. EffectPreview(
  80. not_null<QWidget*> parent,
  81. std::shared_ptr<ChatHelpers::Show> show,
  82. Details details,
  83. QPoint position,
  84. const Data::Reaction &effect,
  85. Fn<void(Action, Details)> action,
  86. Fn<void()> done);
  87. void hideAnimated();
  88. private:
  89. void paintEvent(QPaintEvent *e) override;
  90. void mousePressEvent(QMouseEvent *e) override;
  91. [[nodiscard]] bool canSend() const;
  92. void setupGeometry(QPoint position);
  93. void setupBackground();
  94. void setupItem();
  95. void repaintBackground();
  96. void setupLottie();
  97. void setupSend(Details details);
  98. void createLottie();
  99. [[nodiscard]] bool ready() const;
  100. void paintLoading(QPainter &p);
  101. void paintLottie(QPainter &p);
  102. bool checkIconBecameLoaded();
  103. [[nodiscard]] bool checkLoaded();
  104. void toggle(bool shown);
  105. const EffectId _effectId = 0;
  106. const Data::Reaction _effect;
  107. const std::shared_ptr<ChatHelpers::Show> _show;
  108. const std::shared_ptr<Ui::ChatTheme> _theme;
  109. const std::unique_ptr<Ui::ChatStyle> _chatStyle;
  110. const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
  111. const std::unique_ptr<Delegate> _delegate;
  112. const not_null<History*> _history;
  113. const AdminLog::OwnedItem _replyTo;
  114. const AdminLog::OwnedItem _item;
  115. const std::unique_ptr<Ui::FlatButton> _send;
  116. const std::unique_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _premiumPromoLabel;
  117. const not_null<Ui::RpWidget*> _bottom;
  118. const Fn<void()> _close;
  119. const Fn<void(Action, Details)> _actionWithEffect;
  120. QImage _icon;
  121. std::shared_ptr<Data::DocumentMedia> _media;
  122. QByteArray _bytes;
  123. QString _filepath;
  124. std::unique_ptr<Lottie::SinglePlayer> _lottie;
  125. QRect _inner;
  126. QImage _bg;
  127. QPoint _itemShift;
  128. QRect _iconRect;
  129. std::unique_ptr<Ui::InfiniteRadialAnimation> _loading;
  130. Ui::Animations::Simple _shownAnimation;
  131. QPixmap _bottomCache;
  132. bool _hiding = false;
  133. rpl::lifetime _readyCheckLifetime;
  134. };
  135. class BottomRounded final : public Ui::FlatButton {
  136. public:
  137. using FlatButton::FlatButton;
  138. private:
  139. QImage prepareRippleMask() const override;
  140. void paintEvent(QPaintEvent *e) override;
  141. };
  142. QImage BottomRounded::prepareRippleMask() const {
  143. const auto fill = false;
  144. return Ui::RippleAnimation::MaskByDrawer(size(), fill, [&](QPainter &p) {
  145. const auto radius = st::previewMenu.radius;
  146. const auto expanded = rect().marginsAdded({ 0, 2 * radius, 0, 0 });
  147. p.drawRoundedRect(expanded, radius, radius);
  148. });
  149. }
  150. void BottomRounded::paintEvent(QPaintEvent *e) {
  151. auto p = QPainter(this);
  152. auto hq = PainterHighQualityEnabler(p);
  153. const auto radius = st::previewMenu.radius;
  154. const auto expanded = rect().marginsAdded({ 0, 2 * radius, 0, 0 });
  155. p.setPen(Qt::NoPen);
  156. const auto &st = st::previewMarkRead;
  157. if (isOver()) {
  158. p.setBrush(st.overBgColor);
  159. }
  160. p.drawRoundedRect(expanded, radius, radius);
  161. p.end();
  162. Ui::FlatButton::paintEvent(e);
  163. }
  164. [[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleEffects(
  165. not_null<Main::Session*> session) {
  166. auto result = Data::PossibleItemReactionsRef();
  167. const auto reactions = &session->data().reactions();
  168. const auto &effects = reactions->list(Data::Reactions::Type::Effects);
  169. const auto premiumPossible = session->premiumPossible();
  170. auto added = base::flat_set<Data::ReactionId>();
  171. result.recent.reserve(effects.size());
  172. result.stickers.reserve(effects.size());
  173. for (const auto &reaction : effects) {
  174. if (premiumPossible || !reaction.premium) {
  175. if (added.emplace(reaction.id).second) {
  176. if (reaction.aroundAnimation) {
  177. result.recent.push_back(&reaction);
  178. } else {
  179. result.stickers.push_back(&reaction);
  180. }
  181. }
  182. }
  183. }
  184. return result;
  185. }
  186. [[nodiscard]] Fn<void(Action, Details)> ComposeActionWithEffect(
  187. Fn<void(Action, Details)> sendAction,
  188. EffectId id,
  189. Fn<void()> done) {
  190. return [=](Action action, Details details) {
  191. action.options.effectId = id;
  192. const auto onstack = done;
  193. sendAction(action, details);
  194. if (onstack) {
  195. onstack();
  196. }
  197. };
  198. }
  199. EffectPreview::EffectPreview(
  200. not_null<QWidget*> parent,
  201. std::shared_ptr<ChatHelpers::Show> show,
  202. Details details,
  203. QPoint position,
  204. const Data::Reaction &effect,
  205. Fn<void(Action, Details)> action,
  206. Fn<void()> done)
  207. : RpWidget(parent)
  208. , _effectId(effect.id.custom())
  209. , _effect(effect)
  210. , _show(show)
  211. , _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
  212. , _chatStyle(
  213. std::make_unique<Ui::ChatStyle>(
  214. _show->session().colorIndicesValue()))
  215. , _pathGradient(
  216. HistoryView::MakePathShiftGradient(_chatStyle.get(), [=] { update(); }))
  217. , _delegate(std::make_unique<Delegate>(_pathGradient.get()))
  218. , _history(show->session().data().history(
  219. PeerData::kServiceNotificationsId))
  220. , _replyTo(HistoryView::GenerateItem(
  221. _delegate.get(),
  222. _history,
  223. HistoryView::GenerateUser(
  224. _history,
  225. tr::lng_settings_chat_message_reply_from(tr::now)),
  226. FullMsgId(),
  227. tr::lng_settings_chat_message(tr::now)))
  228. , _item(HistoryView::GenerateItem(
  229. _delegate.get(),
  230. _history,
  231. _history->peer->id,
  232. _replyTo->data()->fullId(),
  233. tr::lng_settings_chat_message_reply(tr::now),
  234. Data::Reactions::kFakeEffectId))
  235. , _send(canSend()
  236. ? std::make_unique<BottomRounded>(
  237. this,
  238. tr::lng_effect_send(tr::now),
  239. st::effectPreviewSend)
  240. : nullptr)
  241. , _premiumPromoLabel(canSend()
  242. ? nullptr
  243. : std::make_unique<Ui::PaddingWrap<Ui::FlatLabel>>(
  244. this,
  245. object_ptr<Ui::FlatLabel>(
  246. this,
  247. tr::lng_effect_premium(
  248. lt_link,
  249. tr::lng_effect_premium_link() | Ui::Text::ToLink(),
  250. Ui::Text::WithEntities),
  251. st::effectPreviewPromoLabel),
  252. st::effectPreviewPromoPadding))
  253. , _bottom(_send ? ((Ui::RpWidget*)_send.get()) : _premiumPromoLabel.get())
  254. , _close(done)
  255. , _actionWithEffect(ComposeActionWithEffect(action, _effectId, done)) {
  256. _chatStyle->apply(_theme.get());
  257. setupGeometry(position);
  258. setupItem();
  259. setupBackground();
  260. setupLottie();
  261. setupSend(details);
  262. toggle(true);
  263. }
  264. void EffectPreview::paintEvent(QPaintEvent *e) {
  265. checkIconBecameLoaded();
  266. const auto progress = _shownAnimation.value(_hiding ? 0. : 1.);
  267. if (!progress) {
  268. return;
  269. }
  270. auto p = QPainter(this);
  271. p.setOpacity(progress);
  272. p.drawImage(0, 0, _bg);
  273. if (!_bottomCache.isNull()) {
  274. p.drawPixmap(_bottom->pos(), _bottomCache);
  275. }
  276. if (!ready()) {
  277. paintLoading(p);
  278. } else {
  279. _loading = nullptr;
  280. p.drawImage(_iconRect, _icon);
  281. if (!_hiding) {
  282. p.setOpacity(1.);
  283. }
  284. paintLottie(p);
  285. }
  286. }
  287. bool EffectPreview::ready() const {
  288. return !_icon.isNull() && _lottie && _lottie->ready();
  289. }
  290. void EffectPreview::paintLoading(QPainter &p) {
  291. if (!_loading) {
  292. _loading = std::make_unique<Ui::InfiniteRadialAnimation>([=] {
  293. update();
  294. }, st::effectPreviewLoading);
  295. _loading->start(st::defaultInfiniteRadialAnimation.linearPeriod);
  296. }
  297. const auto loading = _iconRect.marginsRemoved(
  298. { st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth });
  299. auto hq = PainterHighQualityEnabler(p);
  300. Ui::InfiniteRadialAnimation::Draw(
  301. p,
  302. _loading->computeState(),
  303. loading.topLeft(),
  304. loading.size(),
  305. width(),
  306. _chatStyle->msgInDateFg(),
  307. st::effectPreviewLoading.thickness);
  308. }
  309. void EffectPreview::paintLottie(QPainter &p) {
  310. const auto factor = style::DevicePixelRatio();
  311. auto request = Lottie::FrameRequest();
  312. request.box = _inner.size() * factor;
  313. const auto rightAligned = _item->hasRightLayout();
  314. if (!rightAligned) {
  315. request.mirrorHorizontal = true;
  316. }
  317. const auto frame = _lottie->frameInfo(request);
  318. p.drawImage(
  319. QRect(_inner.topLeft(), frame.image.size() / factor),
  320. frame.image);
  321. _lottie->markFrameShown();
  322. }
  323. void EffectPreview::hideAnimated() {
  324. toggle(false);
  325. }
  326. void EffectPreview::mousePressEvent(QMouseEvent *e) {
  327. hideAnimated();
  328. }
  329. void EffectPreview::setupGeometry(QPoint position) {
  330. const auto parent = parentWidget();
  331. const auto innerSize = HistoryView::Sticker::MessageEffectSize();
  332. const auto shadow = st::previewMenu.shadow;
  333. const auto extend = shadow.extend;
  334. _inner = QRect(QPoint(extend.left(), extend.top()), innerSize);
  335. _bottom->resizeToWidth(_inner.width());
  336. const auto size = _inner.marginsAdded(extend).size()
  337. + QSize(0, _bottom->height());
  338. const auto left = std::max(
  339. std::min(
  340. position.x() - size.width() / 2,
  341. parent->width() - size.width()),
  342. 0);
  343. const auto topMin = std::min((parent->height() - size.height()) / 2, 0);
  344. const auto top = std::max(
  345. std::min(
  346. position.y() - size.height() / 2,
  347. parent->height() - size.height()),
  348. topMin);
  349. setGeometry(left, top, size.width(), size.height());
  350. _bottom->setGeometry(
  351. _inner.x(),
  352. _inner.y() + _inner.height(),
  353. _inner.width(),
  354. _bottom->height());
  355. }
  356. void EffectPreview::setupBackground() {
  357. const auto ratio = style::DevicePixelRatio();
  358. _bg = QImage(
  359. size() * ratio,
  360. QImage::Format_ARGB32_Premultiplied);
  361. _bg.setDevicePixelRatio(ratio);
  362. repaintBackground();
  363. _theme->repaintBackgroundRequests() | rpl::start_with_next([=] {
  364. repaintBackground();
  365. update();
  366. }, lifetime());
  367. }
  368. void EffectPreview::setupItem() {
  369. _item->resizeGetHeight(st::windowMinWidth);
  370. const auto icon = _item->effectIconGeometry();
  371. Assert(!icon.isEmpty());
  372. const auto size = _inner.size();
  373. const auto shift = _item->hasRightLayout()
  374. ? (-size.width() / 3)
  375. : (size.width() / 3);
  376. const auto position = QPoint(
  377. shift + icon.x() + (icon.width() - size.width()) / 2,
  378. icon.y() + (icon.height() - size.height()) / 2);
  379. _itemShift = _inner.topLeft() - position;
  380. _iconRect = icon.translated(_itemShift);
  381. }
  382. void EffectPreview::repaintBackground() {
  383. const auto ratio = style::DevicePixelRatio();
  384. const auto inner = _inner.size() + QSize(0, _bottom->height());
  385. auto bg = QImage(
  386. inner * ratio,
  387. QImage::Format_ARGB32_Premultiplied);
  388. bg.setDevicePixelRatio(ratio);
  389. {
  390. auto p = Painter(&bg);
  391. Window::SectionWidget::PaintBackground(
  392. p,
  393. _theme.get(),
  394. QSize(inner.width(), inner.height() * 5),
  395. QRect(QPoint(), inner));
  396. p.fillRect(
  397. QRect(0, _inner.height(), _inner.width(), _bottom->height()),
  398. st::previewMarkRead.bgColor);
  399. p.translate(_itemShift - _inner.topLeft());
  400. auto rect = QRect(0, 0, st::windowMinWidth, _inner.height());
  401. auto context = _theme->preparePaintContext(
  402. _chatStyle.get(),
  403. rect,
  404. rect,
  405. false);
  406. context.outbg = _item->hasOutLayout();
  407. _item->draw(p, context);
  408. p.translate(_inner.topLeft() - _itemShift);
  409. auto hq = PainterHighQualityEnabler(p);
  410. p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
  411. auto roundRect = Ui::RoundRect(st::previewMenu.radius, st::menuBg);
  412. roundRect.paint(p, QRect(QPoint(), inner), RectPart::AllCorners);
  413. }
  414. _bg.fill(Qt::transparent);
  415. auto p = QPainter(&_bg);
  416. const auto &shadow = st::previewMenu.animation.shadow;
  417. const auto shadowed = QRect(_inner.topLeft(), inner);
  418. Ui::Shadow::paint(p, shadowed, width(), shadow);
  419. p.drawImage(_inner.topLeft(), bg);
  420. }
  421. void EffectPreview::setupLottie() {
  422. const auto reactions = &_show->session().data().reactions();
  423. reactions->preloadEffectImageFor(_effectId);
  424. if (const auto document = _effect.aroundAnimation) {
  425. _media = document->createMediaView();
  426. } else {
  427. _media = _effect.selectAnimation->createMediaView();
  428. }
  429. rpl::single(rpl::empty) | rpl::then(
  430. _show->session().downloaderTaskFinished()
  431. ) | rpl::start_with_next([=] {
  432. if (checkLoaded()) {
  433. _readyCheckLifetime.destroy();
  434. createLottie();
  435. }
  436. }, _readyCheckLifetime);
  437. }
  438. void EffectPreview::createLottie() {
  439. _lottie = _show->session().emojiStickersPack().effectPlayer(
  440. _media->owner(),
  441. _bytes,
  442. _filepath,
  443. Stickers::EffectType::MessageEffect);
  444. const auto raw = _lottie.get();
  445. raw->updates(
  446. ) | rpl::start_with_next([=](Lottie::Update update) {
  447. v::match(update.data, [&](const Lottie::Information &information) {
  448. }, [&](const Lottie::DisplayFrameRequest &request) {
  449. this->update();
  450. });
  451. }, raw->lifetime());
  452. }
  453. bool EffectPreview::canSend() const {
  454. return !_effect.premium || _show->session().premium();
  455. }
  456. void EffectPreview::setupSend(Details details) {
  457. if (_send) {
  458. _send->setClickedCallback([=] {
  459. _actionWithEffect({}, details);
  460. });
  461. const auto type = details.type;
  462. SetupMenuAndShortcuts(_send.get(), _show, [=] {
  463. return Details{ .type = type };
  464. }, _actionWithEffect);
  465. } else {
  466. _premiumPromoLabel->entity()->setClickHandlerFilter([=](auto&&...) {
  467. const auto window = _show->resolveWindow();
  468. if (window) {
  469. if (const auto onstack = _close) {
  470. onstack();
  471. }
  472. ShowPremiumPreviewBox(window, PremiumFeature::Effects);
  473. }
  474. return false;
  475. });
  476. }
  477. }
  478. bool EffectPreview::checkIconBecameLoaded() {
  479. if (!_icon.isNull()) {
  480. return false;
  481. }
  482. const auto reactions = &_show->session().data().reactions();
  483. _icon = reactions->resolveEffectImageFor(_effect.id.custom());
  484. if (_icon.isNull()) {
  485. return false;
  486. }
  487. repaintBackground();
  488. return true;
  489. }
  490. bool EffectPreview::checkLoaded() {
  491. if (checkIconBecameLoaded()) {
  492. update();
  493. }
  494. if (_effect.aroundAnimation) {
  495. _bytes = _media->bytes();
  496. _filepath = _media->owner()->filepath();
  497. } else {
  498. _bytes = _media->videoThumbnailContent();
  499. }
  500. return !_icon.isNull() && (!_bytes.isEmpty() || !_filepath.isEmpty());
  501. }
  502. void EffectPreview::toggle(bool shown) {
  503. if (!shown && _hiding) {
  504. return;
  505. }
  506. _hiding = !shown;
  507. if (_bottomCache.isNull()) {
  508. _bottomCache = Ui::GrabWidget(_bottom);
  509. _bottom->hide();
  510. }
  511. _shownAnimation.start([=] {
  512. update();
  513. if (!_shownAnimation.animating()) {
  514. if (_hiding) {
  515. delete this;
  516. } else {
  517. _bottomCache = QPixmap();
  518. _bottom->show();
  519. }
  520. }
  521. }, shown ? 0. : 1., shown ? 1. : 0., kToggleDuration, anim::easeOutCirc);
  522. show();
  523. }
  524. } // namespace
  525. Fn<void(Action, Details)> DefaultCallback(
  526. std::shared_ptr<ChatHelpers::Show> show,
  527. Fn<void(Api::SendOptions)> send) {
  528. const auto guard = Ui::MakeWeak(show->toastParent());
  529. return [=](Action action, Details details) {
  530. if (action.type == ActionType::Send) {
  531. send(action.options);
  532. return;
  533. }
  534. auto box = HistoryView::PrepareScheduleBox(
  535. guard,
  536. show,
  537. details,
  538. send,
  539. action.options);
  540. const auto weak = Ui::MakeWeak(box.data());
  541. show->showBox(std::move(box));
  542. if (const auto strong = weak.data()) {
  543. strong->setCloseByOutsideClick(false);
  544. }
  545. };
  546. }
  547. FillMenuResult AttachSendMenuEffect(
  548. not_null<Ui::PopupMenu*> menu,
  549. std::shared_ptr<ChatHelpers::Show> show,
  550. Details details,
  551. Fn<void(Action, Details)> action,
  552. std::optional<QPoint> desiredPositionOverride) {
  553. Expects(show != nullptr);
  554. using namespace HistoryView::Reactions;
  555. const auto effect = std::make_shared<QPointer<EffectPreview>>();
  556. const auto position = desiredPositionOverride.value_or(QCursor::pos());
  557. const auto selector = (show && details.effectAllowed)
  558. ? AttachSelectorToMenu(
  559. menu,
  560. position,
  561. st::reactPanelEmojiPan,
  562. show,
  563. LookupPossibleEffects(&show->session()),
  564. { tr::lng_effect_add_title(tr::now) },
  565. nullptr, // iconFactory
  566. [=] { return (*effect) != nullptr; }) // paused
  567. : base::make_unexpected(AttachSelectorResult::Skipped);
  568. if (!selector) {
  569. if (selector.error() == AttachSelectorResult::Failed) {
  570. return FillMenuResult::Failed;
  571. }
  572. menu->prepareGeometryFor(position);
  573. return FillMenuResult::Prepared;
  574. }
  575. (*selector)->chosen(
  576. ) | rpl::start_with_next([=](ChosenReaction chosen) {
  577. const auto &reactions = show->session().data().reactions();
  578. const auto &effects = reactions.list(Data::Reactions::Type::Effects);
  579. const auto i = ranges::find(effects, chosen.id, &Data::Reaction::id);
  580. if (i != end(effects)) {
  581. if (const auto strong = effect->data()) {
  582. strong->hideAnimated();
  583. }
  584. const auto weak = Ui::MakeWeak(menu);
  585. const auto done = [=] {
  586. delete effect->data();
  587. if (const auto strong = weak.data()) {
  588. strong->hideMenu(true);
  589. }
  590. };
  591. *effect = Ui::CreateChild<EffectPreview>(
  592. menu,
  593. show,
  594. details,
  595. menu->mapFromGlobal(chosen.globalGeometry.center()),
  596. *i,
  597. action,
  598. crl::guard(menu, done));
  599. (*effect)->show();
  600. }
  601. }, menu->lifetime());
  602. return FillMenuResult::Prepared;
  603. }
  604. FillMenuResult FillSendMenu(
  605. not_null<Ui::PopupMenu*> menu,
  606. std::shared_ptr<ChatHelpers::Show> showForEffect,
  607. Details details,
  608. Fn<void(Action, Details)> action,
  609. const style::ComposeIcons *iconsOverride,
  610. std::optional<QPoint> desiredPositionOverride) {
  611. const auto type = details.type;
  612. const auto sending = (type != Type::Disabled);
  613. const auto empty = !sending
  614. && (details.spoiler == SpoilerState::None)
  615. && (details.caption == CaptionState::None)
  616. && !details.price.has_value();
  617. if (empty || !action) {
  618. return FillMenuResult::Skipped;
  619. }
  620. const auto &icons = iconsOverride
  621. ? *iconsOverride
  622. : st::defaultComposeIcons;
  623. if (sending && type != Type::Reminder) {
  624. menu->addAction(
  625. tr::lng_send_silent_message(tr::now),
  626. [=] { action({ Api::SendOptions{ .silent = true } }, details); },
  627. &icons.menuMute);
  628. }
  629. if (sending && type != Type::SilentOnly) {
  630. menu->addAction(
  631. (type == Type::Reminder
  632. ? tr::lng_reminder_message(tr::now)
  633. : tr::lng_schedule_message(tr::now)),
  634. [=] { action({ .type = ActionType::Schedule }, details); },
  635. &icons.menuSchedule);
  636. }
  637. if (sending && type == Type::ScheduledToUser) {
  638. menu->addAction(
  639. tr::lng_scheduled_send_until_online(tr::now),
  640. [=] { action(
  641. { Api::DefaultSendWhenOnlineOptions() },
  642. details); },
  643. &icons.menuWhenOnline);
  644. }
  645. if ((type != Type::Disabled)
  646. && ((details.spoiler != SpoilerState::None)
  647. || (details.caption != CaptionState::None)
  648. || details.price.has_value())) {
  649. menu->addSeparator(&st::expandedMenuSeparator);
  650. }
  651. if (details.spoiler != SpoilerState::None) {
  652. const auto spoilered = (details.spoiler == SpoilerState::Enabled);
  653. menu->addAction(
  654. (spoilered
  655. ? tr::lng_context_disable_spoiler(tr::now)
  656. : tr::lng_context_spoiler_effect(tr::now)),
  657. [=] { action({ .type = spoilered
  658. ? ActionType::SpoilerOff
  659. : ActionType::SpoilerOn
  660. }, details); },
  661. spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
  662. }
  663. if (details.caption != CaptionState::None) {
  664. const auto above = (details.caption == CaptionState::Above);
  665. menu->addAction(
  666. (above
  667. ? tr::lng_caption_move_down(tr::now)
  668. : tr::lng_caption_move_up(tr::now)),
  669. [=] { action({ .type = above
  670. ? ActionType::CaptionDown
  671. : ActionType::CaptionUp
  672. }, details); },
  673. above ? &icons.menuBelow : &icons.menuAbove);
  674. }
  675. if (details.price) {
  676. menu->addAction(
  677. ((*details.price > 0)
  678. ? tr::lng_context_change_price(tr::now)
  679. : tr::lng_context_make_paid(tr::now)),
  680. [=] { action({ .type = ActionType::ChangePrice }, details); },
  681. &icons.menuPrice);
  682. }
  683. if (showForEffect) {
  684. return AttachSendMenuEffect(
  685. menu,
  686. showForEffect,
  687. details,
  688. action,
  689. desiredPositionOverride);
  690. }
  691. const auto position = desiredPositionOverride.value_or(QCursor::pos());
  692. menu->prepareGeometryFor(position);
  693. return FillMenuResult::Prepared;
  694. }
  695. void SetupMenuAndShortcuts(
  696. not_null<Ui::RpWidget*> button,
  697. std::shared_ptr<ChatHelpers::Show> show,
  698. Fn<Details()> details,
  699. Fn<void(Action, Details)> action) {
  700. const auto menu = std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
  701. const auto showMenu = [=] {
  702. *menu = base::make_unique_q<Ui::PopupMenu>(
  703. button,
  704. st::popupMenuWithIcons);
  705. const auto result = FillSendMenu(*menu, show, details(), action);
  706. if (result != FillMenuResult::Prepared) {
  707. return false;
  708. }
  709. (*menu)->popupPrepared();
  710. return true;
  711. };
  712. base::install_event_filter(button, [=](not_null<QEvent*> e) {
  713. if (e->type() == QEvent::ContextMenu && showMenu()) {
  714. return base::EventFilterResult::Cancel;
  715. }
  716. return base::EventFilterResult::Continue;
  717. });
  718. Shortcuts::Requests(
  719. ) | rpl::filter([=] {
  720. return button->isActiveWindow();
  721. }) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
  722. using Command = Shortcuts::Command;
  723. const auto now = details().type;
  724. if (now == Type::Disabled) {
  725. return;
  726. }
  727. ((now != Type::Reminder)
  728. && request->check(Command::SendSilentMessage)
  729. && request->handle([=] {
  730. action({ Api::SendOptions{ .silent = true } }, details());
  731. return true;
  732. }))
  733. ||
  734. ((now != Type::SilentOnly)
  735. && request->check(Command::ScheduleMessage)
  736. && request->handle([=] {
  737. action({ .type = ActionType::Schedule }, details());
  738. return true;
  739. }))
  740. ||
  741. (request->check(Command::JustSendMessage) && request->handle([=] {
  742. const auto post = [&](QEvent::Type type) {
  743. QApplication::postEvent(
  744. button,
  745. new QMouseEvent(
  746. type,
  747. QPointF(0, 0),
  748. Qt::LeftButton,
  749. Qt::LeftButton,
  750. Qt::NoModifier));
  751. };
  752. post(QEvent::MouseButtonPress);
  753. post(QEvent::MouseButtonRelease);
  754. return true;
  755. }));
  756. }, button->lifetime());
  757. }
  758. void SetupReadAllMenu(
  759. not_null<Ui::RpWidget*> button,
  760. Fn<Data::Thread*()> currentThread,
  761. const QString &text,
  762. Fn<void(not_null<Data::Thread*>, Fn<void()>)> sendReadRequest) {
  763. struct State {
  764. base::unique_qptr<Ui::PopupMenu> menu;
  765. base::flat_set<base::weak_ptr<Data::Thread>> sentForEntries;
  766. };
  767. const auto state = std::make_shared<State>();
  768. const auto showMenu = [=] {
  769. const auto thread = base::make_weak(currentThread());
  770. if (!thread) {
  771. return;
  772. }
  773. state->menu = base::make_unique_q<Ui::PopupMenu>(
  774. button,
  775. st::popupMenuWithIcons);
  776. state->menu->addAction(text, [=] {
  777. const auto strong = thread.get();
  778. if (!strong || !state->sentForEntries.emplace(thread).second) {
  779. return;
  780. }
  781. sendReadRequest(strong, [=] {
  782. state->sentForEntries.remove(thread);
  783. });
  784. }, &st::menuIconMarkRead);
  785. state->menu->popup(QCursor::pos());
  786. };
  787. base::install_event_filter(button, [=](not_null<QEvent*> e) {
  788. if (e->type() == QEvent::ContextMenu) {
  789. showMenu();
  790. return base::EventFilterResult::Cancel;
  791. }
  792. return base::EventFilterResult::Continue;
  793. });
  794. }
  795. void SetupUnreadMentionsMenu(
  796. not_null<Ui::RpWidget*> button,
  797. Fn<Data::Thread*()> currentThread) {
  798. const auto text = tr::lng_context_mark_read_mentions_all(tr::now);
  799. const auto sendOne = [=](
  800. base::weak_ptr<Data::Thread> weakThread,
  801. Fn<void()> done,
  802. auto resend) -> void {
  803. const auto thread = weakThread.get();
  804. if (!thread) {
  805. done();
  806. return;
  807. }
  808. const auto peer = thread->peer();
  809. const auto topic = thread->asTopic();
  810. const auto rootId = topic ? topic->rootId() : 0;
  811. using Flag = MTPmessages_ReadMentions::Flag;
  812. peer->session().api().request(MTPmessages_ReadMentions(
  813. MTP_flags(rootId ? Flag::f_top_msg_id : Flag()),
  814. peer->input,
  815. MTP_int(rootId)
  816. )).done([=](const MTPmessages_AffectedHistory &result) {
  817. const auto offset = peer->session().api().applyAffectedHistory(
  818. peer,
  819. result);
  820. if (offset > 0) {
  821. resend(weakThread, done, resend);
  822. } else {
  823. done();
  824. peer->owner().history(peer)->clearUnreadMentionsFor(rootId);
  825. }
  826. }).fail(done).send();
  827. };
  828. const auto sendRequest = [=](
  829. not_null<Data::Thread*> thread,
  830. Fn<void()> done) {
  831. sendOne(base::make_weak(thread), std::move(done), sendOne);
  832. };
  833. SetupReadAllMenu(button, currentThread, text, sendRequest);
  834. }
  835. void SetupUnreadReactionsMenu(
  836. not_null<Ui::RpWidget*> button,
  837. Fn<Data::Thread*()> currentThread) {
  838. const auto text = tr::lng_context_mark_read_reactions_all(tr::now);
  839. const auto sendOne = [=](
  840. base::weak_ptr<Data::Thread> weakThread,
  841. Fn<void()> done,
  842. auto resend) -> void {
  843. const auto thread = weakThread.get();
  844. if (!thread) {
  845. done();
  846. return;
  847. }
  848. const auto topic = thread->asTopic();
  849. const auto peer = thread->peer();
  850. const auto rootId = topic ? topic->rootId() : 0;
  851. using Flag = MTPmessages_ReadReactions::Flag;
  852. peer->session().api().request(MTPmessages_ReadReactions(
  853. MTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)),
  854. peer->input,
  855. MTP_int(rootId)
  856. )).done([=](const MTPmessages_AffectedHistory &result) {
  857. const auto offset = peer->session().api().applyAffectedHistory(
  858. peer,
  859. result);
  860. if (offset > 0) {
  861. resend(weakThread, done, resend);
  862. } else {
  863. done();
  864. peer->owner().history(peer)->clearUnreadReactionsFor(rootId);
  865. }
  866. }).fail(done).send();
  867. };
  868. const auto sendRequest = [=](
  869. not_null<Data::Thread*> thread,
  870. Fn<void()> done) {
  871. sendOne(base::make_weak(thread), std::move(done), sendOne);
  872. };
  873. SetupReadAllMenu(button, currentThread, text, sendRequest);
  874. }
  875. } // namespace SendMenu