From 8a6b3027f555223a3571c9b85f1f561157909bfd Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Aug 2022 00:56:38 +0400 Subject: [PATCH] Support multiple reactions from one user. --- .../icons/chat/reactions_expand_bg.png | Bin 418 -> 0 bytes .../icons/chat/reactions_expand_bg@2x.png | Bin 803 -> 0 bytes .../icons/chat/reactions_expand_bg@3x.png | Bin 1157 -> 0 bytes .../icons/chat/reactions_round_big.png | Bin 0 -> 398 bytes .../icons/chat/reactions_round_big@2x.png | Bin 0 -> 691 bytes .../icons/chat/reactions_round_big@3x.png | Bin 0 -> 1072 bytes .../icons/chat/reactions_round_small.png | Bin 0 -> 359 bytes .../icons/chat/reactions_round_small@2x.png | Bin 0 -> 670 bytes .../icons/chat/reactions_round_small@3x.png | Bin 0 -> 947 bytes Telegram/SourceFiles/api/api_who_reacted.cpp | 23 +- .../data/data_message_reaction_id.h | 6 + .../data/data_message_reactions.cpp | 309 ++++++++++++------ .../SourceFiles/data/data_message_reactions.h | 21 +- .../history/history_inner_widget.cpp | 8 +- Telegram/SourceFiles/history/history_item.cpp | 30 +- Telegram/SourceFiles/history/history_item.h | 15 +- .../history/view/history_view_bottom_info.cpp | 20 +- .../history/view/history_view_bottom_info.h | 5 +- .../history/view/history_view_list_widget.cpp | 10 +- .../history/view/history_view_message.cpp | 7 +- .../view/reactions/history_view_reactions.cpp | 40 +-- .../view/reactions/history_view_reactions.h | 4 +- .../reactions/history_view_reactions_list.cpp | 12 +- .../history_view_reactions_strip.cpp | 4 +- .../reactions/history_view_reactions_tabs.cpp | 10 +- .../reactions/history_view_reactions_tabs.h | 3 +- Telegram/SourceFiles/ui/chat/chat.style | 8 +- .../SourceFiles/window/section_widget.cpp | 3 +- Telegram/lib_tl | 2 +- 29 files changed, 354 insertions(+), 186 deletions(-) delete mode 100644 Telegram/Resources/icons/chat/reactions_expand_bg.png delete mode 100644 Telegram/Resources/icons/chat/reactions_expand_bg@2x.png delete mode 100644 Telegram/Resources/icons/chat/reactions_expand_bg@3x.png create mode 100644 Telegram/Resources/icons/chat/reactions_round_big.png create mode 100644 Telegram/Resources/icons/chat/reactions_round_big@2x.png create mode 100644 Telegram/Resources/icons/chat/reactions_round_big@3x.png create mode 100644 Telegram/Resources/icons/chat/reactions_round_small.png create mode 100644 Telegram/Resources/icons/chat/reactions_round_small@2x.png create mode 100644 Telegram/Resources/icons/chat/reactions_round_small@3x.png diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg.png b/Telegram/Resources/icons/chat/reactions_expand_bg.png deleted file mode 100644 index 9c96eedd59bfa7201083331e1f8b39859f5f3c34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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- diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@2x.png deleted file mode 100644 index 827ac5f141dc582f0377a8ad30ef293f67d0f090..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png b/Telegram/Resources/icons/chat/reactions_expand_bg@3x.png deleted file mode 100644 index 3aaa0e2728d2d860836f10f5a3532b1bf0c6bf53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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<nK%SwX-xH~d{s^j^m4FDq@r_O@kPHYTp_WY3UKY7&^6v2=2V%fEV~6F$>ayN>HE z*ExM%$ZO@QRTm_Zw_JO#daUqHDXacxoBUa6vzDEAG-zdrOz37SjV+HYci@Oqc9yw$ zT~YY30ZUO~Lq+@1Apdo#+{cBUd)$gX`ZK-9&hg*G4ZDp*l*=Og_3BCjMdWIpaw?bc z?|Rg`Rqn{jZuRv&du!*VS1Lb=d{cA%UErh*3a?)9oxZ8tk+p~U)o;1otbv!x9dd84 z`=-KPl)6+?FyUmESlBGHM@B8Bn`V0Sp51t2;$NXz2Lo6Cjs9Hz`q#sMtU42R+~o}H Q*$D~@Pgg&ebxsLQ0EdE&)Bpeg literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_round_big@2x.png b/Telegram/Resources/icons/chat/reactions_round_big@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e74f7fc488f6832dd9d91bfe8da239e599b37d8b GIT binary patch literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H;gF~RuaI~{mv?}kEbkPLo6CEz8ztEEB{y%4 z64$t4{CWTJ~$5pYwL+LM`KAd#~@dSXi`gX(_`JU&i`N+!Z@;~?q2xBpm3Jym zXV%WlyVCQH|7@Po%ooqa?R$MiONWc@tLp{yk&Xa4!;e#@25C_Vk~wCH8oZUqf~!4H}bQ*O^{ zP&(qNpuKd;1)zpYE2k7}%Jo$UcX~4GlTMIV2Gc3$lzkv&-&sCY?$qMn`1;!Gtu_-) zgdFbYS2s*MohoH<^HuPR2SrSux$_$Amj0O*J#ioB&CI7$*$x`+b`kWeTzcuN)3eZ= z6_!s!`!3dX#j$bfM_irLvApBxgl(#OX9@q-I~Ca=I5+CbwCKM@HLTN>0z~}wCuFU8 zkhJlG&n`jd8#*dK9^6a2VkhOA_;*V~*5BC1``2ruJ}Cd$zGNRG>*-sKu8b{DK?%mw L)z4*}Q$iB}D{35M literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_round_big@3x.png b/Telegram/Resources/icons/chat/reactions_round_big@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0007a00bc0fd333b5d37a65ee72a46cbc4d9830 GIT binary patch literal 1072 zcmV-01kd}4P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS;nn^@KRA>e5n>}vgKp2LzY!Vta z1$F2^2o69+0~EA0oPrB*0+fQ1B2v->iHaLgP^G0tkdm~a*(yY0_t{wT47S(UcpQ5s zxIp%Jex7%}@%(%gP()DA)7)oQi3x3?%vs?vf1Oc3Vdaj2w}sIqDY7zK8tSts5C-PQ&v()UJ3 zlwfb+)+2`ic6vXVNY5_Q``?3i{6libU|96eHw|w?kKIgUzHDw0rGkL`-<<2G+g5}O z27`9H?fEKIfj2idd}*`UED#mb>R4yDQB34c3##Wjz+++8@Aoe+F9X#<3L=3D@Np2v z2j%N4cQvaG0oXmPIW5-f^{%e2j#ogMVgL-b9}zBx6wWh(m=|n^zvRGTjYb0plb`go zz5^S0neISXx7(H9>2>dbKM>YzHuVxC(>maV!xU@MQst=H-P1QR7qPH~#qix~r^Ep) zOAx~FNIT+b_b5vc!tk?er^Ep)OAx}?J#fU+?opN?gs~^0oe~GIEI|lkqsI|XyGL1q z=y%Gj`d+N=9y@+F1{Hdoc#QXo@_{=X34!*uW0>17XwY)Q+d! zjUT+R?zjDZKbcIlvlYN#1Mf#}tcx2*)p$Jilb*hJPft&Q{A*@xBYD?9r?Y~1%+nAD zRBsKQp$2T#dg}TYXy)d+CD+X35NI>@01Mg}*62yo&`HP)Ai4KutyUJ|*UUp=Rt1=O ztk2Sh-4bt;04O-n=WeVt>jV$O#dUPn(n&v=$QVGOw(r_qxr()WlcI0ZqUy`c7SqX=! qOd0VW->W%)jEh(o=j7kM2L1v_I4ZF8G)LF~0000nKXMNq)i^--Np%qiP9vPW1|uIU-D&-f{+r0CThVkFtkxAfoRaOaNy8D5&%Yroqq z{qoDK#K1)$c)47Uq2+V)iw3PN?9--PvUC?{Gq^Ot=MkT#Xy+4C4rw_tTL~STMJ=;; z1^9S8yY;v{;O1|Oma7TcGp<$T{?N4IF7)}P9lTk9FZVAHy>FBqfE42q7KVy=&%@J+U&pQ9%O4*%! ZD|xNyliw~WSOfALgQu&X%Q~loCIHovc4hzo literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/reactions_round_small@2x.png b/Telegram/Resources/icons/chat/reactions_round_small@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..548d984c8cbb5246da52023ee4cdd012b019ce85 GIT binary patch literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@HmAM{H=`(* zK$rh={q4$)Ld8u2T@7v*yqX^t%vhG``Gzf&MI}dd$+E_ETh6Jy=SY10qGbfe!!PJNAp&JU+71TYs`VW#A^1w-W;`7ZSu+L+TErO3@Lpl4@D(^ z)h@eh$A96s)jF*$F?!xpP0EknWm<4FDe=SRJImjsUWjH|u6#sp-S_v4L@y|*znosU zNMqa2liCY}6wjI0d58px1au0tuU)iq{vIYN8O4_d2M#^RJ;(j?{rBG)CPJ>BifR)x zK1>UAP<49#`KB~K=f zAKDi??QFq8*$eBgzs}qKI%rCGPD4ZW)YV0Al}A~Ruj5;^=JD;f*NXNZ vUUO{m@{(oOG?tjOe$rWU;Z^ruCXEm5o8qf1w%o0F0!ka6u6{1-oD!MPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS;9Z5t%RA>e5o4u;)KoG~zxrl;r z3k$#S0qz6XiXWw|rB5OFLROYGHr6VFg%5BaKoA>+3xZ#q`$OTJ13U2~*-cE=Y$0K? zJM;g~B)gm0@$D^Xi3bu7BpygS@a2K`uS;`R*=)8}t5vJjKetvYm44qE7XIDZX0!Qw zYpd02KA#^B2Xn>)&{QZCdcEFsIz1kbAFdmn8Nxt@2LMV>WxZY>kH_cp*)<)ZBg0XG zDm^TQMz>n6*=#1j)DWSt*=!mD=v_^@TpkXG8u))0igHYN$E7!--EQyq`yZfb-^K)n z^pJWP-S79W*Q+*cR|+tMIWOpZ_0425gTcTRBBPx!ifLbvT@1?S^NYp8C}0oDFbx0~ zFgp$(7@ z@SVkc8U+ExtWH2WF_2ROe9W%b>r^V`G%OY zK_=Gebj02f6goi_9z}9YyWK9GPDhZ23h8t@<#IVS&m}~R%voGm!4s$eRtXEl$Oo$Z zqAU>s3&corMwy0+O$7W(#K=giUWu|q1S~|1G<%e3sMtin!gNnEJw~%4Jel#;T!;`c z)g^J12*fz(i5C+Q@GBAHR4!f-69Eem zZlXzXN)T=~rPKeR1l;UAMuM2N2Y+z0^QabLG&>L1@7(Mxhk%>41uSs0v*hUhKoYm7 zJd+qz6Kr=#CV!brLMv$rsjTnQJ*)vC6t;w2h zTKhmBG@OP|Z(*uD-B0O;6D(8mVzC&VKNQHR0Zpp5{RKqR9m(*-1BnL`4@AHN{{e`W VogjJBN{9de002ovPDHLkV1gq6jVAyA literal 0 HcmV?d00001 diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index b52c82b23..07ce80485 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -479,21 +479,26 @@ rpl::producer WhoReacted( : Ui::WhoReadType::Reacted; if (resolveWhoReacted) { const auto &list = item->reactions(); - state->current.fullReactionsCount = reaction.empty() - ? ranges::accumulate( + state->current.fullReactionsCount = [&] { + if (reaction.empty()) { + return ranges::accumulate( + list, + 0, + ranges::plus{}, + &Data::MessageReaction::count); + } + const auto i = ranges::find( list, - 0, - ranges::plus{}, - [](const auto &pair) { return pair.second; }) - : list.contains(reaction) - ? list.find(reaction)->second - : 0; + reaction, + &Data::MessageReaction::id); + return (i != end(list)) ? i->count : 0; + }(); // #TODO reactions state->current.singleReaction = (!reaction.empty() ? reaction : (list.size() == 1) - ? list.front().first + ? list.front().id : ReactionId()).emoji(); } std::move( diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.h b/Telegram/SourceFiles/data/data_message_reaction_id.h index ed25fc721..ca64a6191 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.h +++ b/Telegram/SourceFiles/data/data_message_reaction_id.h @@ -26,6 +26,12 @@ struct ReactionId { } }; +struct MessageReaction { + ReactionId id; + int count = 0; + bool my = false; +}; + inline bool operator<(const ReactionId &a, const ReactionId &b) { return a.data < b.data; } diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 9b8edc701..a3398422a 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -63,6 +63,18 @@ constexpr auto kTopReactionsLimit = 10; return result; } +[[nodiscard]] Reaction CustomReaction(not_null document) { + return Reaction{ + .id = { { document->id } }, + .title = "Custom reaction", + .appearAnimation = document, + .selectAnimation = document, + .centerIcon = document, + .active = true, + }; + +} + } // namespace PossibleItemReactions LookupPossibleReactions(not_null item) { @@ -74,22 +86,43 @@ PossibleItemReactions LookupPossibleReactions(not_null item) { const auto session = &peer->session(); const auto reactions = &session->data().reactions(); const auto &full = reactions->list(Reactions::Type::Active); + const auto &top = reactions->list(Reactions::Type::Top); + const auto &recent = reactions->list(Reactions::Type::Recent); 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) { + const auto limited = (all.size() >= limit) && [&] { + const auto my = item->chosenReactions(); + if (my.empty()) { + return true; + } + return true; // #TODO reactions + }(); + auto added = base::flat_set(); + const auto addOne = [&](const Reaction &reaction) { + if (added.emplace(reaction.id).second) { + result.recent.push_back(&reaction); + } + }; + const auto add = [&](auto predicate) { + auto &&all = ranges::views::concat(top, recent, full); + for (const auto &reaction : all) { + if (predicate(reaction)) { + addOne(reaction); + } + } + }; + reactions->clearTemporary(); + if (limited) { result.recent.reserve(all.size()); - for (const auto &reaction : full) { + add([&](const Reaction &reaction) { + return ranges::contains(all, reaction.id, &MessageReaction::id); + }); + for (const auto &reaction : all) { const auto id = reaction.id; - if (all.contains(id)) { - result.recent.push_back(&reaction); + if (!added.contains(id)) { + if (const auto temp = reactions->lookupTemporary(id)) { + result.recent.push_back(temp); + } } } } else { @@ -97,22 +130,21 @@ PossibleItemReactions LookupPossibleReactions(not_null item) { result.recent.reserve((allowed.type == AllowedReactionsType::Some) ? allowed.some.size() : full.size()); - for (const auto &reaction : full) { + add([&](const Reaction &reaction) { const auto id = reaction.id; if ((allowed.type == AllowedReactionsType::Some) && !ranges::contains(allowed.some, id)) { - continue; + return false; } else if (reaction.premium && !session->premium() - && !all.contains(id)) { + && !ranges::contains(all, id, &MessageReaction::id)) { if (session->premiumPossible()) { result.morePremiumAvailable = true; } - continue; - } else { - result.recent.push_back(&reaction); + return false; } - } + return true; + }); result.customAllowed = (allowed.type == AllowedReactionsType::All); } const auto i = ranges::find( @@ -564,14 +596,7 @@ std::optional Reactions::resolveById(const ReactionId &id) { } else if (const auto customId = id.custom()) { const auto document = _owner->document(customId); if (document->sticker()) { - return Reaction{ - .id = id, - .title = "Custom reaction", - .appearAnimation = document, - .selectAnimation = document, - .centerIcon = document, - .active = true, - }; + return CustomReaction(document); } } return {}; @@ -637,7 +662,7 @@ std::optional Reactions::parse(const MTPAvailableReaction &entry) { }); } -void Reactions::send(not_null item, const ReactionId &chosen) { +void Reactions::send(not_null item, bool addToRecent) { const auto id = item->fullId(); auto &api = _owner->session().api(); auto i = _sentRequests.find(id); @@ -646,14 +671,17 @@ void Reactions::send(not_null item, const ReactionId &chosen) { } else { i = _sentRequests.emplace(id).first; } - const auto flags = chosen.empty() - ? MTPmessages_SendReaction::Flag(0) - : MTPmessages_SendReaction::Flag::f_reaction; + const auto chosen = item->chosenReactions(); + using Flag = MTPmessages_SendReaction::Flag; + const auto flags = (chosen.empty() ? Flag(0) : Flag::f_reaction) + | (addToRecent ? Flag::f_add_to_recent : Flag(0)); i->second = api.request(MTPmessages_SendReaction( MTP_flags(flags), item->history()->peer->input, MTP_int(id.msg), - MTP_vector(1, ReactionToMTP(chosen)) + MTP_vector(chosen | ranges::views::transform( + ReactionToMTP + ) | ranges::to>()) )).done([=](const MTPUpdates &result) { _sentRequests.remove(id); _owner->session().api().applyUpdates(result); @@ -693,6 +721,32 @@ void Reactions::updateAllInHistory(not_null peer, bool enabled) { } } +void Reactions::clearTemporary() { + _temporary.clear(); +} + +Reaction *Reactions::lookupTemporary(const ReactionId &id) { + if (const auto emoji = id.emoji(); !emoji.isEmpty()) { + const auto i = ranges::find(_available, id, &Reaction::id); + return (i != end(_available)) ? &*i : nullptr; + } else if (const auto customId = id.custom()) { + if (const auto i = _temporary.find(customId); i != end(_temporary)) { + return &i->second; + } + const auto document = _owner->document(customId); + if (document->sticker()) { + return &_temporary.emplace( + customId, + CustomReaction(document)).first->second; + } + _owner->customEmojiManager().resolve( + customId, + resolveListener()); + return nullptr; + } + return nullptr; +} + void Reactions::repaintCollected() { const auto now = crl::now(); auto closest = crl::time(); @@ -784,45 +838,84 @@ MessageReactions::MessageReactions(not_null item) : _item(item) { } -void MessageReactions::add(const ReactionId &reaction) { - if (_chosen == reaction) { - return; - } +void MessageReactions::add(const ReactionId &id, bool addToRecent) { + Expects(!id.empty()); + const auto history = _item->history(); const auto self = history->session().user(); - if (!_chosen.empty()) { - const auto i = _list.find(_chosen); - Assert(i != end(_list)); - --i->second; - const auto removed = !i->second; - if (removed) { - _list.erase(i); + const auto myLimit = self->isPremium() ? 5 : 1; // #TODO reactions + if (ranges::contains(chosen(), id)) { + return; + } + auto my = 0; + _list.erase(ranges::remove_if(_list, [&](MessageReaction &one) { + const auto removing = one.my && (my == myLimit || ++my == myLimit); + if (!removing) { + return false; } - const auto j = _recent.find(_chosen); + one.my = false; + const auto removed = !--one.count; + const auto j = _recent.find(one.id); if (j != end(_recent)) { j->second.erase( ranges::remove(j->second, self, &RecentReaction::peer), end(j->second)); - if (j->second.empty() || removed) { + if (j->second.empty()) { _recent.erase(j); + } else { + Assert(!removed); } } + return removed; + }), end(_list)); + if (_item->canViewReactions()) { + auto &list = _recent[id]; + list.insert(begin(list), RecentReaction{ self }); } - _chosen = reaction; - if (!reaction.empty()) { - if (_item->canViewReactions()) { - auto &list = _recent[reaction]; - list.insert(begin(list), RecentReaction{ self }); - } - ++_list[reaction]; + const auto i = ranges::find(_list, id, &MessageReaction::id); + if (i != end(_list)) { + i->my = true; + ++i->count; + std::rotate(i, i + 1, end(_list)); + } else { + _list.push_back({ .id = id, .count = 1, .my = true }); } auto &owner = history->owner(); - owner.reactions().send(_item, _chosen); + owner.reactions().send(_item, addToRecent); owner.notifyItemDataChange(_item); } -void MessageReactions::remove() { - add(ReactionId()); +void MessageReactions::remove(const ReactionId &id) { + const auto history = _item->history(); + const auto self = history->session().user(); + const auto i = ranges::find(_list, id, &MessageReaction::id); + const auto j = _recent.find(id); + if (i == end(_list)) { + Assert(j == end(_recent)); + return; + } else if (!i->my) { + Assert(j == end(_recent) + || !ranges::contains(j->second, self, &RecentReaction::peer)); + return; + } + i->my = false; + const auto removed = !--i->count; + if (removed) { + _list.erase(i); + } + if (j != end(_recent)) { + j->second.erase( + ranges::remove(j->second, self, &RecentReaction::peer), + end(j->second)); + if (j->second.empty()) { + _recent.erase(j); + } else { + Assert(!removed); + } + } + auto &owner = history->owner(); + owner.reactions().send(_item, false); + owner.notifyItemDataChange(_item); } bool MessageReactions::checkIfChanged( @@ -836,31 +929,31 @@ bool MessageReactions::checkIfChanged( auto existing = base::flat_set(); for (const auto &count : list) { const auto changed = count.match([&](const MTPDreactionCount &data) { - const auto reaction = ReactionFromMTP(data.vreaction()); + const auto id = ReactionFromMTP(data.vreaction()); const auto nowCount = data.vcount().v; - const auto i = _list.find(reaction); - const auto wasCount = (i != end(_list)) ? i->second : 0; + const auto i = ranges::find(_list, id, &MessageReaction::id); + const auto wasCount = (i != end(_list)) ? i->count : 0; if (wasCount != nowCount) { return true; } - existing.emplace(reaction); + existing.emplace(id); return false; }); if (changed) { return true; } } - for (const auto &[reaction, count] : _list) { - if (!existing.contains(reaction)) { + for (const auto &reaction : _list) { + if (!existing.contains(reaction.id)) { return true; } } auto parsed = base::flat_map>(); for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { - const auto emoji = ReactionFromMTP(data.vreaction()); - if (_list.contains(emoji)) { - parsed[emoji].push_back(RecentReaction{ + const auto id = ReactionFromMTP(data.vreaction()); + if (ranges::contains(_list, id, &MessageReaction::id)) { + parsed[id].push_back(RecentReaction{ .peer = owner.peer(peerFromMTP(data.vpeer_id())), .unread = data.is_unread(), .big = data.is_big(), @@ -890,50 +983,79 @@ bool MessageReactions::change( } auto changed = false; auto existing = base::flat_set(); + auto order = base::flat_map(); for (const auto &count : list) { count.match([&](const MTPDreactionCount &data) { - const auto reaction = ReactionFromMTP(data.vreaction()); - if (!ignoreChosen) { - if (data.vchosen_order() && _chosen != reaction) { - _chosen = reaction; - changed = true; - } else if (!data.vchosen_order() && _chosen == reaction) { - _chosen = ReactionId(); + const auto id = ReactionFromMTP(data.vreaction()); + const auto &chosen = data.vchosen_order(); + if (!ignoreChosen && chosen) { + order[id] = chosen->v; + } + const auto i = ranges::find(_list, id, &MessageReaction::id); + const auto nowCount = data.vcount().v; + if (i == end(_list)) { + changed = true; + _list.push_back({ + .id = id, + .count = nowCount, + .my = (!ignoreChosen && chosen) + }); + } else { + const auto nowMy = ignoreChosen ? i->my : chosen.has_value(); + if (i->count != nowCount || i->my != nowMy) { + i->count = nowCount; + i->my = nowMy; changed = true; } } - const auto nowCount = data.vcount().v; - auto &wasCount = _list[reaction]; - if (wasCount != nowCount) { - wasCount = nowCount; - changed = true; - } - existing.emplace(reaction); + existing.emplace(id); }); } + if (!ignoreChosen && !order.empty()) { + const auto min = std::numeric_limits::min(); + const auto proj = [&](const MessageReaction &reaction) { + return reaction.my ? order[reaction.id] : min; + }; + const auto correctOrder = [&] { + auto previousOrder = min; + for (const auto &reaction : _list) { + const auto nowOrder = proj(reaction); + if (nowOrder < previousOrder) { + return false; + } + previousOrder = nowOrder; + } + return true; + }(); + if (!correctOrder) { + changed = true; + ranges::sort(_list, std::less(), proj); + } + } if (_list.size() != existing.size()) { changed = true; for (auto i = begin(_list); i != end(_list);) { - if (!existing.contains(i->first)) { + if (!existing.contains(i->id)) { i = _list.erase(i); } else { ++i; } } - if (!_chosen.empty() && !_list.contains(_chosen)) { - _chosen = ReactionId(); - } } auto parsed = base::flat_map>(); for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { - const auto emoji = ReactionFromMTP(data.vreaction()); - if (_list.contains(emoji)) { - parsed[emoji].push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); + const auto id = ReactionFromMTP(data.vreaction()); + const auto i = ranges::find(_list, id, &MessageReaction::id); + if (i != end(_list)) { + auto &list = parsed[id]; + if (list.size() < i->count) { + list.push_back(RecentReaction{ + .peer = owner.peer(peerFromMTP(data.vpeer_id())), + .unread = data.is_unread(), + .big = data.is_big(), + }); + } } }); } @@ -944,7 +1066,7 @@ bool MessageReactions::change( return changed; } -const base::flat_map &MessageReactions::list() const { +const std::vector &MessageReactions::list() const { return _list; } @@ -974,8 +1096,11 @@ void MessageReactions::markRead() { } } -ReactionId MessageReactions::chosen() const { - return _chosen; +std::vector MessageReactions::chosen() const { + return _list + | ranges::views::filter(&MessageReaction::my) + | ranges::views::transform(&MessageReaction::id) + | ranges::to_vector; } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index d5d00493a..84591c796 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -83,13 +83,16 @@ public: const ReactionId &emoji, ImageSize size); - void send(not_null item, const ReactionId &chosen); + void send(not_null item, bool addToRecent); [[nodiscard]] bool sending(not_null item) const; void poll(not_null item, crl::time now); void updateAllInHistory(not_null peer, bool enabled); + void clearTemporary(); + [[nodiscard]] Reaction *lookupTemporary(const ReactionId &id); + [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); static void CheckUnknownForUnread( not_null owner, @@ -160,6 +163,11 @@ private: rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _favoriteUpdated; + // We need &i->second stay valid while inserting new items. + // So we use std::map instead of base::flat_map here. + // Otherwise we could use flat_map>. + std::map _temporary; + base::Timer _topRefreshTimer; mtpRequestId _topRequestId = 0; bool _topRequestScheduled = false; @@ -208,8 +216,8 @@ class MessageReactions final { public: explicit MessageReactions(not_null item); - void add(const ReactionId &reaction); - void remove(); + void add(const ReactionId &id, bool addToRecent); + void remove(const ReactionId &id); bool change( const QVector &list, const QVector &recent, @@ -217,10 +225,10 @@ public: [[nodiscard]] bool checkIfChanged( const QVector &list, const QVector &recent) const; - [[nodiscard]] const base::flat_map &list() const; + [[nodiscard]] const std::vector &list() const; [[nodiscard]] auto recent() const -> const base::flat_map> &; - [[nodiscard]] ReactionId chosen() const; + [[nodiscard]] std::vector chosen() const; [[nodiscard]] bool empty() const; [[nodiscard]] bool hasUnread() const; @@ -229,8 +237,7 @@ public: private: const not_null _item; - ReactionId _chosen; - base::flat_map _list; + std::vector _list; base::flat_map> _recent; }; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index f744617f9..745c4072e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -471,8 +471,8 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) { reaction.id)) { return; } - item->toggleReaction(reaction.id); - if (item->chosenReaction() != reaction.id) { + item->toggleReaction(reaction.id, HistoryItem::ReactionSource::Selector); + if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = item->mainView()) { if (const auto top = itemTop(view); top >= 0) { @@ -1948,12 +1948,12 @@ void HistoryInner::toggleFavoriteReaction(not_null view) const { &Data::Reaction::id) || Window::ShowReactPremiumError(_controller, item, favorite)) { return; - } else if (item->chosenReaction() != favorite) { + } else if (!ranges::contains(item->chosenReactions(), favorite)) { if (const auto top = itemTop(view); top >= 0) { view->animateReaction({ .id = favorite }); } } - item->toggleReaction(favorite); + item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } void HistoryInner::contextMenuEvent(QContextMenuEvent *e) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0576d1a5e..0c2df7c8f 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -883,15 +883,9 @@ bool HistoryItem::canReact() const { return true; } -void HistoryItem::addReaction(const Data::ReactionId &reaction) { - if (!_reactions) { - _reactions = std::make_unique(this); - } - _reactions->add(reaction); - history()->owner().notifyItemDataChange(this); -} - -void HistoryItem::toggleReaction(const Data::ReactionId &reaction) { +void HistoryItem::toggleReaction( + const Data::ReactionId &reaction, + ReactionSource source) { if (!_reactions) { _reactions = std::make_unique(this); const auto canViewReactions = !isDiscussionPost() @@ -899,16 +893,16 @@ void HistoryItem::toggleReaction(const Data::ReactionId &reaction) { if (canViewReactions) { _flags |= MessageFlag::CanViewReactions; } - _reactions->add(reaction); - } else if (_reactions->chosen() == reaction) { - _reactions->remove(); + _reactions->add(reaction, (source == ReactionSource::Selector)); + } else if (ranges::contains(_reactions->chosen(), reaction)) { + _reactions->remove(reaction); if (_reactions->empty()) { _reactions = nullptr; _flags &= ~MessageFlag::CanViewReactions; history()->owner().notifyItemDataChange(this); } } else { - _reactions->add(reaction); + _reactions->add(reaction, (source == ReactionSource::Selector)); } history()->owner().notifyItemDataChange(this); } @@ -977,8 +971,8 @@ void HistoryItem::updateReactionsUnknown() { _reactionsLastRefreshed = 1; } -const base::flat_map &HistoryItem::reactions() const { - static const auto kEmpty = base::flat_map(); +const std::vector &HistoryItem::reactions() const { + static const auto kEmpty = std::vector(); return _reactions ? _reactions->list() : kEmpty; } @@ -998,8 +992,10 @@ bool HistoryItem::canViewReactions() const { && !_reactions->list().empty(); } -Data::ReactionId HistoryItem::chosenReaction() const { - return _reactions ? _reactions->chosen() : Data::ReactionId(); +std::vector HistoryItem::chosenReactions() const { + return _reactions + ? _reactions->chosen() + : std::vector(); } Data::ReactionId HistoryItem::lookupUnreadReaction( diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index c7a8372f9..be1f33a6f 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -44,6 +44,7 @@ struct MessagePosition; struct RecentReaction; struct ReactionId; class Media; +struct MessageReaction; class MessageReactions; } // namespace Data @@ -373,18 +374,24 @@ public: [[nodiscard]] bool suggestDeleteAllReport() const; [[nodiscard]] bool canReact() const; - void addReaction(const Data::ReactionId &reaction); - void toggleReaction(const Data::ReactionId &reaction); + enum class ReactionSource { + Selector, + Quick, + Existing, + }; + void toggleReaction( + const Data::ReactionId &reaction, + ReactionSource source); void updateReactions(const MTPMessageReactions *reactions); void updateReactionsUnknown(); [[nodiscard]] auto reactions() const - -> const base::flat_map &; + -> const std::vector &; [[nodiscard]] auto recentReactions() const -> const base::flat_map< Data::ReactionId, std::vector> &; [[nodiscard]] bool canViewReactions() const; - [[nodiscard]] Data::ReactionId chosenReaction() const; + [[nodiscard]] std::vector chosenReactions() const; [[nodiscard]] Data::ReactionId lookupUnreadReaction( not_null from) const; [[nodiscard]] crl::time lastReactionsRefreshTime() const; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index e9518122a..8961fc87e 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -160,7 +160,7 @@ ClickHandlerPtr BottomInfo::revokeReactionLink( auto y = top; auto widthLeft = available; for (const auto &reaction : _reactions) { - const auto chosen = (reaction.id == _data.chosenReaction); + const auto chosen = reaction.chosen; const auto add = (reaction.countTextWidth > 0) ? st::reactionInfoDigitSkip : st::reactionInfoBetween; @@ -201,9 +201,11 @@ ClickHandlerPtr BottomInfo::revokeReactionLink( if (controller->session().uniqueId() == sessionId) { auto &owner = controller->session().data(); if (const auto item = owner.message(itemId)) { - const auto chosen = item->chosenReaction(); + const auto chosen = item->chosenReactions(); if (!chosen.empty()) { - item->toggleReaction(chosen); + item->toggleReaction( + chosen.front(), + HistoryItem::ReactionSource::Existing); } } } @@ -483,22 +485,23 @@ void BottomInfo::layoutReactionsText() { } auto sorted = ranges::view::all( _data.reactions - ) | ranges::view::transform([](const auto &pair) { - return std::make_pair(pair.first, pair.second); + ) | ranges::view::transform([](const MessageReaction &reaction) { + return not_null{ &reaction }; }) | ranges::to_vector; ranges::sort( sorted, std::greater<>(), - &std::pair::second); + &MessageReaction::count); auto reactions = std::vector(); reactions.reserve(sorted.size()); - for (const auto &[id, count] : sorted) { + for (const auto &reaction : sorted) { + const auto &id = reaction->id; const auto i = ranges::find(_reactions, id, &Reaction::id); reactions.push_back((i != end(_reactions)) ? std::move(*i) : prepareReactionWithId(id)); - setReactionCount(reactions.back(), count); + setReactionCount(reactions.back(), reaction->count); } _reactions = std::move(reactions); } @@ -593,7 +596,6 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { result.date = message->dateTime(); if (message->embedReactionsInBottomInfo()) { result.reactions = item->reactions(); - result.chosenReaction = item->chosenReaction(); } if (message->hasOutLayout()) { result.flags |= Flag::OutLayout; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index 3d7a8d7a8..615fbccbd 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -45,6 +45,7 @@ struct TextState; class BottomInfo final : public Object { public: using ReactionId = ::Data::ReactionId; + using MessageReaction = ::Data::MessageReaction; struct Data { enum class Flag : uchar { Edited = 0x01, @@ -62,8 +63,7 @@ public: QDateTime date; QString author; - base::flat_map reactions; - ReactionId chosenReaction; + std::vector reactions; std::optional views; std::optional replies; Flags flags; @@ -105,6 +105,7 @@ private: QString countText; int count = 0; int countTextWidth = 0; + bool chosen = false; }; void layout(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 660bd41ba..833f62b45 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -364,8 +364,10 @@ ListWidget::ListWidget( reaction.id)) { return; } - item->toggleReaction(reaction.id); - if (item->chosenReaction() != reaction.id) { + item->toggleReaction( + reaction.id, + HistoryItem::ReactionSource::Selector); + if (!ranges::contains(item->chosenReactions(), reaction.id)) { return; } else if (const auto view = viewForItem(item)) { if (const auto top = itemTop(view); top >= 0) { @@ -2129,12 +2131,12 @@ void ListWidget::toggleFavoriteReaction(not_null view) const { &Data::Reaction::id) || Window::ShowReactPremiumError(_controller, item, favorite)) { return; - } else if (item->chosenReaction() != favorite) { + } else if (!ranges::contains(item->chosenReactions(), favorite)) { if (const auto top = itemTop(view); top >= 0) { view->animateReaction({ .id = favorite }); } } - item->toggleReaction(favorite); + item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } void ListWidget::trySwitchToWordSelection() { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 1f4b962a5..30982b8ff 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2188,9 +2188,12 @@ void Message::refreshReactions() { const auto weak = base::make_weak(this); return std::make_shared([=] { if (const auto strong = weak.get()) { - strong->data()->toggleReaction(id); + strong->data()->toggleReaction( + id, + HistoryItem::ReactionSource::Existing); if (const auto now = weak.get()) { - if (now->data()->chosenReaction() == id) { + const auto chosen = now->data()->chosenReactions(); + if (ranges::contains(chosen, id)) { now->animateReaction({ .id = id, }); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index acb901bf9..ee43def46 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -45,6 +45,7 @@ struct InlineList::Button { QString countText; int count = 0; int countTextWidth = 0; + bool chosen = false; }; InlineList::InlineList( @@ -87,34 +88,39 @@ void InlineList::layoutButtons() { } auto sorted = ranges::view::all( _data.reactions - ) | ranges::view::transform([](const auto &pair) { - return std::make_pair(pair.first, pair.second); + ) | ranges::view::transform([](const MessageReaction &reaction) { + return not_null{ &reaction }; }) | ranges::to_vector; const auto &list = _owner->list(::Data::Reactions::Type::All); - ranges::sort(sorted, [&](const auto &p1, const auto &p2) { - if (p1.second > p2.second) { + ranges::sort(sorted, [&]( + not_null a, + not_null b) { + const auto acount = a->count - (a->my ? 1 : 0); + const auto bcount = b->count - (b->my ? 1 : 0); + if (acount > bcount) { return true; - } else if (p1.second < p2.second) { + } else if (acount < bcount) { return false; } - return ranges::find(list, p1.first, &::Data::Reaction::id) - < ranges::find(list, p2.first, &::Data::Reaction::id); + return ranges::find(list, a->id, &::Data::Reaction::id) + < ranges::find(list, b->id, &::Data::Reaction::id); }); auto buttons = std::vector