From 43a8733fc7959b9f97d46e98cb6129b1f627a3f0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 Nov 2023 14:35:57 +0400 Subject: [PATCH] Implement rich reactions selector. --- .../icons/info/edit/stickers_add.png | Bin 0 -> 470 bytes .../icons/info/edit/stickers_add@2x.png | Bin 0 -> 899 bytes .../icons/info/edit/stickers_add@3x.png | Bin 0 -> 1345 bytes Telegram/Resources/langs/lang.strings | 11 + .../boxes/peers/edit_peer_color_box.cpp | 2 +- .../boxes/peers/edit_peer_info_box.cpp | 89 +++- .../boxes/peers/edit_peer_reactions.cpp | 479 +++++++++++++++--- .../boxes/peers/edit_peer_reactions.h | 18 +- .../chat_helpers/emoji_list_widget.cpp | 4 + .../chat_helpers/tabbed_selector.cpp | 52 +- .../chat_helpers/tabbed_selector.h | 16 +- .../data/data_message_reaction_id.h | 16 +- .../history_view_reactions_selector.cpp | 157 +++--- .../history_view_reactions_selector.h | 40 +- Telegram/SourceFiles/info/info.style | 11 +- Telegram/SourceFiles/ui/boxes/boost_box.cpp | 21 +- Telegram/SourceFiles/ui/boxes/boost_box.h | 16 +- 17 files changed, 718 insertions(+), 214 deletions(-) create mode 100644 Telegram/Resources/icons/info/edit/stickers_add.png create mode 100644 Telegram/Resources/icons/info/edit/stickers_add@2x.png create mode 100644 Telegram/Resources/icons/info/edit/stickers_add@3x.png diff --git a/Telegram/Resources/icons/info/edit/stickers_add.png b/Telegram/Resources/icons/info/edit/stickers_add.png new file mode 100644 index 0000000000000000000000000000000000000000..d4c77a202c4fe31fcf48f38f030c8f0df0d2be25 GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%)!&eF~maf z?i9nURtJH$_@uss<-O_w0;?Mm-m>m#*}h@>3E8IQ7ra>39$~(*V(QkZM}I$R3$1bU zFmT;;)adj1?VC4mUbp@Bep!EYbz|SlpKW5VC+=EzdefS9uZ909Jo#OB+2pg0xa-G# zJUlTjPVC%rs;+^LEaolK>{1F1-Oiw=q{y%#_r(D=skVdL-ZtM*5@cfd&GqAg=4@X7 zO&v!W64KL`u!`)z{i;;bT9Btgy>8dE)Qjh0<2&~1CU0lsnUOv9*XAudUaneotZ>Sy zPDZvfLMQilcwgSKEqCeZ#wLXWrLogzZkjP!<>coWg9DF*h2KR@{7`SRUi53Nzo?O* zb@y3U-9(8~6AX9`Yn%@1%6841W!3ljMowoQuR(&`*|3bP0l+XkK$!@e| literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/info/edit/stickers_add@2x.png b/Telegram/Resources/icons/info/edit/stickers_add@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f188f440d5c88257b522c9abcd0c829c02f5d77b GIT binary patch literal 899 zcmV-}1AP36P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NE?MXyIR9Fe^n7@i5K@i5(UDiaw zR1-B=A3+QS1<^pC!w2vg6!s1DDOB)JU}R=ys4T1pi{@%%a`(F{wpX@$#%X80n*%41 zw&ts^zNzZ2?wL^Ne%&*0&%kXn5cW?Wi^Wo@6g=DQ_INz{C$we=hr@+Jq1|pTm&^Tr z|MTqPFGg+w0A@{X>+tya*laf6-`~$PI1YgU#>Fa%#Y5EV^_Q2ITrMX84u`{dJYFmo zRBE@|VN52I)GnXTSF6={JT9>HdfjTZ27`f>tP9{sB+}`0R4V54IU%2(o}Apc1pt_G z!5tDOyq%pnI~)#$`t|jdJbCvAjEnd}0+n}PGm^(rYNpd^B9SoToUORvs5JS_ld4p= z+kJR=aE|MC2ghEoCz>F1gSy`%;ljcrUadw#;I8syRrF7VEZ5a)g&-!u6_xrV`WQhX z^9fqpUhtcP79#2K@<7pO)Me3qND8TxL#~XT1~ggeek|RK4oLa&=klUSdI98ieVoQG z>7kum6hx~Oui={o+J#ZWR;Db6>#4yy3{ z{Cr*=N;a8HKvpUh%A{PJN~UO4AmzfqAc(rMGO4kgl+V#JMia*_-HrmI{R>iP31OT- z=90a=y}`oS?+e$r{1sXWp%a<~jYcD&?#Du%%Zr6+F5O>3=q_eK{eB-PRV$TBx{n`A zxm=c>A|bp|1#WXixYhB4LU3Jt-0P1YPzUaSh9>nufjGiS{)2VMM^oTlgU}SL7ey%? zc~Z4wqrT~@^#A#>LGr{ey@397)6X#~mc@WRe!CA=7A13%%&h;%pY)uR-R<`b+%sUx Zz%Qq1$)w8hFSq~z002ovPDHLkV1nQ&jgbHV literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/info/edit/stickers_add@3x.png b/Telegram/Resources/icons/info/edit/stickers_add@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..48de8115e323951613e07414864f0ccc3f964d3f GIT binary patch literal 1345 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbHUhz_QxY#W5s< z_3bQMeUm_uept zOZ0rd^l?LL>+kR0%#A(&#P8ru>*s&Y|DR)79KSq(Bm9H|GTdtvaOOnd$`C18Sy@R* zNjW(=8JV2?{QT_f!`DB5{(SlJ<)=@d9zA;W=g*%vZ{ECpJK9aA=8&1W`T29_-o1T$ z_x}C;)-fv<_Hee@RaP8%G;__`wYGM4RxQGdn-rflR4fV64hasv{O8Y~w{LTK?ypzV znsDo}(~qp~pZpvF4A)MrEc?B3rDoiWqQ2|buS-iyKYsjpPx7<3Lh*l=OpMl85SGWS zuwhYr{=x_yJzZVb3|kwUHEY&f$lbBoQBs)4RK%9+!H$CC9~a-ebm@@y`jvb3?OV5g zeR)~gvf>7y?HN8X4>V*Gq*mX%7q=_(Y(jng{`Kqid9V3PJ2YH6VR7iL|Jt0oiB7Vj zrki){cyV2{;ec1gj?;g327cMa6dxD2Z29uPZmAau4?sF{%oiWvjPDP0&b^S;FLN#nZogFMZ6Abf4tQ@k4(1Oo0w&E4{bY z*^%ETU-F{Zf6HZmbwPd6s7BtH}2jQ?VmNtBust7q3K%< zyn9~tO#OE8;zUhXma8#yHL_(i9CdEqD4K2R>h9j&XklUDaqQHubpj2ON>fw#eO>_5 zM%NtyN8uHw2Ge#4F*EN!!7FIO=&LiW=V>P|4^PL(u#9I*#@)ubn*E`SR?I1g}fK z_oy%4a7JjAP|UHs3<;kO)z2HJByIYbzb{+URdfHmd2-njSA?vldTm_2x_ftddHM3? z%bx};6FGfbS=Y(A!&M}v{@t559+s}#*kcv#zV6|3a9D*S1N5n|H6~ z%t+cOr`!0_?(nU{$E-fwSOF5y(`hWcq?{%!EUaYia)HHUqe)7UHD_A>iyuF3ENM~Q zRoXU9tuoJS$246|P*9x<>XHCv749Qv&h(rp72A^St7+T&za;yB=7XBNX98Bn=W6oy z3r5?S`*7-PkG^x{*vtkyr4uI?vNRSx{VdUWps4zTd(KarRev>(J1Nd@f30RVZ?;`e z+TZEz=M{Z&_T+z2z2rY@t8Ti|>C>k}&rS_|#w=!e!O-FW)4aEhyEqqLia*96ws}*M zc%v=P=aUyNTnI?E6Pby>nF!|clyQ1RvI>gTe~DWM4fuGm(L literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ff69f9773..1aa436225 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1347,6 +1347,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages."; "lng_manage_peer_reactions_some_title" = "Only allow these reactions"; "lng_manage_peer_reactions_available" = "Available reactions"; +"lng_manage_peer_reactions_own" = "You can also {link} emoji packs and use them as reactions."; +"lng_manage_peer_reactions_own_link" = "create your own"; +"lng_manage_peer_reactions_level#one" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reaction."; +"lng_manage_peer_reactions_level#other" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reactions."; +"lng_manage_peer_reactions_boost" = "Boost your channel {link}."; +"lng_manage_peer_reactions_boost_link" = "here"; "lng_manage_peer_antispam" = "Aggressive Anti-Spam"; "lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions."; @@ -2087,6 +2093,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_channel_title_color" = "Enable colors"; "lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color."; "lng_boost_channel_needs_level_color#other" = "Your channel needs to reach **Level {count}** to change channel color."; + +"lng_boost_channel_title_reactions" = "Custom reactions"; +"lng_boost_channel_needs_level_reactions#one" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction."; +"lng_boost_channel_needs_level_reactions#other" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions."; + "lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:"; "lng_boost_channel_ask_button" = "Copy Link"; "lng_boost_channel_or" = "or"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 1ebe39119..f32a4d32a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -527,7 +527,7 @@ void Apply( show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ .link = qs(data.vboost_url()), .boost = counters, - .requiredLevel = required, + .reason = { Ui::AskBoostChannelColor{ required } }, }, openStatistics, nullptr)); cancel(); }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 8b7031826..8f81f0cc0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -22,6 +22,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_linked_chat_box.h" #include "boxes/peers/edit_peer_requests_box.h" #include "boxes/peers/edit_peer_reactions.h" +#include "boxes/peers/replace_boost_box.h" +#include "boxes/peer_list_controllers.h" #include "boxes/stickers_box.h" #include "boxes/username_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -36,12 +38,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_user.h" #include "history/admin_log/history_admin_log_section.h" +#include "info/boosts/info_boosts_widget.h" #include "info/profile/info_profile_values.h" +#include "info/info_memento.h" #include "lang/lang_keys.h" #include "mtproto/sender.h" #include "main/main_account.h" #include "main/main_app_config.h" -#include "settings/settings_common.h" // IconDescriptor. +#include "settings/settings_common.h" +#include "ui/boxes/boost_box.h" #include "ui/controls/userpic_button.h" #include "ui/rp_widget.h" #include "ui/vertical_list.h" @@ -255,6 +260,7 @@ private: Ui::VerticalLayout *buttonsLayout = nullptr; Ui::SettingsButton *forumToggle = nullptr; bool forumToggleLocked = false; + bool levelRequested = false; Ui::SlideWrap<> *historyVisibilityWrap = nullptr; }; struct Saving { @@ -304,6 +310,7 @@ private: void submitDescription(); void deleteWithConfirmation(); void deleteChannel(); + void editReactions(); [[nodiscard]] std::optional validate() const; [[nodiscard]] bool validateUsernamesOrder(Saving &to) const; @@ -1100,36 +1107,21 @@ void Controller::fillManageSection() { return Data::PeerAllowedReactions(peer); }); }) | rpl::flatten_latest(); - auto label = rpl::combine( - std::move(allowedReactions), - Info::Profile::FullReactionsCountValue(session) - ) | rpl::map([=](const Data::AllowedReactions &allowed, int total) { + auto label = std::move( + allowedReactions + ) | rpl::map([=](const Data::AllowedReactions &allowed) { const auto some = int(allowed.some.size()); return (allowed.type != Data::AllowedReactionsType::Some) ? tr::lng_manage_peer_reactions_on(tr::now) : some - ? (QString::number(some) - + " / " - + QString::number(std::max(some, total))) + ? QString::number(some) : tr::lng_manage_peer_reactions_off(tr::now); }); - const auto done = [=](const Data::AllowedReactions &chosen) { - SaveAllowedReactions(_peer, chosen); - }; AddButtonWithCount( _controls.buttonsLayout, tr::lng_manage_peer_reactions(), std::move(label), - [=] { - _navigation->parentController()->show(Box( - EditAllowedReactionsBox, - _navigation, - !_peer->isBroadcast(), - session->data().reactions().list( - Data::Reactions::Type::Active), - Data::PeerAllowedReactions(_peer), - done)); - }, + [=] { editReactions(); }, { &st::menuIconGroupReactions }); } if (canEditPermissions) { @@ -1277,6 +1269,61 @@ void Controller::fillManageSection() { } } +void Controller::editReactions() { + const auto done = [=](const Data::AllowedReactions &chosen) { + SaveAllowedReactions(_peer, chosen); + }; + if (!_peer->isBroadcast()) { + _navigation->uiShow()->show(Box( + EditAllowedReactionsBox, + EditAllowedReactionsArgs{ + .navigation = _navigation, + .isGroup = true, + .list = _navigation->session().data().reactions().list( + Data::Reactions::Type::Active), + .allowed = Data::PeerAllowedReactions(_peer), + .save = done, + })); + return; + } + if (_controls.levelRequested) { + return; + } + _controls.levelRequested = true; + _api.request(MTPpremium_GetBoostsStatus( + _peer->input + )).done([=](const MTPpremium_BoostsStatus &result) { + _controls.levelRequested = false; + const auto link = qs(result.data().vboost_url()); + const auto weak = base::make_weak(_navigation->parentController()); + auto counters = ParseBoostCounters(result); + counters.mine = 0; // Don't show current level as just-reached. + const auto askForBoosts = [=](int required) { + if (const auto strong = weak.get()) { + const auto openStatistics = [=, peer = _peer] { + strong->showSection(Info::Boosts::Make(peer)); + }; + strong->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ + .link = link, + .boost = counters, + .reason = { Ui::AskBoostCustomReactions{ required } }, + }, openStatistics, nullptr)); + } + }; + _navigation->uiShow()->show(Box( + EditAllowedReactionsBox, + EditAllowedReactionsArgs{ + .navigation = _navigation, + .allowedCustomReactions = counters.level, + .list = _navigation->session().data().reactions().list( + Data::Reactions::Type::Active), + .allowed = Data::PeerAllowedReactions(_peer), + .askForBoosts = askForBoosts, + .save = done, + })); + }).send(); +} + void Controller::fillPendingRequestsButton() { auto pendingRequestsCount = Info::Profile::MigratedOrMeValue( _peer diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp index 1ab14337f..b52fbf718 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp @@ -15,13 +15,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_chat.h" #include "data/data_channel.h" +#include "data/data_document.h" #include "data/data_session.h" +#include "history/view/reactions/history_view_reactions_selector.h" #include "main/main_session.h" #include "apiwrap.h" #include "lang/lang_keys.h" +#include "ui/boxes/boost_box.h" #include "ui/widgets/fields/input_field.h" -#include "ui/controls/emoji_button.h" #include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" @@ -30,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_info.h" #include "styles/style_settings.h" +#include "styles/style_layers.h" #include #include @@ -37,6 +41,70 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +constexpr auto kDisabledEmojiOpacity = 0.4; + +struct UniqueCustomEmojiContext { + std::vector ids; +}; + +class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji { +public: + MaybeDisabledEmoji( + std::unique_ptr wrapped, + Fn enabled); + + int width() override; + QString entityData() override; + void paint(QPainter &p, const Context &context) override; + void unload() override; + bool ready() override; + bool readyInDefaultState() override; + +private: + const std::unique_ptr _wrapped; + const Fn _enabled; + +}; + +MaybeDisabledEmoji::MaybeDisabledEmoji( + std::unique_ptr wrapped, + Fn enabled) +: _wrapped(std::move(wrapped)) +, _enabled(std::move(enabled)) { +} + +int MaybeDisabledEmoji::width() { + return _wrapped->width(); +} + +QString MaybeDisabledEmoji::entityData() { + return _wrapped->entityData(); +} + +void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) { + const auto disabled = !_enabled(); + const auto was = disabled ? p.opacity() : 1.; + if (disabled) { + p.setOpacity(kDisabledEmojiOpacity); + } + _wrapped->paint(p, context); + if (disabled) { + p.setOpacity(was); + } +} + +void MaybeDisabledEmoji::unload() { + _wrapped->unload(); +} + +bool MaybeDisabledEmoji::ready() { + return _wrapped->ready(); +} + +bool MaybeDisabledEmoji::readyInDefaultState() { + return _wrapped->readyInDefaultState(); +} + [[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) { auto all = TextUtilities::SplitTags(mimeTag); for (auto i = all.begin(); i != all.end();) { @@ -78,9 +146,11 @@ namespace { Unexpected("Action in MimeData hook."); } -struct UniqueCustomEmojiContext { - base::flat_set ids; -}; +[[nodiscard]] std::vector DefaultSelected() { + const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d"); + const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e"); + return { Data::ReactionId{ like }, Data::ReactionId{ dislike } }; +} [[nodiscard]] bool RemoveNonCustomEmojiFragment( not_null document, @@ -100,10 +170,12 @@ struct UniqueCustomEmojiContext { break; } const auto id = format.property(Ui::InputField::kCustomEmojiId); - if (!context.ids.emplace(id.toULongLong()).second) { + const auto documentId = id.toULongLong(); + if (ranges::contains(context.ids, documentId)) { removeTill += fragment.length(); break; } + context.ids.push_back(documentId); } while (removeTill == removeFrom) { block = block.next(); @@ -131,7 +203,9 @@ bool RemoveNonCustomEmoji( return true; } -void SetupOnlyCustomEmojiField(not_null field) { +void SetupOnlyCustomEmojiField( + not_null field, + Fn)> callback) { field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor); field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook); @@ -164,12 +238,54 @@ void SetupOnlyCustomEmojiField(not_null field) { document->setPageSize(pageSize); } } + callback(context.ids); if (changed) { field->forceProcessContentsChanges(); } }, field->lifetime()); } +[[nodiscard]] TextWithTags ComposeEmojiList( + not_null reactions, + const std::vector &list) { + auto result = TextWithTags(); + const auto size = [&] { + return int(result.text.size()); + }; + auto added = base::flat_set(); + const auto &all = reactions->list(Data::Reactions::Type::All); + const auto add = [&](Data::ReactionId id) { + if (!added.emplace(id).second) { + return; + } + auto unifiedId = id.custom(); + const auto offset = size(); + if (unifiedId) { + result.text.append('@'); + } else { + result.text.append(id.emoji()); + const auto i = ranges::find(all, id, &Data::Reaction::id); + if (i == end(all)) { + return; + } + unifiedId = i->selectAnimation->id; + } + const auto data = Data::SerializeCustomEmojiId(unifiedId); + const auto tag = Ui::InputField::CustomEmojiLink(data); + result.tags.append({ offset, size() - offset, tag }); + }; + for (const auto &id : list) { + add(id); + } + return result; +} + +enum class ReactionsSelectorState { + Active, + Disabled, + Hidden, +}; + struct ReactionsSelectorArgs { not_null outer; not_null controller; @@ -177,13 +293,16 @@ struct ReactionsSelectorArgs { std::vector list; std::vector selected; Fn)> callback; - rpl::producer<> focusRequests; + rpl::producer stateValue; + int customAllowed = 0; + bool all = false; }; object_ptr AddReactionsSelector( not_null parent, ReactionsSelectorArgs &&args) { using namespace ChatHelpers; + using HistoryView::Reactions::UnifiedFactoryOwner; auto result = object_ptr( parent, @@ -191,23 +310,111 @@ object_ptr AddReactionsSelector( Ui::InputField::Mode::MultiLine, std::move(args.title)); const auto raw = result.data(); + const auto session = &args.controller->session(); + const auto owner = &session->data(); + const auto reactions = &owner->reactions(); + const auto customAllowed = args.customAllowed; + + struct State { + std::unique_ptr overlay; + std::unique_ptr unifiedFactoryOwner; + UnifiedFactoryOwner::RecentFactory factory; + base::flat_set allowed; + rpl::lifetime focusLifetime; + }; + const auto state = raw->lifetime().make_state(); + state->unifiedFactoryOwner = std::make_unique( + session, + reactions->list(Data::Reactions::Type::Active)); + state->factory = state->unifiedFactoryOwner->factory(); const auto customEmojiPaused = [controller = args.controller] { return controller->isGifPausedAtLeastFor(PauseReason::Layer); }; - raw->setCustomEmojiFactory( - args.controller->session().data().customEmojiManager().factory(), - std::move(customEmojiPaused)); + raw->setCustomEmojiFactory([=](QStringView data, Fn update) + -> std::unique_ptr { + const auto id = Data::ParseCustomEmojiData(data); + auto result = owner->customEmojiManager().create( + data, + std::move(update)); + if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) { + return std::make_unique( + std::move(result), + [=] { return state->allowed.contains(id); }); + } + using namespace Ui::Text; + return std::make_unique(std::move(result)); + }, std::move(customEmojiPaused)); - SetupOnlyCustomEmojiField(raw); + const auto callback = args.callback; + SetupOnlyCustomEmojiField(raw, [=](std::vector ids) { + auto allowed = base::flat_set(); + auto reactions = std::vector(); + reactions.reserve(ids.size()); + allowed.reserve(std::min(customAllowed, int(ids.size()))); + const auto owner = state->unifiedFactoryOwner.get(); + for (const auto id : ids) { + const auto reactionId = owner->lookupReactionId(id); + if (reactionId.custom() && allowed.size() < customAllowed) { + allowed.emplace(id); + } + reactions.push_back(reactionId); + } + if (state->allowed != allowed) { + state->allowed = std::move(allowed); + raw->rawTextEdit()->update(); + } + callback(std::move(reactions)); + }); + raw->setTextWithTags(ComposeEmojiList(reactions, args.selected)); - std::move(args.focusRequests) | rpl::start_with_next([=] { - raw->setFocusFast(); + using SelectorState = ReactionsSelectorState; + std::move( + args.stateValue + ) | rpl::start_with_next([=](SelectorState value) { + switch (value) { + case SelectorState::Active: + state->overlay = nullptr; + state->focusLifetime.destroy(); + if (raw->empty()) { + raw->setTextWithTags( + ComposeEmojiList(reactions, DefaultSelected())); + } + raw->setDisabled(false); + raw->setFocusFast(); + break; + case SelectorState::Disabled: + state->overlay = std::make_unique(parent); + state->overlay->show(); + raw->geometryValue() | rpl::start_with_next([=](QRect rect) { + state->overlay->setGeometry(rect); + }, state->overlay->lifetime()); + state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto color = st::boxBg->c; + color.setAlphaF(0.5); + QPainter(state->overlay.get()).fillRect( + clip, + color); + }, state->overlay->lifetime()); + [[fallthrough]]; + case SelectorState::Hidden: + if (Ui::InFocusChain(raw)) { + raw->parentWidget()->setFocus(); + } + raw->setDisabled(true); + raw->focusedChanges( + ) | rpl::start_with_next([=](bool focused) { + if (focused) { + raw->parentWidget()->setFocus(); + } + }, state->focusLifetime); + break; + } }, raw->lifetime()); - const auto toggle = Ui::CreateChild( + const auto toggle = Ui::CreateChild( parent.get(), - st::boxAttachEmoji); + st::manageGroupReactions); const auto panel = Ui::CreateChild( args.outer.get(), @@ -216,7 +423,11 @@ object_ptr AddReactionsSelector( nullptr, args.controller->uiShow(), Window::GifPauseReason::Layer, - TabbedSelector::Mode::EmojiOnly)); + (args.all + ? TabbedSelector::Mode::FullReactions + : TabbedSelector::Mode::RecentReactions))); + panel->selector()->provideRecentEmoji( + state->unifiedFactoryOwner->unifiedIdsList()); panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -247,7 +458,13 @@ object_ptr AddReactionsSelector( } return base::EventFilterResult::Continue; }; - base::install_event_filter(args.outer, filterCallback); + for (auto widget = (QWidget*)raw + ; widget && widget != args.outer + ; widget = widget->parentWidget()) { + base::install_event_filter(raw, widget, filterCallback); + } + base::install_event_filter(raw, args.outer, filterCallback); + scheduleUpdateEmojiPanelGeometry(); toggle->installEventFilter(panel); toggle->addClickHandler([=] { @@ -264,33 +481,105 @@ object_ptr AddReactionsSelector( return result; } +void AddReactionsText( + not_null container, + not_null navigation, + int allowedCustomReactions, + rpl::producer customCountValue, + Fn askForBoosts) { + auto ownedInner = object_ptr(container); + const auto inner = ownedInner.data(); + const auto count = inner->lifetime().make_state>( + std::move(customCountValue)); + + auto outer = container->add( + object_ptr( + container, + std::move(ownedInner), + st::defaultBoxDividerLabelPadding), + QMargins(0, st::manageGroupReactionsTextSkip, 0, 0)); + const auto label = inner->add( + object_ptr( + inner, + tr::lng_manage_peer_reactions_own( + lt_link, + tr::lng_manage_peer_reactions_own_link( + ) | Ui::Text::ToLink(), + Ui::Text::WithEntities), + st::boxDividerLabel)); + const auto weak = base::make_weak(navigation); + label->setClickHandlerFilter([=](const auto &...) { + if (const auto strong = weak.get()) { + using Info = Window::SessionNavigation::PeerByLinkInfo; + strong->showPeerByLink(Info{ + .usernameOrId = u"stickers"_q, + .resolveType = Window::ResolveType::Mention, + }); + } + return false; + }); + auto countString = count->value() | rpl::map([](int count) { + return TextWithEntities{ QString::number(count) }; + }); + auto needs = rpl::combine( + tr::lng_manage_peer_reactions_level( + lt_count, + count->value() | tr::to_count(), + lt_same_count, + std::move(countString), + Ui::Text::RichLangValue), + tr::lng_manage_peer_reactions_boost( + lt_link, + tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(), + Ui::Text::RichLangValue) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + a.append(' ').append(std::move(b)); + return std::move(a); + }); + const auto wrap = inner->add( + object_ptr>( + inner, + object_ptr( + inner, + std::move(needs), + st::boxDividerLabel), + QMargins{ 0, st::normalFont->height, 0, 0 })); + wrap->toggleOn(count->value() | rpl::map( + rpl::mappers::_1 > allowedCustomReactions + )); + wrap->finishAnimating(); + + wrap->entity()->setClickHandlerFilter([=](const auto &...) { + askForBoosts(count->current()); + return false; + }); +} + } // namespace void EditAllowedReactionsBox( not_null box, - not_null navigation, - bool isGroup, - const std::vector &list, - const Data::AllowedReactions &allowed, - Fn callback) { + EditAllowedReactionsArgs &&args) { using namespace Data; using namespace rpl::mappers; const auto iconHeight = st::editPeerReactionsPreview; box->setTitle(tr::lng_manage_peer_reactions()); + box->setWidth(st::boxWideWidth); enum class Option { All, Some, None, }; + using SelectorState = ReactionsSelectorState; struct State { - base::flat_map> toggles; rpl::variable