From bd42c23999718499d6329a0b8dbbdc36739ea00a Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 19 Aug 2022 15:08:11 +0300 Subject: [PATCH] Implement reactions selector above the menu. --- Telegram/CMakeLists.txt | 2 + .../Resources/icons/chat/reactions_bubble.png | Bin 0 -> 331 bytes .../icons/chat/reactions_bubble@2x.png | Bin 0 -> 531 bytes .../icons/chat/reactions_bubble@3x.png | Bin 0 -> 772 bytes .../icons/chat/reactions_bubble_shadow.png | Bin 0 -> 496 bytes .../icons/chat/reactions_bubble_shadow@2x.png | Bin 0 -> 967 bytes .../icons/chat/reactions_bubble_shadow@3x.png | Bin 0 -> 1671 bytes .../icons/chat/reactions_expand_bg.png | Bin 0 -> 418 bytes .../icons/chat/reactions_expand_bg@2x.png | Bin 0 -> 803 bytes .../icons/chat/reactions_expand_bg@3x.png | Bin 0 -> 1157 bytes .../icons/chat/reactions_expand_panel.png | Bin 0 -> 244 bytes .../icons/chat/reactions_expand_panel@2x.png | Bin 0 -> 337 bytes .../icons/chat/reactions_expand_panel@3x.png | Bin 0 -> 465 bytes .../boxes/reactions_settings_box.cpp | 2 +- .../chat_helpers/chat_helpers.style | 5 + .../data/data_message_reactions.cpp | 62 + .../SourceFiles/data/data_message_reactions.h | 11 +- .../SourceFiles/data/data_peer_values.cpp | 4 +- Telegram/SourceFiles/data/data_peer_values.h | 2 +- .../history/history_inner_widget.cpp | 62 +- .../history/history_inner_widget.h | 6 +- .../history/view/history_view_list_widget.cpp | 32 +- .../history/view/history_view_list_widget.h | 1 + .../_history_view_reactions_button.cpp | 1422 +++++++++++++++++ .../_history_view_reactions_button.h | 373 +++++ .../history_view_reactions_button.cpp | 826 ++-------- .../reactions/history_view_reactions_button.h | 148 +- .../history_view_reactions_selector.cpp | 358 ++--- .../history_view_reactions_selector.h | 77 +- .../history_view_reactions_strip.cpp | 529 ++++++ .../reactions/history_view_reactions_strip.h | 156 ++ Telegram/SourceFiles/ui/chat/chat.style | 4 + .../SourceFiles/window/section_widget.cpp | 11 + Telegram/SourceFiles/window/section_widget.h | 4 + 34 files changed, 3023 insertions(+), 1074 deletions(-) create mode 100644 Telegram/Resources/icons/chat/reactions_bubble.png create mode 100644 Telegram/Resources/icons/chat/reactions_bubble@2x.png create mode 100644 Telegram/Resources/icons/chat/reactions_bubble@3x.png create mode 100644 Telegram/Resources/icons/chat/reactions_bubble_shadow.png create mode 100644 Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png create mode 100644 Telegram/Resources/icons/chat/reactions_bubble_shadow@3x.png create mode 100644 Telegram/Resources/icons/chat/reactions_expand_bg.png create mode 100644 Telegram/Resources/icons/chat/reactions_expand_bg@2x.png create mode 100644 Telegram/Resources/icons/chat/reactions_expand_bg@3x.png create mode 100644 Telegram/Resources/icons/chat/reactions_expand_panel.png create mode 100644 Telegram/Resources/icons/chat/reactions_expand_panel@2x.png create mode 100644 Telegram/Resources/icons/chat/reactions_expand_panel@3x.png create mode 100644 Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp create mode 100644 Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.h create mode 100644 Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp create mode 100644 Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 46972e2e3f..e8cd57be87 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -671,6 +671,8 @@ PRIVATE history/view/reactions/history_view_reactions_list.h history/view/reactions/history_view_reactions_selector.cpp history/view/reactions/history_view_reactions_selector.h + history/view/reactions/history_view_reactions_strip.cpp + history/view/reactions/history_view_reactions_strip.h history/view/reactions/history_view_reactions_tabs.cpp history/view/reactions/history_view_reactions_tabs.h history/view/history_view_bottom_info.cpp diff --git a/Telegram/Resources/icons/chat/reactions_bubble.png b/Telegram/Resources/icons/chat/reactions_bubble.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c42875716ebbdd65f5595c65214da0245fc30c GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!2~3&3z#Z_6k~CayA#8@b22Z19GBDx&op0O z1}z|)gMqOPiR=-lT$I^6zeKiy-l-ssx$zxkNNbD7N>g8NyIDC|2C!TnC8Zbl==5wW@%*Y^M2 zuY2{HVvAUiyrkldgZa;wOX|+}`RMoiLw}fT7|g5xKKUlNY9GiWp00i_>zopr08++n ArT_o{ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_bubble@2x.png b/Telegram/Resources/icons/chat/reactions_bubble@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d98f191943516e273f02277e31555312553d2b29 GIT binary patch literal 531 zcmeAS@N?(olHy`uVBq!ia0vp^T0pGA!2~1=R@>JADaPU;cPEB*=VV?2IWDOYo@u_m zU{xFpjP02WEFdL7ECs|249p7{8JK}IBS>rk6I@nn0W+Mf0#cZ*C7lISR_p2F7!twx zHq7vni-E+}=*J8peGeFp^$2UkoapV*)|fJdtCg+SZ~2D*zuhOV=@7U%ckV*3eS7wN zulZT?@j^$sjC<_$&W1MvEz2^CBIZ4bx?cJur0&jl+xab%@^4S;x7)SL+v8+gPKJ5H^PPK`Cm5`7b?SS}b6DXq z!)-^UMMsbQ*k|v1*^>Q6$J?B^j~W_&PZ*;0F2uzsHMYNENLV??xVV|=-d`z3i}2_C z%br+uunDy-d|tUNH+mW?gC571kS|+RJi8V}6`mb`O?~ORh1pJgbb$))&;#ENS>+bbodUbHl3O1B{csqLU9yk#UTi7Ck8? zCe+_$;$$PB$hLd)eqFxHw6fd#bBM?K7wC;49vafuU9b8=1G_vhik)8@C&P2#dh zFq`jha`z~sALCxu+z0;;{roW1dFo-?pL2QsFe=2Xf4s0P{S+udJYD@<);T3K0RTsf B(4_zX literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_bubble@3x.png b/Telegram/Resources/icons/chat/reactions_bubble@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..53bbc4cf995a3bdab62188da96081053e8b944a6 GIT binary patch literal 772 zcmeAS@N?(olHy`uVBq!ia0vp^_CRdL!2~4#8(nh%QjEnx?oJHr&dIz4a$Hg)JkxxA z8MJ_G4hF{dOa>N^5+IfWVg?501&j>LK$;OGwtxvPYrlXQ&Nc%nob+{7B?ALfo~Mgr zNCxZM2;F`rLjhaA21Zu{bps8SH|re|0{&ZJ5}B{ zdUx+;Z@+vmZ=GoQ-LkZMvoo#dPWt)a*2ju7&p+SU(AjwTWr&8zk%@Md7w_gB=bt4x zv31*C+<3)D_GFlu?5Jh*b`NQTKI z(~ns-+9`JG>8GbfhK~>YtYMqe?R)8U+3vI( zM*l8@im2JwfakvqENj zgX2CXgN!CCZkB7)Kkyuwa*y}$dbx9AlbVyHT#H%+V~gVhUTOGPKZ2vsowht!~c>P;HR_uw;Ns8K@ zWyS=8o?Q)D$EJKbS~#a{_uUMWf^^aDqkrr6-_28>d@{jc#`)(#%X+xNwWdBSkO*9n zBxdAh$=PH&`|RO|0*-Ti9oZ%|nelcuUie%0f2x&mLrRm(fi!{6UDwS|sHbwfzAx3! z%$JX9Gjn6%G3uEy`xyM@`jkHxxK5x#W{ndtT;%SeG61jNU+r u>^<3%`S0&<*_XZ7?#8~}hTa4AeFyke<6hs8)ti(AN`aoPelF{r5}E+zl|Xj@ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_bubble_shadow.png b/Telegram/Resources/icons/chat/reactions_bubble_shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..27cecc54bfe27bba70bac01d27150deae59a7602 GIT binary patch literal 496 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!2~3&3z#Z_6k~CayA#8@b22Z19GBDx&op0O z1}z|)gMqOJFh8#r>$Ka1n^-tS(wJ8u1FoAs}`K9sQhbAP)fSK5p1r}ZoAh)0)i=kg1+#HniBOI{#w z=#$QAKl_6pmV|T!Og!^pe)WV0Uk|Wr9S?r?ZlW8rZly$9p-gh|{eO*W6O)B(*(Ixs zUNJI9epq;<(@gwKr*ohP&!egTL>TwzX`FqzMUPP}xv`uVfQ?_Fr?G?wkBHGj-_uVf)>7nTrQ=)M2=ABjt$FJ&t4{JUzeu9_YY!9iS7w?|J8-w{q#GXtQ@dKxkZLWWvc$af2(G9 SzWrnh3T;nUKbLh*2~7agQN8j2 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png b/Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d6875ca3220304d396f3570e3443b14f53bf871f GIT binary patch literal 967 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 RbMpWI002ovPDHLkV1g?)8SnrA literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg.png b/Telegram/Resources/icons/chat/reactions_expand_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..9c96eedd59bfa7201083331e1f8b39859f5f3c34 GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9oCO|{#S9GG!XV7ZFl!D-#cod* z#}JF&t5Y`eGAr`9PE>u*-a0*rzltr9(R*Fw0cI6ej=KvdD9c>ZT)*&_|BVFc&+-aN zf~^8GB11!`&UEH*Y3bN3_Iysq9=48-;&)Dr4EKIdeAMw>j-hRlr*|>0_N?t9c2_z& z;(r-Ax%|2(d~y9WCc&%!rX7x|{wTowOTl>k-Hw_5N};jBD_^bL@;fB(T&3>)rqC%t zQNmjjM2|{bcd0G7-EOkmZOMl8jI+l~FJ<_Pky^R$~PV84F*qF zKbLh*2~F!>-{EXuvrQoPk^RhDtrsk~ecgBddMhBcQu4}@)>Rh^WjsImaEo>n&ttpf z#5(J_C2tD%WvNfQ)-B6?!yz<}LvM;lWu(<`70vD z++A+9+=`BWyDitM>-Un}=#^EOg#|&Op|RIp13mm7IM|;z)~I>*fmPw|<<+I1irA1H F1_0NNtnUB- literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..827ac5f141dc582f0377a8ad30ef293f67d0f090 GIT binary patch literal 803 zcmV+;1Kj+HP)LP!>Yv zIxVqio!U%+g7_tXaU74t=|GO&(T(xvA#`$b3{UT8l+q}Q!Z7@Of)J{oR8@7oUT?RX zQPm{~A!(X&&f5)e&eJp{gnUE=V+;UN$}K8Gq?7=FF*a$#7!yM5RT(0LV2l}1@jMRz zlu}1*I+RiX;CY&}79oUu-!F>dxXO^CD16`7%*MD)@1&Cx0DQux0|55fFvg6`n^!2M zcFb{%@xgCu3qpv+wcuy|DoK(c2=+y~pCAa5Bv~Iv2+>&m^pH}{*YRmWaU2_7RbPWK zj^lV`(8hhr6rA&SHk4AEHXW4Gp~L7ei=s#~{R2#jj)cN6v@5evbR^WBvn(?y>YUJM zlao?f`{sKdkH_V5=^sZ4wQXzJW?pn8bid#2$}AKe3DtFNS7xE8+d@@U*_Bx<>cZAB z7Opikis~)#ndOd`YEX%exJ+my^+w}bBZQ+xXIe+j}dOl*7)r|lE002ovPDHLk zV1glpR|By#CUBsyO$hHPV~HU^GSF8zItbDK!nYr;%p3x#@p#O0^s}ypGkHO~Ua!ey zlI!npu70^(LZMKxSmfIMs0M?3>3j}dKkFmldDy=TBQl`a|k*dF7{L-={f=W&fViWUwB>Y&N4A2!`yThIOr08;wS^_4muj zlULs{Tw+L-%aB%|-G5jS_-A1{oobQbjIb9VRen5b+wC@oWuuCHyw~e_c;4-HyI3qn zGhpu2v_dXRzJ-s+V>+GA=kxLi(9Y*`teeedLt8o|M28T{sZ=VL%UL}cZnxWNwZca> hrj*eA-S~qt@DD*34|6S0@fZLA002ovPDHLkV1kP$bf5qL literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3aaa0e2728d2d860836f10f5a3532b1bf0c6bf53 GIT binary patch literal 1157 zcmV;01bX|4P)S(cvXsj51(4FCWbh7pEg7oR3!7#fB#n35z(uIu7e z`ibkhk|gzQWtwJ^B;>4Wk|Z%rvp*}_wyUZlZBCOBjZ2tTas5z1A-tNkH^_;_V)Scm+STV_4Rc=pPPsNBEPWNY<|BL0L*5y&1Qo< zgce(`*Cg}cLLQ9i{A0Os?#qA1PJ5n`*= zihLfNAOj=BWLYL31}DhCV-gcZk$f1OAOj=B>P7Ys0vQ-F+~;}zL8Br76h+Zo3qeoN z6o3#*)3pB_T|fpNli2BWA|D1P$iN7(IF8AO!3i?(n8Xf;1NksGK?WYv-pF?DbNg}%UNlX^RZnkI-5||!x$N>n!E(987-o6}WMX1sVif!bZ-kvGFOLXJ00000 zNkvXXu0mjf$)7ZUBui9k$Z_66Yz@#1Ybu9A=ywc^aasn4fz+s5eGf&&)zwu!C~!E% zj4%td`SJ0gb8AA{ZKP3jZf@2a&;35VB>2$hYuS>#& z2L>-Ol&(^#l#RIY5<`$49v(2W$^1-?XHo+(s3C9r`}?_EPV#XQP_+>oh6H0tGMRjS zejbmIh3#&)%X-&C3|24)tgo-1oSd*0r!Tp_z83EkOEqhcgW#nIg+k(q4K3olh4JI* z|1Fd_e-;V_vsjJn1_A*ce<iMaZSk1#-9UVJ@R%LDHTay@Q7{2f8E1(Swp00i_ z>zoprjE~j3ho1FuIj7&y$#FpJU>8TZzU&$ULB<)VUBIS(7~v_8CG*uu-iyTmqXQ`<&)~D!1KSfZ)r< zC~cs|UAscBZd|*yPM5<0DER*FnP=^3*Q({C8H^{N4vN%Vy4CDvO4{13L8mTtYOboP zzIgI|$^YDAPkQg~Eqkya^X0?&=5s@gE8kq1HC0D^z_MH8EhJVXp%1MOyD@O1TaS?83{qs%H(S=}gyr)&$vtOcUK<%7mv~yl z;g}u!`cw3ptv~&|R(eSrFQ4*GKRWxg-(R_SXBRKc=XVa8rInr2T)N6fYsuDszY|vN z*?zoSRsQGA`H!wm*|e=|`KuL1FN#7#(_**0da%PgytAp3H>5MxdG<=7{ByZ$y!Oc4 zO_^F@wD+ObM$rppQ-5q<`%~-Vp0!uCHeS6}Hnk$>cbisX&&PY-Q@`JnC_dzT`v2z} zOJDCwl<3Hq_q;Ure5D4=LktJ>%v%|bM!Wp_(gPCnboFyt=akU&$E0R=>7nKCMeZHl zBEE9{eE item) { + if (!item->canReact()) { + return {}; + } + auto result = PossibleItemReactions(); + const auto peer = item->history()->peer; + const auto session = &peer->session(); + const auto reactions = &session->data().reactions(); + const auto &full = reactions->list(Reactions::Type::Active); + const auto &all = item->reactions(); + const auto my = item->chosenReaction(); + auto myIsUnique = false; + for (const auto &[id, count] : all) { + if (count == 1 && id == my) { + myIsUnique = true; + } + } + const auto notMineCount = int(all.size()) - (myIsUnique ? 1 : 0); + const auto limit = UniqueReactionsLimit(peer); + if (limit > 0 && notMineCount >= limit) { + result.recent.reserve(all.size()); + for (const auto &reaction : full) { + const auto id = reaction.id; + if (all.contains(id)) { + result.recent.push_back(&reaction); + } + } + } else { + const auto filter = PeerReactionsFilter(peer); + result.recent.reserve(filter.allowed + ? filter.allowed->size() + : full.size()); + for (const auto &reaction : full) { + const auto id = reaction.id; + const auto emoji = filter.allowed ? id.emoji() : QString(); + if (filter.allowed + && (emoji.isEmpty() || !filter.allowed->contains(emoji))) { + continue; + } else if (reaction.premium + && !session->premium() + && !all.contains(id)) { + if (session->premiumPossible()) { + result.morePremiumAvailable = true; + } + continue; + } else { + result.recent.push_back(&reaction); + } + } + result.customAllowed = session->premium() && peer->isUser(); + } + const auto i = ranges::find( + result.recent, + reactions->favorite(), + &Reaction::id); + if (i != end(result.recent) && i != begin(result.recent)) { + std::rotate(begin(result.recent), i, i + 1); + } + return result; +} + Reactions::Reactions(not_null owner) : _owner(owner) , _repaintTimer([=] { repaintCollected(); }) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index b47f72ac30..8dbb3b7824 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -37,6 +37,15 @@ struct Reaction { bool premium = false; }; +struct PossibleItemReactions { + std::vector> recent; + bool morePremiumAvailable = false; + bool customAllowed = false; +}; + +[[nodiscard]] PossibleItemReactions LookupPossibleReactions( + not_null item); + class Reactions final { public: explicit Reactions(not_null owner); @@ -115,7 +124,7 @@ private: ReactionId _favorite; base::flat_map< not_null, - std::shared_ptr> _iconsCache; + std::shared_ptr> _iconsCache; rpl::event_stream<> _updated; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index eff154d101..89f0616f10 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -542,8 +542,8 @@ int UniqueReactionsLimit(not_null peer) { } rpl::producer UniqueReactionsLimitValue( - not_null session) { - const auto config = &session->account().appConfig(); + not_null peer) { + const auto config = &peer->session().account().appConfig(); return config->value( ) | rpl::map([=] { return UniqueReactionsLimit(config); diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index 61866e7fa4..c4c13475fd 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -140,6 +140,6 @@ inline auto PeerFullFlagValue( [[nodiscard]] int UniqueReactionsLimit(not_null peer); [[nodiscard]] rpl::producer UniqueReactionsLimitValue( - not_null session); + not_null peer); } // namespace Data diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0ae16886d8..374846d159 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -342,10 +342,8 @@ HistoryInner::HistoryInner( , _reactionsManager( std::make_unique( this, - Data::UniqueReactionsLimitValue(&controller->session()), [=](QRect updated) { update(updated); }, controller->cachedReactionIconFactory().createMethod())) -, _reactionsSelector(std::make_unique()) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) @@ -394,26 +392,16 @@ HistoryInner::HistoryInner( _controller->emojiInteractions().playStarted(_peer, std::move(emoji)); }, lifetime()); - rpl::merge( - _reactionsManager->chosen(), - _reactionsSelector->chosen() + _reactionsManager->chosen( ) | rpl::start_with_next([=](ChosenReaction reaction) { _reactionsManager->updateButton({}); reactionChosen(reaction); }, lifetime()); - _reactionsManager->setExternalSelectorShown(_reactionsSelector->shown()); - _reactionsManager->expandSelectorRequests( - ) | rpl::start_with_next([=](ReactionExpandRequest request) { - if (request.expanded) { - _reactionsSelector->show( - _controller, - this, - request.context, - request.button); - } else { - _reactionsSelector->hide(); - } + _reactionsManager->premiumPromoChosen( + ) | rpl::start_with_next([=](FullMsgId context) { + _reactionsManager->updateButton({}); + premiumPromoChosen(context); }, lifetime()); session().data().itemRemoved( @@ -448,7 +436,6 @@ HistoryInner::HistoryInner( return item->mainView() != nullptr; }) | rpl::start_with_next([=](not_null item) { item->mainView()->itemDataChanged(); - _reactionsManager->updateUniqueLimit(item); }, lifetime()); session().changes().historyUpdates( @@ -460,8 +447,7 @@ HistoryInner::HistoryInner( HistoryView::Reactions::SetupManagerList( _reactionsManager.get(), - &session(), - Data::PeerReactionsFilterValue(_peer)); + _reactionsItem.value()); controller->adaptive().chatWideValue( ) | rpl::start_with_next([=](bool wide) { @@ -477,8 +463,6 @@ HistoryInner::HistoryInner( } void HistoryInner::reactionChosen(const ChosenReaction &reaction) { - const auto guard = gsl::finally([&] { _reactionsSelector->hide(); }); - const auto item = session().data().message(reaction.context); if (!item || Window::ShowReactPremiumError( @@ -501,6 +485,12 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { } } +void HistoryInner::premiumPromoChosen(FullMsgId context) { + if (const auto item = session().data().message(context)) { + ShowPremiumPromoBox(_controller, item); + } +} + Main::Session &HistoryInner::session() const { return _controller->session(); } @@ -1740,6 +1730,9 @@ void HistoryInner::itemRemoved(not_null item) { return; } + if (_reactionsItem.current() == item) { + _reactionsItem = nullptr; + } _animatedStickersPlayed.remove(item); _reactionsManager->remove(item->fullId()); @@ -1947,16 +1940,13 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) { } void HistoryInner::toggleFavoriteReaction(not_null view) const { - const auto favorite = session().data().reactions().favorite(); - const auto &filter = _reactionsManager->filter(); - if (favorite.emoji().isEmpty() && !filter.customAllowed) { - return; - } else if (filter.allowed - && !filter.allowed->contains(favorite.emoji())) { - return; - } const auto item = view->data(); - if (Window::ShowReactPremiumError(_controller, item, favorite)) { + const auto favorite = session().data().reactions().favorite(); + if (!ranges::contains( + Data::LookupPossibleReactions(item).recent, + favorite, + &Data::Reaction::id) + || Window::ShowReactPremiumError(_controller, item, favorite)) { return; } else if (item->chosenReaction() != favorite) { if (const auto top = itemTop(view); top >= 0) { @@ -2454,7 +2444,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { ? Element::Hovered()->data().get() : nullptr; const auto attached = reactItem - ? AttachSelectorToMenu(_menu.get(), desiredPosition, reactItem) + ? AttachSelectorToMenu( + _menu.get(), + desiredPosition, + reactItem, + [=](ChosenReaction reaction) { reactionChosen(reaction); }, + [=](FullMsgId context) { premiumPromoChosen(context); }, + _controller->cachedReactionIconFactory().createMethod()) : AttachSelectorResult::Skipped; if (attached == AttachSelectorResult::Failed) { _menu = nullptr; @@ -3417,7 +3413,7 @@ void HistoryInner::mouseActionUpdate() { m, reactionState)); if (changed) { - _reactionsManager->updateUniqueLimit(item); + _reactionsItem = item; } if (view->pointState(m) != PointState::Outside) { if (Element::Hovered() != view) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 3d90999df2..11b4a01147 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -36,9 +36,7 @@ class Element; namespace HistoryView::Reactions { class Manager; -class Selector; struct ChosenReaction; -struct ExpandRequest; struct ButtonParameters; } // namespace HistoryView::Reactions @@ -229,7 +227,6 @@ private: class BotAbout; using ChosenReaction = HistoryView::Reactions::ChosenReaction; - using ReactionExpandRequest = HistoryView::Reactions::ExpandRequest; using VideoUserpic = Dialogs::Ui::VideoUserpic; using SelectedItems = std::map>; enum class MouseAction { @@ -402,6 +399,7 @@ private: -> HistoryView::Reactions::ButtonParameters; void toggleFavoriteReaction(not_null view) const; void reactionChosen(const ChosenReaction &reaction); + void premiumPromoChosen(FullMsgId context); void setupSharingDisallowed(); [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; @@ -464,7 +462,7 @@ private: std::unique_ptr> _videoUserpics; std::unique_ptr _reactionsManager; - std::unique_ptr _reactionsSelector; + rpl::variable _reactionsItem; MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 0cb72a5b63..5c5d9fe9a1 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -277,7 +277,6 @@ ListWidget::ListWidget( , _reactionsManager( std::make_unique( this, - Data::UniqueReactionsLimitValue(&controller->session()), [=](QRect updated) { update(updated); }, controller->cachedReactionIconFactory().createMethod())) , _scrollDateCheck([this] { scrollDateCheck(); }) @@ -379,10 +378,17 @@ ListWidget::ListWidget( } }, lifetime()); + _reactionsManager->premiumPromoChosen( + ) | rpl::start_with_next([=] { + _reactionsManager->updateButton({}); + if (const auto item = _reactionsItem.current()) { + ShowPremiumPromoBox(_controller, item); + } + }, lifetime()); + Reactions::SetupManagerList( _reactionsManager.get(), - &session(), - _delegate->listAllowedReactionsValue()); + _reactionsItem.value()); controller->adaptive().chatWideValue( ) | rpl::start_with_next([=](bool wide) { @@ -2115,16 +2121,13 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) { } void ListWidget::toggleFavoriteReaction(not_null view) const { - const auto favorite = session().data().reactions().favorite(); - const auto &filter = _reactionsManager->filter(); - if (favorite.emoji().isEmpty() && !filter.customAllowed) { - return; - } else if (filter.allowed - && !filter.allowed->contains(favorite.emoji())) { - return; - } const auto item = view->data(); - if (Window::ShowReactPremiumError(_controller, item, favorite)) { + const auto favorite = session().data().reactions().favorite(); + if (!ranges::contains( + Data::LookupPossibleReactions(item).recent, + favorite, + &Data::Reaction::id) + || Window::ShowReactPremiumError(_controller, item, favorite)) { return; } else if (item->chosenReaction() != favorite) { if (const auto top = itemTop(view); top >= 0) { @@ -2727,7 +2730,7 @@ void ListWidget::mouseActionUpdate() { reactionState) : Reactions::ButtonParameters()); if (viewChanged && view) { - _reactionsManager->updateUniqueLimit(item); + _reactionsItem = item; } TextState dragState; @@ -3161,6 +3164,9 @@ void ListWidget::viewReplaced(not_null was, Element *now) { } void ListWidget::itemRemoved(not_null item) { + if (_reactionsItem.current() == item) { + _reactionsItem = nullptr; + } if (_selectedTextItem == item) { clearTextSelection(); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index f9b433c21a..1f50c4b151 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -586,6 +586,7 @@ private: base::unique_qptr _emptyInfo = nullptr; std::unique_ptr _reactionsManager; + rpl::variable _reactionsItem; int _minHeight = 0; int _visibleTop = 0; diff --git a/Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp new file mode 100644 index 0000000000..e4578c7b5f --- /dev/null +++ b/Telegram/SourceFiles/history/view/reactions/_history_view_reactions_button.cpp @@ -0,0 +1,1422 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_react_button.h" + +#include "history/view/history_view_cursor_state.h" +#include "history/history_item.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/message_bubble.h" +#include "ui/widgets/popup_menu.h" +#include "data/data_message_reactions.h" +#include "data/data_session.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_peer_values.h" +#include "lang/lang_keys.h" +#include "core/click_handler_types.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "base/event_filter.h" +#include "styles/style_chat.h" +#include "styles/style_menu_icons.h" + +namespace HistoryView::Reactions { +namespace { + +constexpr auto kDivider = 4; +constexpr auto kToggleDuration = crl::time(120); +constexpr auto kActivateDuration = crl::time(150); +constexpr auto kExpandDuration = crl::time(300); +constexpr auto kCollapseDuration = crl::time(250); +constexpr auto kEmojiCacheIndex = 0; +constexpr auto kButtonShowDelay = crl::time(300); +constexpr auto kButtonExpandDelay = crl::time(25); +constexpr auto kButtonHideDelay = crl::time(300); +constexpr auto kButtonExpandedHideDelay = crl::time(0); +constexpr auto kSizeForDownscale = 96; +constexpr auto kHoverScaleDuration = crl::time(200); +constexpr auto kHoverScale = 1.24; +constexpr auto kMaxReactionsScrollAtOnce = 2; + +[[nodiscard]] QPoint LocalPosition(not_null e) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return e->position().toPoint(); +#else // Qt >= 6.0 + return e->pos(); +#endif // Qt >= 6.0 +} + +[[nodiscard]] QSize CountMaxSizeWithMargins(style::margins margins) { + return QRect( + QPoint(), + st::reactionCornerSize + ).marginsAdded(margins).size(); +} + +[[nodiscard]] QSize CountOuterSize() { + return CountMaxSizeWithMargins(st::reactionCornerShadow); +} + +[[nodiscard]] int CornerImageSize(float64 scale) { + return int(base::SafeRound(st::reactionCornerImage * scale)); +} + +[[nodiscard]] int MainReactionSize() { + return style::ConvertScale(kSizeForDownscale); +} + +[[nodiscard]] std::shared_ptr CreateIcon( + not_null media, + int size, + int frame) { + Expects(media->loaded()); + + return std::make_shared(Lottie::IconDescriptor{ + .path = media->owner()->filepath(true), + .json = media->bytes(), + .sizeOverride = QSize(size, size), + .frame = frame, + }); +} + +} // namespace + +Button::Button( + Fn update, + ButtonParameters parameters, + Fn toggleExpanded, + Fn hide) +: _update(std::move(update)) +, _toggleExpanded(std::move(toggleExpanded)) +, _finalScale(ScaleForState(_state)) +, _collapsed(QPoint(), CountOuterSize()) +, _finalHeight(_collapsed.height()) +, _expandTimer([=] { _toggleExpanded(true); }) +, _hideTimer(hide) { + applyParameters(parameters, nullptr); +} + +Button::~Button() = default; + +void Button::expandWithoutCustom() { + applyState(State::Inside, _update); +} + +bool Button::isHidden() const { + return (_state == State::Hidden) && !_opacityAnimation.animating(); +} + +QRect Button::geometry() const { + return _geometry; +} + +int Button::expandedHeight() const { + return _expandedHeight; +} + +int Button::scroll() const { + return _scroll; +} + +int Button::scrollMax() const { + return _expandedInnerHeight - _expandedHeight; +} + +float64 Button::expandAnimationOpacity(float64 expandRatio) const { + return (_collapseType == CollapseType::Fade) + ? expandRatio + : 1.; +} + +int Button::expandAnimationScroll(float64 expandRatio) const { + return (_collapseType == CollapseType::Scroll && expandRatio < 1.) + ? std::clamp(int(base::SafeRound(expandRatio * _scroll)), 0, _scroll) + : _scroll; +} + +bool Button::expandUp() const { + return (_expandDirection == ExpandDirection::Up); +} + +bool Button::consumeWheelEvent(not_null e) { + const auto scrollMax = (_expandedInnerHeight - _expandedHeight); + if (_state != State::Inside + || scrollMax <= 0 + || !_geometry.contains(LocalPosition(e))) { + return false; + } + const auto delta = e->angleDelta(); + const auto horizontal = std::abs(delta.x()) > std::abs(delta.y()); + if (horizontal) { + return false; + } + const auto between = st::reactionCornerSkip; + const auto oneHeight = (st::reactionCornerSize.height() + between); + const auto max = oneHeight * kMaxReactionsScrollAtOnce; + const auto shift = std::clamp( + delta.y() * (expandUp() ? 1 : -1), + -max, + max); + _scroll = std::clamp(_scroll + shift, 0, scrollMax); + _update(_geometry); + e->accept(); + return true; +} + +void Button::applyParameters(ButtonParameters parameters) { + applyParameters(std::move(parameters), _update); +} + +void Button::applyParameters( + ButtonParameters parameters, + Fn update) { + const auto shift = parameters.center - _collapsed.center(); + _collapsed = _collapsed.translated(shift); + updateGeometry(update); + const auto inner = _geometry.marginsRemoved(st::reactionCornerShadow); + const auto active = inner.marginsAdded( + st::reactionCornerActiveAreaPadding + ).contains(parameters.pointer); + const auto inside = inner.contains(parameters.pointer) + || (active && (_state == State::Inside)); + if (_state != State::Inside && !_heightAnimation.animating()) { + updateExpandDirection(parameters); + } + const auto delayInside = inside && (_state != State::Inside); + if (!delayInside) { + _expandTimer.cancel(); + _lastGlobalPosition = std::nullopt; + } else { + const auto globalPositionChanged = _lastGlobalPosition + && (*_lastGlobalPosition != parameters.globalPointer); + if (globalPositionChanged || _state == State::Hidden) { + _expandTimer.callOnce(kButtonExpandDelay); + } + _lastGlobalPosition = parameters.globalPointer; + } + const auto wasInside = (_state == State::Inside); + const auto state = (inside && !delayInside) + ? State::Inside + : active + ? State::Active + : State::Shown; + applyState(state, update); + if (parameters.outside && _state == State::Shown) { + _hideTimer.callOnce(wasInside + ? kButtonExpandedHideDelay + : kButtonHideDelay); + } else { + _hideTimer.cancel(); + } +} + +void Button::updateExpandDirection(const ButtonParameters ¶meters) { + const auto maxAddedHeight = (parameters.reactionsCount - 1) + * (st::reactionCornerSize.height() + st::reactionCornerSkip) + + (parameters.reactionsCount > 1 ? 2 * st::reactionExpandedSkip : 0); + _expandedInnerHeight = _collapsed.height() + maxAddedHeight; + const auto addedHeight = std::min( + maxAddedHeight, + st::reactionCornerAddedHeightMax); + _expandedHeight = _collapsed.height() + addedHeight; + _scroll = std::clamp(_scroll, 0, scrollMax()); + if (parameters.reactionsCount < 2) { + return; + } + const auto up = (_collapsed.y() - addedHeight >= parameters.visibleTop) + || (_collapsed.y() + _collapsed.height() + addedHeight + > parameters.visibleBottom); + _expandDirection = up ? ExpandDirection::Up : ExpandDirection::Down; +} + +void Button::updateGeometry(Fn update) { + const auto added = int(base::SafeRound( + _heightAnimation.value(_finalHeight) + )) - _collapsed.height(); + if (!added && _state != State::Inside) { + _scroll = 0; + } + const auto geometry = _collapsed.marginsAdded({ + 0, + (_expandDirection == ExpandDirection::Up) ? added : 0, + 0, + (_expandDirection == ExpandDirection::Down) ? added : 0, + }); + if (_geometry != geometry) { + if (update) { + update(_geometry); + } + _geometry = geometry; + if (update) { + update(_geometry); + } + } +} + +void Button::applyState(State state) { + applyState(state, _update); +} + +void Button::applyState(State state, Fn update) { + if (state == State::Hidden) { + _expandTimer.cancel(); + _hideTimer.cancel(); + } + const auto finalHeight = (state == State::Hidden) + ? _heightAnimation.value(_finalHeight) + : (state == State::Inside) + ? _expandedHeight + : _collapsed.height(); + if (_finalHeight != finalHeight) { + if (state == State::Hidden) { + _heightAnimation.stop(); + } else { + if (!_heightAnimation.animating()) { + _collapseType = (_scroll < st::reactionCollapseFadeThreshold) + ? CollapseType::Scroll + : CollapseType::Fade; + } + _heightAnimation.start( + [=] { updateGeometry(_update); }, + _finalHeight, + finalHeight, + (state == State::Inside + ? kExpandDuration + : kCollapseDuration), + anim::easeOutCirc); + } + _finalHeight = finalHeight; + } + updateGeometry(update); + if (_state == state) { + return; + } + const auto duration = (state == State::Hidden || _state == State::Hidden) + ? kToggleDuration + : kActivateDuration; + const auto finalScale = ScaleForState(state); + _opacityAnimation.start( + [=] { _update(_geometry); }, + OpacityForScale(ScaleForState(_state)), + OpacityForScale(ScaleForState(state)), + duration, + anim::sineInOut); + if (state != State::Hidden && _finalScale != finalScale) { + _scaleAnimation.start( + [=] { _update(_geometry); }, + _finalScale, + finalScale, + duration, + anim::sineInOut); + _finalScale = finalScale; + } + _state = state; + _toggleExpanded(false); +} + +float64 Button::ScaleForState(State state) { + switch (state) { + case State::Hidden: return 1. / 3; + case State::Shown: return 2. / 3; + case State::Active: + case State::Inside: return 1.; + } + Unexpected("State in ReactionButton::ScaleForState."); +} + +float64 Button::OpacityForScale(float64 scale) { + return std::min( + ((scale - ScaleForState(State::Hidden)) + / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))), + 1.); +} + +float64 Button::currentScale() const { + return _scaleAnimation.value(_finalScale); +} + +float64 Button::currentOpacity() const { + return _opacityAnimation.value(OpacityForScale(ScaleForState(_state))); +} + +Manager::Manager( + QWidget *wheelEventsTarget, + rpl::producer uniqueLimitValue, + Fn buttonUpdate, + IconFactory iconFactory) +: _iconFactory(std::move(iconFactory)) +, _outer(CountOuterSize()) +, _inner(QRect({}, st::reactionCornerSize)) +, _cachedRound( + st::reactionCornerSize, + st::reactionCornerShadow, + _inner.width()) +, _uniqueLimit(std::move(uniqueLimitValue)) +, _buttonShowTimer([=] { showButtonDelayed(); }) +, _buttonUpdate(std::move(buttonUpdate)) { + _inner.translate(QRect({}, _outer).center() - _inner.center()); + + _emojiParts = _cachedRound.PrepareFramesCache(_outer); + _expandedBuffer = _cachedRound.PrepareImage(QSize( + _outer.width(), + _outer.height() + st::reactionCornerAddedHeightMax)); + if (wheelEventsTarget) { + stealWheelEvents(wheelEventsTarget); + } + + _uniqueLimit.changes( + ) | rpl::start_with_next([=] { + applyListFilters(); + }, _lifetime); + + _createChooseCallback = [=](ReactionId id) { + return [=] { + if (auto chosen = lookupChosen(id)) { + _chosen.fire(std::move(chosen)); + } + }; + }; +} + +ChosenReaction Manager::lookupChosen(const ReactionId &id) const { + auto result = ChosenReaction{ + .context = _buttonContext, + .id = id, + }; + const auto button = _button.get(); + const auto i = ranges::find(_icons, id, &ReactionIcons::id); + if (i == end(_icons) || !button) { + return result; + } + const auto &icon = *i; + if (const auto &appear = icon->appear; appear && appear->animating()) { + result.icon = CreateIcon( + icon->appearAnimation->activeMediaView().get(), + appear->width(), + appear->frameIndex()); + } else if (const auto &select = icon->select) { + result.icon = CreateIcon( + icon->selectAnimation->activeMediaView().get(), + select->width(), + select->frameIndex()); + } + const auto index = (i - begin(_icons)); + const auto between = st::reactionCornerSkip; + const auto oneHeight = (st::reactionCornerSize.height() + between); + const auto expanded = (_icons.size() > 1); + const auto skip = (expanded ? st::reactionExpandedSkip : 0); + const auto scroll = button->scroll(); + const auto local = skip + index * oneHeight - scroll; + const auto geometry = button->geometry(); + const auto top = button->expandUp() + ? (geometry.height() - local - _outer.height()) + : local; + const auto rect = QRect(geometry.topLeft() + QPoint(0, top), _outer); + const auto imageSize = int(base::SafeRound( + st::reactionCornerImage * kHoverScale)); + result.geometry = QRect( + rect.x() + (rect.width() - imageSize) / 2, + rect.y() + (rect.height() - imageSize) / 2, + imageSize, + imageSize); + return result; +} + +bool Manager::applyUniqueLimit() const { + const auto limit = _uniqueLimit.current(); + return _buttonContext + && (limit > 0) + && (_buttonAlreadyNotMineCount >= limit); +} + +void Manager::applyListFilters() { + const auto limited = applyUniqueLimit(); + auto icons = std::vector>(); + icons.reserve(_list.size()); + auto showPremiumLock = (ReactionIcons*)nullptr; + auto favoriteIndex = -1; + for (auto &icon : _list) { + const auto &id = icon.id; + const auto add = limited + ? _buttonAlreadyList.contains(id) + : id.emoji().isEmpty() + ? _filter.customAllowed + : (!_filter.allowed || _filter.allowed->contains(id.emoji())); + if (add) { + if (icon.premium + && !_allowSendingPremium + && !_buttonAlreadyList.contains(id)) { + if (_premiumPossible) { + showPremiumLock = &icon; + } else { + clearStateForHidden(icon); + } + } else { + icon.premiumLock = false; + if (id == _favorite) { + favoriteIndex = int(icons.size()); + } + icons.push_back(&icon); + } + } else { + clearStateForHidden(icon); + } + } + if (showPremiumLock) { + showPremiumLock->premiumLock = true; + icons.push_back(showPremiumLock); + } + if (favoriteIndex > 0) { + const auto first = begin(icons); + std::rotate(first, first + favoriteIndex, first + favoriteIndex + 1); + } + if (!limited && _filter.customAllowed && icons.size() > 1) { + icons.erase(begin(icons) + 1, end(icons)); + } + if (_icons == icons) { + return; + } + const auto selected = _selectedIcon; + setSelectedIcon(-1); + _icons = std::move(icons); + setSelectedIcon((selected < _icons.size()) ? selected : -1); + resolveMainReactionIcon(); +} + +void Manager::stealWheelEvents(not_null target) { + base::install_event_filter(target, [=](not_null e) { + if (e->type() != QEvent::Wheel + || !consumeWheelEvent(static_cast(e.get()))) { + return base::EventFilterResult::Continue; + } + Ui::SendSynteticMouseEvent(target, QEvent::MouseMove, Qt::NoButton); + return base::EventFilterResult::Cancel; + }); +} + +Manager::~Manager() = default; + +void Manager::updateButton(ButtonParameters parameters) { + if (parameters.cursorLeft) { + if (_menu) { + return; + } else if (_externalSelectorShown) { + setSelectedIcon(-1); + return; + } + } + const auto contextChanged = (_buttonContext != parameters.context); + if (contextChanged) { + setSelectedIcon(-1); + if (_button) { + _button->applyState(ButtonState::Hidden); + _buttonHiding.push_back(std::move(_button)); + } + _buttonShowTimer.cancel(); + _scheduledParameters = std::nullopt; + } + _buttonContext = parameters.context; + parameters.reactionsCount = _icons.size(); + if (!_buttonContext || !parameters.reactionsCount) { + return; + } else if (_button) { + _button->applyParameters(parameters); + if (_button->geometry().height() == _outer.height()) { + clearAppearAnimations(); + } + return; + } else if (parameters.outside) { + _buttonShowTimer.cancel(); + _scheduledParameters = std::nullopt; + return; + } + const auto globalPositionChanged = _scheduledParameters + && (_scheduledParameters->globalPointer != parameters.globalPointer); + const auto positionChanged = _scheduledParameters + && (_scheduledParameters->pointer != parameters.pointer); + _scheduledParameters = parameters; + if ((_buttonShowTimer.isActive() && positionChanged) + || globalPositionChanged) { + _buttonShowTimer.callOnce(kButtonShowDelay); + } +} + +void Manager::toggleExpanded(bool expanded) { + if (!_button || !_buttonContext) { + } else if (!expanded || (_filter.customAllowed && !applyUniqueLimit())) { + _expandSelectorRequests.fire({ + .context = _buttonContext, + .button = _button->geometry().marginsRemoved( + st::reactionCornerShadow), + .expanded = expanded, + }); + } else { + _button->expandWithoutCustom(); + } +} + +void Manager::setExternalSelectorShown(rpl::producer shown) { + std::move(shown) | rpl::start_with_next([=](bool shown) { + _externalSelectorShown = shown; + }, _lifetime); +} + +void Manager::showButtonDelayed() { + clearAppearAnimations(); + _button = std::make_unique