edit_peer_reactions.cpp 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  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/peers/edit_peer_reactions.h"
  8. #include "apiwrap.h"
  9. #include "base/event_filter.h"
  10. #include "chat_helpers/emoji_list_widget.h"
  11. #include "chat_helpers/tabbed_panel.h"
  12. #include "chat_helpers/tabbed_selector.h"
  13. #include "core/ui_integration.h"
  14. #include "data/data_channel.h"
  15. #include "data/data_chat.h"
  16. #include "data/data_document.h"
  17. #include "data/data_peer_values.h" // UniqueReactionsLimit.
  18. #include "data/data_session.h"
  19. #include "data/data_user.h"
  20. #include "history/view/reactions/history_view_reactions_selector.h"
  21. #include "lang/lang_keys.h"
  22. #include "main/main_session.h"
  23. #include "ui/boxes/boost_box.h"
  24. #include "ui/layers/generic_box.h"
  25. #include "ui/text/text_utilities.h"
  26. #include "ui/vertical_list.h"
  27. #include "ui/widgets/checkbox.h"
  28. #include "ui/widgets/continuous_sliders.h"
  29. #include "ui/widgets/fields/input_field.h"
  30. #include "ui/wrap/slide_wrap.h"
  31. #include "ui/ui_utility.h"
  32. #include "window/window_session_controller.h"
  33. #include "window/window_session_controller_link_info.h"
  34. #include "styles/style_chat_helpers.h"
  35. #include "styles/style_info.h"
  36. #include "styles/style_layers.h"
  37. #include "styles/style_settings.h"
  38. #include <QtWidgets/QTextEdit>
  39. #include <QtGui/QTextBlock>
  40. #include <QtGui/QTextDocumentFragment>
  41. namespace {
  42. constexpr auto kDisabledEmojiOpacity = 0.4;
  43. struct UniqueCustomEmojiContext {
  44. std::vector<DocumentId> ids;
  45. Fn<bool(DocumentId)> applyHardLimit;
  46. int hardLimit = 0;
  47. int hardLimitChecked = 0;
  48. bool hardLimitHit = false;
  49. };
  50. class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {
  51. public:
  52. MaybeDisabledEmoji(
  53. std::unique_ptr<CustomEmoji> wrapped,
  54. Fn<bool()> enabled);
  55. int width() override;
  56. QString entityData() override;
  57. void paint(QPainter &p, const Context &context) override;
  58. void unload() override;
  59. bool ready() override;
  60. bool readyInDefaultState() override;
  61. private:
  62. const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
  63. const Fn<bool()> _enabled;
  64. };
  65. MaybeDisabledEmoji::MaybeDisabledEmoji(
  66. std::unique_ptr<CustomEmoji> wrapped,
  67. Fn<bool()> enabled)
  68. : _wrapped(std::move(wrapped))
  69. , _enabled(std::move(enabled)) {
  70. }
  71. int MaybeDisabledEmoji::width() {
  72. return _wrapped->width();
  73. }
  74. QString MaybeDisabledEmoji::entityData() {
  75. return _wrapped->entityData();
  76. }
  77. void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {
  78. const auto disabled = !_enabled();
  79. const auto was = disabled ? p.opacity() : 1.;
  80. if (disabled) {
  81. p.setOpacity(kDisabledEmojiOpacity);
  82. }
  83. _wrapped->paint(p, context);
  84. if (disabled) {
  85. p.setOpacity(was);
  86. }
  87. }
  88. void MaybeDisabledEmoji::unload() {
  89. _wrapped->unload();
  90. }
  91. bool MaybeDisabledEmoji::ready() {
  92. return _wrapped->ready();
  93. }
  94. bool MaybeDisabledEmoji::readyInDefaultState() {
  95. return _wrapped->readyInDefaultState();
  96. }
  97. [[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
  98. auto all = TextUtilities::SplitTags(mimeTag);
  99. for (auto i = all.begin(); i != all.end();) {
  100. if (Ui::InputField::IsCustomEmojiLink(*i)) {
  101. ++i;
  102. } else {
  103. i = all.erase(i);
  104. }
  105. }
  106. return TextUtilities::JoinTag(all);
  107. }
  108. [[nodiscard]] bool AllowOnlyCustomEmojiMimeDataHook(
  109. not_null<const QMimeData*> data,
  110. Ui::InputField::MimeAction action) {
  111. if (action == Ui::InputField::MimeAction::Check) {
  112. const auto textMime = TextUtilities::TagsTextMimeType();
  113. const auto tagsMime = TextUtilities::TagsMimeType();
  114. if (!data->hasFormat(textMime) || !data->hasFormat(tagsMime)) {
  115. return false;
  116. }
  117. auto text = QString::fromUtf8(data->data(textMime));
  118. auto tags = TextUtilities::DeserializeTags(
  119. data->data(tagsMime),
  120. text.size());
  121. auto checkedTill = 0;
  122. ranges::sort(tags, ranges::less(), &TextWithTags::Tag::offset);
  123. for (const auto &tag : tags) {
  124. if (tag.offset != checkedTill
  125. || AllowOnlyCustomEmojiProcessor(tag.id) != tag.id) {
  126. return false;
  127. }
  128. checkedTill += tag.length;
  129. }
  130. return true;
  131. } else if (action == Ui::InputField::MimeAction::Insert) {
  132. return false;
  133. }
  134. Unexpected("Action in MimeData hook.");
  135. }
  136. [[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {
  137. const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
  138. const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
  139. return { Data::ReactionId{ like }, Data::ReactionId{ dislike } };
  140. }
  141. [[nodiscard]] bool RemoveNonCustomEmojiFragment(
  142. not_null<QTextDocument*> document,
  143. UniqueCustomEmojiContext &context) {
  144. context.ids.clear();
  145. context.hardLimitChecked = 0;
  146. auto removeFrom = 0;
  147. auto removeTill = 0;
  148. auto block = document->begin();
  149. for (auto j = block.begin(); !j.atEnd(); ++j) {
  150. const auto fragment = j.fragment();
  151. Assert(fragment.isValid());
  152. removeTill = removeFrom = fragment.position();
  153. const auto format = fragment.charFormat();
  154. if (format.objectType() != Ui::InputField::kCustomEmojiFormat) {
  155. removeTill += fragment.length();
  156. break;
  157. }
  158. const auto id = format.property(Ui::InputField::kCustomEmojiId);
  159. const auto documentId = id.toULongLong();
  160. const auto applyHardLimit = context.applyHardLimit(documentId);
  161. if (ranges::contains(context.ids, documentId)) {
  162. removeTill += fragment.length();
  163. break;
  164. } else if (applyHardLimit
  165. && context.hardLimitChecked >= context.hardLimit) {
  166. context.hardLimitHit = true;
  167. removeTill += fragment.length();
  168. break;
  169. }
  170. context.ids.push_back(documentId);
  171. if (applyHardLimit) {
  172. ++context.hardLimitChecked;
  173. }
  174. }
  175. while (removeTill == removeFrom) {
  176. block = block.next();
  177. if (block == document->end()) {
  178. return false;
  179. }
  180. removeTill = block.position();
  181. }
  182. Ui::PrepareFormattingOptimization(document);
  183. auto cursor = QTextCursor(document);
  184. cursor.setPosition(removeFrom);
  185. cursor.setPosition(removeTill, QTextCursor::KeepAnchor);
  186. cursor.removeSelectedText();
  187. return true;
  188. }
  189. bool RemoveNonCustomEmoji(
  190. not_null<QTextDocument*> document,
  191. UniqueCustomEmojiContext &context) {
  192. if (!RemoveNonCustomEmojiFragment(document, context)) {
  193. return false;
  194. }
  195. while (RemoveNonCustomEmojiFragment(document, context)) {
  196. }
  197. return true;
  198. }
  199. void SetupOnlyCustomEmojiField(
  200. not_null<Ui::InputField*> field,
  201. Fn<void(std::vector<DocumentId>, bool)> callback,
  202. Fn<bool(DocumentId)> applyHardLimit,
  203. int customHardLimit) {
  204. field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
  205. field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);
  206. struct State {
  207. bool processing = false;
  208. bool pending = false;
  209. };
  210. const auto state = field->lifetime().make_state<State>();
  211. field->changes(
  212. ) | rpl::start_with_next([=] {
  213. state->pending = true;
  214. if (state->processing) {
  215. return;
  216. }
  217. auto context = UniqueCustomEmojiContext{
  218. .applyHardLimit = applyHardLimit,
  219. .hardLimit = customHardLimit,
  220. };
  221. auto changed = false;
  222. state->processing = true;
  223. while (state->pending) {
  224. state->pending = false;
  225. const auto document = field->rawTextEdit()->document();
  226. const auto pageSize = document->pageSize();
  227. QTextCursor(document).joinPreviousEditBlock();
  228. if (RemoveNonCustomEmoji(document, context)) {
  229. changed = true;
  230. }
  231. state->processing = false;
  232. QTextCursor(document).endEditBlock();
  233. if (document->pageSize() != pageSize) {
  234. document->setPageSize(pageSize);
  235. }
  236. }
  237. callback(context.ids, context.hardLimitHit);
  238. if (changed) {
  239. field->forceProcessContentsChanges();
  240. }
  241. }, field->lifetime());
  242. }
  243. [[nodiscard]] TextWithTags ComposeEmojiList(
  244. not_null<Data::Reactions*> reactions,
  245. const std::vector<Data::ReactionId> &list) {
  246. auto result = TextWithTags();
  247. const auto size = [&] {
  248. return int(result.text.size());
  249. };
  250. auto added = base::flat_set<Data::ReactionId>();
  251. const auto &all = reactions->list(Data::Reactions::Type::All);
  252. const auto add = [&](Data::ReactionId id) {
  253. if (!added.emplace(id).second) {
  254. return;
  255. }
  256. auto unifiedId = id.custom();
  257. const auto offset = size();
  258. if (unifiedId) {
  259. result.text.append('@');
  260. } else if (id.paid()) {
  261. result.text.append(QChar(0x2B50));
  262. unifiedId = reactions->lookupPaid()->selectAnimation->id;
  263. } else {
  264. result.text.append(id.emoji());
  265. const auto i = ranges::find(all, id, &Data::Reaction::id);
  266. if (i == end(all)) {
  267. return;
  268. }
  269. unifiedId = i->selectAnimation->id;
  270. }
  271. const auto data = Data::SerializeCustomEmojiId(unifiedId);
  272. const auto tag = Ui::InputField::CustomEmojiLink(data);
  273. result.tags.append({ offset, size() - offset, tag });
  274. };
  275. for (const auto &id : list) {
  276. add(id);
  277. }
  278. return result;
  279. }
  280. enum class ReactionsSelectorState {
  281. Active,
  282. Disabled,
  283. Hidden,
  284. };
  285. struct ReactionsSelectorArgs {
  286. not_null<QWidget*> outer;
  287. not_null<Window::SessionController*> controller;
  288. rpl::producer<QString> title;
  289. std::vector<Data::Reaction> list;
  290. std::vector<Data::ReactionId> selected;
  291. rpl::producer<bool> paid;
  292. Fn<void(std::vector<Data::ReactionId>, bool)> callback;
  293. rpl::producer<ReactionsSelectorState> stateValue;
  294. int customAllowed = 0;
  295. int customHardLimit = 0;
  296. bool all = false;
  297. };
  298. object_ptr<Ui::RpWidget> AddReactionsSelector(
  299. not_null<Ui::RpWidget*> parent,
  300. ReactionsSelectorArgs &&args) {
  301. using namespace ChatHelpers;
  302. using HistoryView::Reactions::UnifiedFactoryOwner;
  303. auto result = object_ptr<Ui::InputField>(
  304. parent,
  305. st::manageGroupReactionsField,
  306. Ui::InputField::Mode::MultiLine,
  307. std::move(args.title));
  308. const auto raw = result.data();
  309. const auto session = &args.controller->session();
  310. const auto owner = &session->data();
  311. const auto reactions = &owner->reactions();
  312. const auto customAllowed = args.customAllowed;
  313. struct State {
  314. std::unique_ptr<Ui::RpWidget> overlay;
  315. std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
  316. UnifiedFactoryOwner::RecentFactory factory;
  317. base::flat_set<DocumentId> allowed;
  318. std::vector<Data::ReactionId> reactions;
  319. rpl::lifetime focusLifetime;
  320. };
  321. const auto paid = reactions->lookupPaid();
  322. auto normal = reactions->list(Data::Reactions::Type::Active);
  323. normal.push_back(*paid);
  324. const auto state = raw->lifetime().make_state<State>();
  325. state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
  326. session,
  327. normal);
  328. state->factory = state->unifiedFactoryOwner->factory();
  329. state->reactions = std::move(args.selected);
  330. const auto customEmojiPaused = [controller = args.controller] {
  331. return controller->isGifPausedAtLeastFor(PauseReason::Layer);
  332. };
  333. auto simpleContext = Core::TextContext({
  334. .session = session,
  335. .repaint = [=] { raw->update(); },
  336. });
  337. auto context = simpleContext;
  338. context.customEmojiFactory = [=](
  339. QStringView data,
  340. const Ui::Text::MarkedContext &context
  341. ) -> std::unique_ptr<Ui::Text::CustomEmoji> {
  342. const auto id = Data::ParseCustomEmojiData(data);
  343. auto result = Ui::Text::MakeCustomEmoji(data, simpleContext);
  344. if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
  345. return std::make_unique<MaybeDisabledEmoji>(
  346. std::move(result),
  347. [=] { return state->allowed.contains(id); });
  348. }
  349. using namespace Ui::Text;
  350. return std::make_unique<FirstFrameEmoji>(std::move(result));
  351. };
  352. raw->setCustomTextContext(
  353. std::move(context),
  354. customEmojiPaused,
  355. customEmojiPaused);
  356. const auto callback = args.callback;
  357. const auto isCustom = [=](DocumentId id) {
  358. return state->unifiedFactoryOwner->lookupReactionId(id).custom();
  359. };
  360. SetupOnlyCustomEmojiField(raw, [=](
  361. std::vector<DocumentId> ids,
  362. bool hardLimitHit) {
  363. auto allowed = base::flat_set<DocumentId>();
  364. auto reactions = std::vector<Data::ReactionId>();
  365. reactions.reserve(ids.size());
  366. allowed.reserve(std::min(customAllowed, int(ids.size())));
  367. const auto owner = state->unifiedFactoryOwner.get();
  368. for (const auto id : ids) {
  369. const auto reactionId = owner->lookupReactionId(id);
  370. if (reactionId.custom() && allowed.size() < customAllowed) {
  371. allowed.emplace(id);
  372. }
  373. reactions.push_back(reactionId);
  374. }
  375. if (state->allowed != allowed) {
  376. state->allowed = std::move(allowed);
  377. raw->rawTextEdit()->update();
  378. }
  379. state->reactions = reactions;
  380. callback(std::move(reactions), hardLimitHit);
  381. }, isCustom, args.customHardLimit);
  382. const auto applyFromState = [=] {
  383. raw->setTextWithTags(ComposeEmojiList(reactions, state->reactions));
  384. };
  385. applyFromState();
  386. std::move(
  387. args.paid
  388. ) | rpl::start_with_next([=](bool paid) {
  389. const auto id = Data::ReactionId::Paid();
  390. if (paid && !ranges::contains(state->reactions, id)) {
  391. state->reactions.insert(begin(state->reactions), id);
  392. applyFromState();
  393. } else if (!paid && ranges::contains(state->reactions, id)) {
  394. state->reactions.erase(
  395. ranges::remove(state->reactions, id),
  396. end(state->reactions));
  397. applyFromState();
  398. }
  399. }, raw->lifetime());
  400. const auto toggle = Ui::CreateChild<Ui::IconButton>(
  401. parent.get(),
  402. st::manageGroupReactions);
  403. using SelectorState = ReactionsSelectorState;
  404. std::move(
  405. args.stateValue
  406. ) | rpl::start_with_next([=](SelectorState value) {
  407. switch (value) {
  408. case SelectorState::Active:
  409. state->overlay = nullptr;
  410. state->focusLifetime.destroy();
  411. if (raw->empty()) {
  412. raw->setTextWithTags(
  413. ComposeEmojiList(reactions, DefaultSelected()));
  414. }
  415. raw->setDisabled(false);
  416. raw->setFocusFast();
  417. break;
  418. case SelectorState::Disabled:
  419. state->overlay = std::make_unique<Ui::RpWidget>(parent);
  420. state->overlay->show();
  421. raw->geometryValue() | rpl::start_with_next([=](QRect rect) {
  422. state->overlay->setGeometry(rect);
  423. }, state->overlay->lifetime());
  424. state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) {
  425. auto color = st::boxBg->c;
  426. color.setAlphaF(0.5);
  427. QPainter(state->overlay.get()).fillRect(
  428. clip,
  429. color);
  430. }, state->overlay->lifetime());
  431. [[fallthrough]];
  432. case SelectorState::Hidden:
  433. if (Ui::InFocusChain(raw)) {
  434. raw->parentWidget()->setFocus();
  435. }
  436. raw->setDisabled(true);
  437. raw->focusedChanges(
  438. ) | rpl::start_with_next([=](bool focused) {
  439. if (focused) {
  440. raw->parentWidget()->setFocus();
  441. }
  442. }, state->focusLifetime);
  443. break;
  444. }
  445. }, raw->lifetime());
  446. const auto panel = Ui::CreateChild<TabbedPanel>(
  447. args.outer.get(),
  448. args.controller,
  449. object_ptr<TabbedSelector>(
  450. nullptr,
  451. args.controller->uiShow(),
  452. Window::GifPauseReason::Layer,
  453. (args.all
  454. ? TabbedSelector::Mode::FullReactions
  455. : TabbedSelector::Mode::RecentReactions)));
  456. auto panelList = state->unifiedFactoryOwner->unifiedIdsList();
  457. panelList.erase(
  458. ranges::remove(panelList, paid->selectAnimation->id),
  459. end(panelList));
  460. panel->selector()->provideRecentEmoji(
  461. ChatHelpers::DocumentListToRecent(panelList));
  462. panel->setDesiredHeightValues(
  463. 1.,
  464. st::emojiPanMinHeight / 2,
  465. st::emojiPanMinHeight);
  466. panel->hide();
  467. panel->selector()->customEmojiChosen(
  468. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  469. Data::InsertCustomEmoji(raw, data.document);
  470. }, panel->lifetime());
  471. const auto updateEmojiPanelGeometry = [=] {
  472. const auto parent = panel->parentWidget();
  473. const auto global = toggle->mapToGlobal({ 0, 0 });
  474. const auto local = parent->mapFromGlobal(global);
  475. panel->moveBottomRight(
  476. local.y(),
  477. local.x() + toggle->width() * 3);
  478. };
  479. const auto scheduleUpdateEmojiPanelGeometry = [=] {
  480. // updateEmojiPanelGeometry uses not only container geometry, but
  481. // also container children geometries that will be updated later.
  482. crl::on_main(raw, updateEmojiPanelGeometry);
  483. };
  484. const auto filterCallback = [=](not_null<QEvent*> event) {
  485. const auto type = event->type();
  486. if (type == QEvent::Move || type == QEvent::Resize) {
  487. scheduleUpdateEmojiPanelGeometry();
  488. }
  489. return base::EventFilterResult::Continue;
  490. };
  491. for (auto widget = (QWidget*)raw
  492. ; widget && widget != args.outer
  493. ; widget = widget->parentWidget()) {
  494. base::install_event_filter(raw, widget, filterCallback);
  495. }
  496. base::install_event_filter(raw, args.outer, filterCallback);
  497. scheduleUpdateEmojiPanelGeometry();
  498. toggle->installEventFilter(panel);
  499. toggle->addClickHandler([=] {
  500. panel->toggleAnimated();
  501. });
  502. raw->geometryValue() | rpl::start_with_next([=](QRect geometry) {
  503. toggle->move(
  504. geometry.x() + geometry.width() - toggle->width(),
  505. geometry.y() + geometry.height() - toggle->height());
  506. updateEmojiPanelGeometry();
  507. }, toggle->lifetime());
  508. return result;
  509. }
  510. void AddReactionsText(
  511. not_null<Ui::VerticalLayout*> container,
  512. not_null<Window::SessionNavigation*> navigation,
  513. int allowedCustomReactions,
  514. rpl::producer<int> customCountValue,
  515. Fn<void(int required)> askForBoosts) {
  516. auto ownedInner = object_ptr<Ui::VerticalLayout>(container);
  517. const auto inner = ownedInner.data();
  518. const auto count = inner->lifetime().make_state<rpl::variable<int>>(
  519. std::move(customCountValue));
  520. container->add(
  521. object_ptr<Ui::DividerLabel>(
  522. container,
  523. std::move(ownedInner),
  524. st::defaultBoxDividerLabelPadding),
  525. QMargins(0, st::manageGroupReactionsTextSkip, 0, 0));
  526. const auto label = inner->add(
  527. object_ptr<Ui::FlatLabel>(
  528. inner,
  529. tr::lng_manage_peer_reactions_own(
  530. lt_link,
  531. tr::lng_manage_peer_reactions_own_link(
  532. ) | Ui::Text::ToLink(),
  533. Ui::Text::WithEntities),
  534. st::boxDividerLabel));
  535. const auto weak = base::make_weak(navigation);
  536. label->setClickHandlerFilter([=](const auto &...) {
  537. if (const auto strong = weak.get()) {
  538. strong->showPeerByLink(Window::PeerByLinkInfo{
  539. .usernameOrId = u"stickers"_q,
  540. .resolveType = Window::ResolveType::Mention,
  541. });
  542. }
  543. return false;
  544. });
  545. auto countString = count->value() | rpl::map([](int count) {
  546. return TextWithEntities{ QString::number(count) };
  547. });
  548. auto needs = rpl::combine(
  549. tr::lng_manage_peer_reactions_level(
  550. lt_count,
  551. count->value() | tr::to_count(),
  552. lt_same_count,
  553. std::move(countString),
  554. Ui::Text::RichLangValue),
  555. tr::lng_manage_peer_reactions_boost(
  556. lt_link,
  557. tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(),
  558. Ui::Text::RichLangValue)
  559. ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {
  560. a.append(' ').append(std::move(b));
  561. return std::move(a);
  562. });
  563. const auto wrap = inner->add(
  564. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  565. inner,
  566. object_ptr<Ui::FlatLabel>(
  567. inner,
  568. std::move(needs),
  569. st::boxDividerLabel),
  570. QMargins{ 0, st::normalFont->height, 0, 0 }));
  571. wrap->toggleOn(count->value() | rpl::map(
  572. rpl::mappers::_1 > allowedCustomReactions
  573. ));
  574. wrap->finishAnimating();
  575. wrap->entity()->setClickHandlerFilter([=](const auto &...) {
  576. askForBoosts(count->current());
  577. return false;
  578. });
  579. }
  580. } // namespace
  581. void EditAllowedReactionsBox(
  582. not_null<Ui::GenericBox*> box,
  583. EditAllowedReactionsArgs &&args) {
  584. using namespace Data;
  585. using namespace rpl::mappers;
  586. box->setTitle(tr::lng_manage_peer_reactions());
  587. box->setWidth(st::boxWideWidth);
  588. enum class Option {
  589. All,
  590. Some,
  591. None,
  592. };
  593. using SelectorState = ReactionsSelectorState;
  594. struct State {
  595. rpl::variable<Option> option; // For groups.
  596. rpl::variable<SelectorState> selectorState;
  597. std::vector<Data::ReactionId> selected;
  598. rpl::variable<int> customCount;
  599. rpl::variable<bool> paidEnabled;
  600. };
  601. const auto allowed = args.allowed;
  602. const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
  603. ? Option::All
  604. : (allowed.some.empty() && !allowed.paidEnabled)
  605. ? Option::None
  606. : Option::Some;
  607. const auto state = box->lifetime().make_state<State>(State{
  608. .option = optionInitial,
  609. .paidEnabled = allowed.paidEnabled,
  610. });
  611. const auto container = box->verticalLayout();
  612. const auto isGroup = args.isGroup;
  613. const auto enabled = isGroup
  614. ? nullptr
  615. : container->add(object_ptr<Ui::SettingsButton>(
  616. container.get(),
  617. tr::lng_manage_peer_reactions_enable(),
  618. st::manageGroupNoIconButton.button));
  619. if (enabled) {
  620. enabled->toggleOn(rpl::single(optionInitial != Option::None));
  621. enabled->toggledValue(
  622. ) | rpl::start_with_next([=](bool value) {
  623. state->selectorState = value
  624. ? SelectorState::Active
  625. : SelectorState::Disabled;
  626. }, enabled->lifetime());
  627. }
  628. const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
  629. state->option.current());
  630. group->setChangedCallback([=](Option value) {
  631. state->option = value;
  632. });
  633. const auto addOption = [&](Option option, const QString &text) {
  634. if (!isGroup) {
  635. return;
  636. }
  637. container->add(
  638. object_ptr<Ui::Radioenum<Option>>(
  639. container,
  640. group,
  641. option,
  642. text,
  643. st::settingsSendType),
  644. st::settingsSendTypePadding);
  645. };
  646. addOption(Option::All, tr::lng_manage_peer_reactions_all(tr::now));
  647. addOption(Option::Some, tr::lng_manage_peer_reactions_some(tr::now));
  648. addOption(Option::None, tr::lng_manage_peer_reactions_none(tr::now));
  649. const auto about = [](Option option) {
  650. switch (option) {
  651. case Option::All: return tr::lng_manage_peer_reactions_all_about();
  652. case Option::Some: return tr::lng_manage_peer_reactions_some_about();
  653. case Option::None: return tr::lng_manage_peer_reactions_none_about();
  654. }
  655. Unexpected("Option value in EditAllowedReactionsBox.");
  656. };
  657. Ui::AddSkip(container);
  658. Ui::AddDividerText(
  659. container,
  660. (isGroup
  661. ? (state->option.value()
  662. | rpl::map(about)
  663. | rpl::flatten_latest())
  664. : tr::lng_manage_peer_reactions_about_channel()));
  665. const auto wrap = enabled ? nullptr : container->add(
  666. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  667. container,
  668. object_ptr<Ui::VerticalLayout>(container)));
  669. if (wrap) {
  670. wrap->toggleOn(state->option.value(
  671. ) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {
  672. if (!some) {
  673. state->selectorState = SelectorState::Hidden;
  674. }
  675. }) | rpl::after_next([=](bool some) {
  676. if (some) {
  677. state->selectorState = SelectorState::Active;
  678. }
  679. }));
  680. wrap->finishAnimating();
  681. }
  682. const auto reactions = wrap ? wrap->entity() : container.get();
  683. Ui::AddSkip(reactions);
  684. const auto all = args.list;
  685. auto selected = (allowed.type != AllowedReactionsType::Some)
  686. ? (all
  687. | ranges::views::transform(&Data::Reaction::id)
  688. | ranges::to_vector)
  689. : allowed.some;
  690. if (allowed.paidEnabled) {
  691. selected.insert(begin(selected), Data::ReactionId::Paid());
  692. }
  693. const auto changed = [=](
  694. std::vector<Data::ReactionId> chosen,
  695. bool hardLimitHit) {
  696. state->selected = std::move(chosen);
  697. state->customCount = ranges::count_if(
  698. state->selected,
  699. &Data::ReactionId::custom);
  700. state->paidEnabled = ranges::contains(
  701. state->selected,
  702. Data::ReactionId::Paid());
  703. if (hardLimitHit) {
  704. box->uiShow()->showToast(
  705. tr::lng_manage_peer_reactions_limit(tr::now));
  706. }
  707. };
  708. changed(selected.empty() ? DefaultSelected() : std::move(selected), {});
  709. Ui::AddSubsectionTitle(
  710. reactions,
  711. enabled
  712. ? tr::lng_manage_peer_reactions_available()
  713. : tr::lng_manage_peer_reactions_some_title(),
  714. st::manageGroupReactionsFieldPadding);
  715. reactions->add(AddReactionsSelector(reactions, {
  716. .outer = box->getDelegate()->outerContainer(),
  717. .controller = args.navigation->parentController(),
  718. .title = tr::lng_manage_peer_reactions_available_ph(),
  719. .list = all,
  720. .selected = state->selected,
  721. .paid = state->paidEnabled.value(),
  722. .callback = changed,
  723. .stateValue = state->selectorState.value(),
  724. .customAllowed = args.allowedCustomReactions,
  725. .customHardLimit = args.customReactionsHardLimit,
  726. .all = !args.isGroup,
  727. }), st::boxRowPadding);
  728. box->setFocusCallback([=] {
  729. if (state->option.current() == Option::Some) {
  730. state->selectorState.force_assign(SelectorState::Active);
  731. }
  732. });
  733. const auto reactionsLimit = container->lifetime().make_state<int>(0);
  734. if (!isGroup) {
  735. AddReactionsText(
  736. container,
  737. args.navigation,
  738. args.allowedCustomReactions,
  739. state->customCount.value(),
  740. args.askForBoosts);
  741. const auto session = &args.navigation->parentController()->session();
  742. const auto wrap = container->add(
  743. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  744. container,
  745. object_ptr<Ui::VerticalLayout>(container)));
  746. const auto max = Data::UniqueReactionsLimit(session->user());
  747. const auto inactiveColor = std::make_optional(st::windowSubTextFg->c);
  748. const auto activeColor = std::make_optional(
  749. st::windowActiveTextFg->c);
  750. const auto inner = wrap->entity();
  751. Ui::AddSkip(inner);
  752. Ui::AddSubsectionTitle(
  753. inner,
  754. tr::lng_manage_peer_reactions_max_title(),
  755. st::manageGroupReactionsMaxSubtitlePadding);
  756. Ui::AddSkip(inner);
  757. const auto line = inner->add(
  758. object_ptr<Ui::RpWidget>(inner),
  759. st::boxRowPadding);
  760. Ui::AddSkip(inner);
  761. Ui::AddSkip(inner);
  762. const auto left = Ui::CreateChild<Ui::FlatLabel>(
  763. line,
  764. QString::number(1),
  765. st::defaultFlatLabel);
  766. const auto center = Ui::CreateChild<Ui::FlatLabel>(
  767. line,
  768. st::defaultFlatLabel);
  769. const auto right = Ui::CreateChild<Ui::FlatLabel>(
  770. line,
  771. QString::number(max),
  772. st::defaultFlatLabel);
  773. const auto slider = Ui::CreateChild<Ui::MediaSlider>(
  774. line,
  775. st::settingsScale);
  776. rpl::combine(
  777. line->sizeValue(),
  778. left->sizeValue(),
  779. center->sizeValue(),
  780. right->sizeValue()
  781. ) | rpl::start_with_next([=](
  782. const QSize &s,
  783. const QSize &leftSize,
  784. const QSize &centerSize,
  785. const QSize &rightSize) {
  786. const auto sliderHeight = st::settingsScale.seekSize.height();
  787. line->resize(
  788. line->width(),
  789. leftSize.height() + sliderHeight * 2);
  790. {
  791. const auto r = line->rect();
  792. slider->setGeometry(
  793. 0,
  794. r.height() - sliderHeight * 1.5,
  795. r.width(),
  796. sliderHeight);
  797. }
  798. left->moveToLeft(0, 0);
  799. right->moveToRight(0, 0);
  800. center->moveToLeft((s.width() - centerSize.width()) / 2, 0);
  801. }, line->lifetime());
  802. const auto updateLabels = [=](int limit) {
  803. left->setTextColorOverride((limit <= 1)
  804. ? activeColor
  805. : inactiveColor);
  806. center->setText(tr::lng_manage_peer_reactions_max_slider(
  807. tr::now,
  808. lt_count,
  809. limit));
  810. center->setTextColorOverride(activeColor);
  811. right->setTextColorOverride((limit >= max)
  812. ? activeColor
  813. : inactiveColor);
  814. (*reactionsLimit) = limit;
  815. };
  816. const auto current = args.allowed.maxCount
  817. ? std::clamp(1, args.allowed.maxCount, max)
  818. : max / 2;
  819. slider->setPseudoDiscrete(
  820. max,
  821. [=](int index) { return index + 1; },
  822. current,
  823. updateLabels,
  824. updateLabels);
  825. updateLabels(current);
  826. wrap->toggleOn(rpl::single(
  827. optionInitial != Option::None
  828. ) | rpl::then(
  829. state->selectorState.value(
  830. ) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));
  831. Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());
  832. Ui::AddSkip(inner);
  833. const auto paid = inner->add(object_ptr<Ui::SettingsButton>(
  834. inner,
  835. tr::lng_manage_peer_reactions_paid(),
  836. st::manageGroupNoIconButton.button));
  837. paid->toggleOn(state->paidEnabled.value());
  838. paid->toggledValue(
  839. ) | rpl::start_with_next([=](bool value) {
  840. state->paidEnabled = value;
  841. }, paid->lifetime());
  842. Ui::AddSkip(inner);
  843. Ui::AddDividerText(
  844. inner,
  845. tr::lng_manage_peer_reactions_paid_about(
  846. lt_link,
  847. tr::lng_manage_peer_reactions_paid_link([=](QString text) {
  848. return Ui::Text::Link(
  849. text,
  850. u"https://telegram.org/tos/stars"_q);
  851. }),
  852. Ui::Text::WithEntities));
  853. }
  854. const auto collect = [=] {
  855. auto result = AllowedReactions();
  856. result.maxCount = (*reactionsLimit);
  857. if (isGroup
  858. ? (state->option.current() == Option::Some)
  859. : (enabled->toggled())) {
  860. result.some = state->selected;
  861. }
  862. if (!isGroup && enabled->toggled()) {
  863. result.paidEnabled = state->paidEnabled.current();
  864. }
  865. auto some = result.some;
  866. auto simple = all | ranges::views::transform(
  867. &Data::Reaction::id
  868. ) | ranges::to_vector;
  869. ranges::sort(some);
  870. ranges::sort(simple);
  871. result.type = isGroup
  872. ? (state->option.current() != Option::All
  873. ? AllowedReactionsType::Some
  874. : AllowedReactionsType::All)
  875. : (some == simple)
  876. ? AllowedReactionsType::Default
  877. : AllowedReactionsType::Some;
  878. return result;
  879. };
  880. box->addButton(tr::lng_settings_save(), [=] {
  881. const auto result = collect();
  882. if (!isGroup) {
  883. const auto custom = ranges::count_if(
  884. result.some,
  885. &Data::ReactionId::custom);
  886. if (custom > args.allowedCustomReactions) {
  887. args.askForBoosts(custom);
  888. return;
  889. }
  890. }
  891. box->closeBox();
  892. args.save(result);
  893. });
  894. box->addButton(tr::lng_cancel(), [=] {
  895. box->closeBox();
  896. });
  897. }
  898. void SaveAllowedReactions(
  899. not_null<PeerData*> peer,
  900. const Data::AllowedReactions &allowed) {
  901. auto ids = allowed.some | ranges::views::transform(
  902. Data::ReactionToMTP
  903. ) | ranges::to<QVector<MTPReaction>>;
  904. using Flag = MTPmessages_SetChatAvailableReactions::Flag;
  905. using Type = Data::AllowedReactionsType;
  906. const auto updated = (allowed.type != Type::Some)
  907. ? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
  908. ? MTPDchatReactionsAll::Flag(0)
  909. : MTPDchatReactionsAll::Flag::f_allow_custom))
  910. : allowed.some.empty()
  911. ? MTP_chatReactionsNone()
  912. : MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
  913. const auto editPaidEnabled = peer->isBroadcast();
  914. const auto paidEnabled = editPaidEnabled && allowed.paidEnabled;
  915. const auto maxCount = allowed.maxCount;
  916. peer->session().api().request(MTPmessages_SetChatAvailableReactions(
  917. MTP_flags(Flag()
  918. | (maxCount ? Flag::f_reactions_limit : Flag())
  919. | (editPaidEnabled ? Flag::f_paid_enabled : Flag())),
  920. peer->input,
  921. updated,
  922. MTP_int(maxCount),
  923. MTP_bool(paidEnabled)
  924. )).done([=](const MTPUpdates &result) {
  925. peer->session().api().applyUpdates(result);
  926. auto parsed = Data::Parse(updated, maxCount, paidEnabled);
  927. if (const auto chat = peer->asChat()) {
  928. chat->setAllowedReactions(parsed);
  929. } else if (const auto channel = peer->asChannel()) {
  930. channel->setAllowedReactions(parsed);
  931. } else {
  932. Unexpected("Invalid peer type in SaveAllowedReactions.");
  933. }
  934. }).fail([=](const MTP::Error &error) {
  935. if (error.type() == u"REACTION_INVALID"_q) {
  936. peer->updateFullForced();
  937. peer->owner().reactions().refreshDefault();
  938. }
  939. }).send();
  940. }