From 1c2951598b13ccf85ed22a155de9f5097754a458 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 12 Sep 2023 21:00:39 +0400 Subject: [PATCH] Handle t.me/channel?boost links. --- Telegram/Resources/icons/limits/boost.png | Bin 0 -> 464 bytes Telegram/Resources/icons/limits/boost@2x.png | Bin 0 -> 796 bytes Telegram/Resources/icons/limits/boost@3x.png | Bin 0 -> 1146 bytes Telegram/Resources/langs/lang.strings | 31 ++ .../SourceFiles/boxes/premium_limits_box.cpp | 10 +- .../SourceFiles/boxes/premium_limits_box.h | 3 +- .../SourceFiles/core/local_url_handlers.cpp | 44 +- .../SourceFiles/settings/settings_folders.cpp | 5 +- Telegram/SourceFiles/ui/boxes/boost_box.cpp | 251 +++++++++ Telegram/SourceFiles/ui/boxes/boost_box.h | 31 ++ Telegram/SourceFiles/ui/effects/premium.style | 72 ++- .../ui/effects/premium_graphics.cpp | 484 +++++++++++------- .../SourceFiles/ui/effects/premium_graphics.h | 30 ++ .../window/window_filters_menu.cpp | 5 +- .../window/window_session_controller.cpp | 143 ++++++ .../window/window_session_controller.h | 13 + Telegram/cmake/td_ui.cmake | 2 + 17 files changed, 920 insertions(+), 204 deletions(-) create mode 100644 Telegram/Resources/icons/limits/boost.png create mode 100644 Telegram/Resources/icons/limits/boost@2x.png create mode 100644 Telegram/Resources/icons/limits/boost@3x.png create mode 100644 Telegram/SourceFiles/ui/boxes/boost_box.cpp create mode 100644 Telegram/SourceFiles/ui/boxes/boost_box.h diff --git a/Telegram/Resources/icons/limits/boost.png b/Telegram/Resources/icons/limits/boost.png new file mode 100644 index 0000000000000000000000000000000000000000..b2c8bc7a06f0f935c798c4746480c6efbabbff68 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%*xZnF~maf z>14yM<^X}V$E!B_D2tgjwfgR9TF}}genLsF`}clb;2p;j;*aXIHoxk zv`-bC_*`Ylo!Ztjv)|9ZUs&_L@ zrzg;{$Ya{SluRXImW3Kz6?vJVp*H8eCtdk&Te8db$&AN4jz8Y`HdS1@&2inGR8!yA zyZGX)c@Cdr+U6+m=F9r+1x&ZM-K|QEbi4c{Goei9wB7vkS68gdws;hAP;htL`qkVu z%NA+4pA!Fh`|UP~gZ7(rxWkjfS*)VEY|l5pZj(+l=}Ug0(WT_`g|GSLwA4zw`S%5r jrS5+(Z-T@xzx;#B`!}7|T6{4=N9<^lVX4VXAHCuet|3}iGfWm$Iq&^dV^{QzM}1*H#QN(m zO*U+c(CINLx*ei*bb+N@zffn34C7r*sa~Uu1nb#nUsyi57Pnq}Rv~A|+OSIJ5C5k` zuDw=c_y0#n0MIc@8n;B{E)5FQn(C2!X@2F(kXvud^xt2xeDtwm!h(pV4|h4sU%ly# zk>Fv|x#w~5`}cAc7F!N;k;rP_fCAr3mg)i=Pl|Sao|b#@_1CPeQVKzw^XJcRb$S?o z?`4Ma?z{W$=DnP|qb^o!>V-vn7FX@9+ZYk!f8fNI-+%Y4z1TX_r|n_kUo)}pg|2}Y zv)v;q7-sn_`&z}@tXb;lqBeP<$^N6Ko)%fi{1ZF0MX=N5@WTna{hJRuEDT6sayVf& z+m}xvkoy&G1ZS&9hn{91WUOE_@KIC$` ztN*vd^2--96aIet)^>Y}_x-nJmm4?T)Co6h-0Y!J#MV?O))vLIEJo}@JQJ(Q{PPdL z6)v4*`MSi4;mgO5iJNbpu=w|Y$)0upzI_Rc{wnt0*chW{KKtyEH}BlO^WCv7IL!Bl Yad9hy>{Geb`k<8Q>FVdQ&MBb@01$m;6aWAK literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/limits/boost@3x.png b/Telegram/Resources/icons/limits/boost@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..194195fdb67fa94c752d909e65dfc4851aac8837 GIT binary patch literal 1146 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbAj}{>0P8F(iZa zZKSV;OQ49^Kro(I+ZkO&6||e(|%#IX7#paigj)Lx_M&j z{3U;KO*)e%JV|dUeD&1q-R{SCzdyV8J-A$KPW8Q=&+G2*EI+rWI4^3}j}8uG_@~z) zFfg#R^lSXwukLHtuKoV~`^So!tgKVNa_S^Q!ouR>BEm78Hr&>j~-1=PnS^5 z+_8K2>E;=7k%q>`#v)u-uU!-3Eb!{A5{Qn7c=76$(k7;-M~}L0a$k1M-_K9TUm>W< zMlM)bq3QM8w|{^Aa+2wA^7iH~Tvs{O*Vp&_`SUJDou`_bm_8RxP}bJg?sie?Wa;>H z_H1v>x=PcXJ9kbv(&=OH^x8Esb*VR-7oAPxW?{%_h?v-R=fLZ)m)!i8&h=etd(g(#)>ekEyu4g^p61GZ`}gzn@d3lx@bLES z=KJr<^ROB2U3d8B&&r=+OV%-Na}!(e`0?YF#R^pwRaH&G>t2g3Jo?~AZC#yE;?fl@ z%2oyj4Puu9$x?$iu9*swyhnsI%MgvhXd@lM|LMijRx)i}9E}Cn)UA%a@YU z(#&n2wk{M52nNZu%+!V^|o6L+Lx(&d*Su4kTq{)!%c793@m_4U)=gC@el!cj5O*3Z9u5n0^GD=seH>$)QKD=;1YzyE9d_U#Y3 z+`6-qpPQJOCH box, - not_null session) { + not_null session, + std::optional filtersCountOverride) { const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); const auto limits = Data::PremiumLimits(session); const auto defaultLimit = float64(limits.dialogFiltersDefault()); const auto premiumLimit = float64(limits.dialogFiltersPremium()); - const auto current = float64(ranges::count_if( + const auto cloud = int(ranges::count_if( session->data().chatsFilters().list(), [](const Data::ChatFilter &f) { return f.id() != FilterId(); })); + const auto current = float64(filtersCountOverride.value_or(cloud)); auto text = rpl::combine( tr::lng_filters_limit1( @@ -1079,6 +1082,7 @@ void AccountsLimitBox( Settings::AddSkip(top, st::premiumInfographicPadding.top()); Ui::Premium::AddBubbleRow( top, + st::defaultPremiumBubble, BoxShowFinishes(box), 0, current, diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h index 7bcda6e53..5a4fa8a0a 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.h +++ b/Telegram/SourceFiles/boxes/premium_limits_box.h @@ -42,7 +42,8 @@ void FilterLinksLimitBox( not_null session); void FiltersLimitBox( not_null box, - not_null session); + not_null session, + std::optional filtersCountOverride); void ShareableFiltersLimitBox( not_null box, not_null session); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index fbf4d1a39..d9db3c540 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -378,6 +378,8 @@ bool ResolveUsernameOrPhone( startToken = params.value(u"startgroup"_q); } else if (params.contains(u"startchannel"_q)) { resolveType = ResolveType::AddToChannel; + } else if (params.contains(u"boost"_q)) { + resolveType = ResolveType::Boost; } auto post = ShowAtUnreadMsgId; auto adminRights = ChatAdminRights(); @@ -842,6 +844,32 @@ bool ResolveLoginCode( return true; } +bool ResolveBoost( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto params = url_parse_params( + match->captured(1), + qthelp::UrlParamNameTransform::ToLower); + const auto domainParam = params.value(u"domain"_q); + const auto channelParam = params.value(u"channel"_q); + + const auto myContext = context.value(); + using Navigation = Window::SessionNavigation; + controller->window().activate(); + controller->showPeerByLink(Navigation::PeerByLinkInfo{ + .usernameOrId = (!domainParam.isEmpty() + ? std::variant(domainParam) + : ChannelId(BareId(channelParam.toULongLong()))), + .resolveType = Window::ResolveType::Boost, + .clickFromMessageId = myContext.itemId, + }); + return true; +} + } // namespace const std::vector &LocalUrlHandlers() { @@ -922,6 +950,10 @@ const std::vector &LocalUrlHandlers() { u"^login/?(\\?code=([0-9]+))(&|$)"_q, ResolveLoginCode }, + { + u"^boost/?\\?(.+)(#|$)"_q, + ResolveBoost, + }, { u"^([^\\?]+)(\\?|#|$)"_q, HandleUnknown @@ -1025,8 +1057,13 @@ QString TryConvertUrlToLocal(QString url) { "/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" ")"_q, query, matchOptions)) { + const auto channel = privateMatch->captured(1); const auto params = query.mid(privateMatch->captured(0).size()).toString(); - const auto base = u"tg://privatepost?channel="_q + privateMatch->captured(1); + if (params.indexOf("boost", 0, Qt::CaseInsensitive) >= 0 + && params.toLower().split('&').contains(u"boost"_q)) { + return u"tg://boost?channel="_q + channel; + } + const auto base = u"tg://privatepost?channel="_q + channel; auto added = QString(); if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, privateMatch->captured(2))) { added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); @@ -1044,7 +1081,12 @@ QString TryConvertUrlToLocal(QString url) { "/s/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" ")"_q, query, matchOptions)) { + const auto domain = usernameMatch->captured(1); const auto params = query.mid(usernameMatch->captured(0).size()).toString(); + if (params.indexOf("boost", 0, Qt::CaseInsensitive) >= 0 + && params.toLower().split('&').contains(u"boost"_q)) { + return u"tg://boost?domain="_q + domain; + } const auto base = u"tg://resolve?domain="_q + url_encode(usernameMatch->captured(1)); auto added = QString(); if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index c5be97152..c3ef96f03 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -362,10 +362,11 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const auto removed = ranges::count_if( state->rows, &FilterRow::removed); - if (state->rows.size() < limit() + removed) { + const auto count = int(state->rows.size() - removed); + if (count < limit()) { return false; } - controller->show(Box(FiltersLimitBox, session)); + controller->show(Box(FiltersLimitBox, session, count)); return true; }; const auto markForRemovalSure = [=](not_null button) { diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp new file mode 100644 index 000000000..dcfa948f2 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -0,0 +1,251 @@ +/* +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 "ui/boxes/boost_box.h" + +#include "lang/lang_keys.h" +#include "ui/effects/fireworks_animation.h" +#include "ui/effects/premium_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" + +namespace Ui { +namespace { + +void StartFireworks(not_null parent) { + const auto result = Ui::CreateChild(parent.get()); + result->setAttribute(Qt::WA_TransparentForMouseEvents); + result->setGeometry(parent->rect()); + result->show(); + + auto &lifetime = result->lifetime(); + const auto animation = lifetime.make_state([=] { + result->update(); + }); + result->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(result); + if (!animation->paint(p, result->rect())) { + crl::on_main(result, [=] { delete result; }); + } + }, lifetime); +} + +} // namespace + +void BoostBox( + not_null box, + BoostBoxData data, + Fn)> boost) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::boostBox); + + struct State { + rpl::variable you = false; + bool submitted = false; + }; + const auto state = box->lifetime().make_state(); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + const auto addSkip = [&](int skip) { + box->addRow(object_ptr(box, skip)); + }; + + addSkip(st::boostSkipTop); + + const auto levelWidth = [&](int add) { + return st::normalFont->width( + tr::lng_boost_level(tr::now, lt_count, data.boost.level + add)); + }; + const auto paddings = 2 * st::premiumLineTextSkip; + const auto labelLeftWidth = paddings + levelWidth(0); + const auto labelRightWidth = paddings + levelWidth(1); + const auto ratio = [=](int boosts) { + const auto min = std::min( + data.boost.boosts, + data.boost.thisLevelBoosts); + const auto max = std::max({ + data.boost.boosts, + data.boost.nextLevelBoosts, + 1, + }); + Assert(boosts >= min && boosts <= max); + const auto count = (max - min); + const auto index = (boosts - min); + if (!index) { + return 0.; + } else if (index == count) { + return 1.; + } else if (count == 2) { + return 0.5; + } + const auto available = st::boxWideWidth + - st::boxPadding.left() + - st::boxPadding.right(); + const auto average = available / float64(count); + const auto first = std::max(average, labelLeftWidth * 1.); + const auto last = std::max(average, labelRightWidth * 1.); + const auto other = (available - first - last) / (count - 2); + return (first + (index - 1) * other) / available; + }; + + const auto min = std::min( + data.boost.boosts, + data.boost.thisLevelBoosts); + const auto now = data.boost.boosts; + const auto max = (data.boost.nextLevelBoosts > min) + ? (data.boost.nextLevelBoosts) + : (data.boost.boosts > 0) + ? data.boost.boosts + : 1; + auto bubbleRowState = state->you.value( + ) | rpl::map([=](bool mine) { + const auto index = mine ? (now + 1) : now; + return Premium::BubbleRowState{ + .counter = index, + .ratio = ratio(index), + .dynamic = true, + }; + }); + Premium::AddBubbleRow( + box->verticalLayout(), + st::boostBubble, + BoxShowFinishes(box), + rpl::duplicate(bubbleRowState), + max, + true, + nullptr, + &st::premiumIconBoost); + addSkip(st::premiumLineTextSkip); + + const auto level = [](int level) { + return tr::lng_boost_level(tr::now, lt_count, level); + }; + auto ratioValue = std::move( + bubbleRowState + ) | rpl::map([](const Premium::BubbleRowState &state) { + return state.ratio; + }); + Premium::AddLimitRow( + box->verticalLayout(), + st::boostLimits, + Premium::LimitRowLabels{ + .leftLabel = level(data.boost.level), + .rightLabel = level(data.boost.level + 1), + .dynamic = true, + }, + std::move(ratioValue)); + + const auto name = data.name; + auto title = state->you.value() | rpl::map([=](bool your) { + return your + ? tr::lng_boost_channel_you_title( + lt_channel, + rpl::single(data.name)) + : !data.boost.level + ? tr::lng_boost_channel_title_first() + : tr::lng_boost_channel_title_more(); + }) | rpl::flatten_latest(); + auto text = state->you.value() | rpl::map([=](bool your) { + const auto bold = Ui::Text::Bold(data.name); + const auto now = data.boost.boosts + (your ? 1 : 0); + const auto left = (data.boost.nextLevelBoosts > now) + ? (data.boost.nextLevelBoosts - now) + : 0; + auto post = tr::lng_boost_channel_post_stories( + lt_count, + rpl::single(float64(data.boost.level + 1)), + Ui::Text::RichLangValue); + return your + ? ((left > 0) + ? (!data.boost.level + ? tr::lng_boost_channel_you_first( + lt_count, + rpl::single(float64(left)), + Ui::Text::RichLangValue) + : tr::lng_boost_channel_you_more( + lt_count, + rpl::single(float64(left)), + lt_post, + std::move(post), + Ui::Text::RichLangValue)) + : (!data.boost.level + ? tr::lng_boost_channel_reached_first( + Ui::Text::RichLangValue) + : tr::lng_boost_channel_reached_more( + lt_count, + rpl::single(float64(data.boost.level + 1)), + lt_post, + std::move(post), + Ui::Text::RichLangValue))) + : !data.boost.level + ? tr::lng_boost_channel_needs_first( + lt_count, + rpl::single(float64(left)), + lt_channel, + rpl::single(bold), + Ui::Text::RichLangValue) + : tr::lng_boost_channel_needs_more( + lt_count, + rpl::single(float64(left)), + lt_channel, + rpl::single(bold), + lt_post, + std::move(post), + Ui::Text::RichLangValue); + }) | rpl::flatten_latest(); + box->addRow( + object_ptr( + box, + std::move(title), + st::boostTitle), + st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0)); + box->addRow( + object_ptr( + box, + std::move(text), + st::boostText), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip))); + + auto submit = state->you.value( + ) | rpl::map([](bool mine) { + return mine ? tr::lng_box_ok() : tr::lng_boost_channel_button(); + }) | rpl::flatten_latest(); + const auto button = box->addButton(rpl::duplicate(submit), [=] { + if (state->submitted) { + return; + } else if (!state->you.current()) { + state->submitted = true; + boost(crl::guard(box, [=](bool success) { + state->submitted = false; + if (success) { + StartFireworks(box->parentWidget()); + state->you = true; + } + })); + } else { + box->closeBox(); + } + }); + rpl::combine( + std::move(submit), + box->widthValue() + ) | rpl::start_with_next([=](const QString &, int width) { + const auto &padding = st::boostBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), button->y()); + }, button->lifetime()); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h new file mode 100644 index 000000000..bd6d1cc84 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -0,0 +1,31 @@ +/* +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 +*/ +#pragma once + +namespace Ui { + +class GenericBox; + +struct BoostCounters { + int level = 0; + int boosts = 0; + int thisLevelBoosts = 0; + int nextLevelBoosts = 0; // Zero means no next level is available. +}; + +struct BoostBoxData { + QString name; + BoostCounters boost; +}; + +void BoostBox( + not_null box, + BoostBoxData data, + Fn)> boost); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 2deb51c8d..d0eb5cdc5 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -13,6 +13,17 @@ PremiumLimits { boxLabel: FlatLabel; nonPremiumBg: color; nonPremiumFg: color; + gradientFromLeft: bool; +} +PremiumBubble { + widthLimit: pixels; + height: pixels; + padding: margins; + skip: pixels; + penWidth: pixels; + textSkip: pixels; + tailSize: size; + font: font; } defaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) { @@ -26,6 +37,7 @@ defaultPremiumLimits: PremiumLimits { boxLabel: defaultPremiumBoxLabel; nonPremiumBg: windowBgOver; nonPremiumFg: windowFg; + gradientFromLeft: false; } // Preview. @@ -74,15 +86,16 @@ premiumVideoWidth: 182px; // Graphics. -premiumBubblePadding: margins(14px, 0px, 14px, 0px); -premiumBubblePenWidth: 6; -premiumBubbleHeight: 40px; -premiumBubbleSkip: 8px; -premiumBubbleWidthLimit: 80px; -premiumBubbleTextSkip: 3px; -premiumBubbleSlideDuration: 1000; -premiumBubbleTailSize: size(21px, 7px); -premiumBubbleFont: font(19px); +defaultPremiumBubble: PremiumBubble { + widthLimit: 80px; + height: 40px; + padding: margins(14px, 0px, 14px, 0px); + skip: 8px; + penWidth: 6px; + textSkip: 3px; + tailSize: size(21px, 7px); + font: font(19px); +} premiumLineTextSkip: 11px; premiumInfographicPadding: margins(0px, 10px, 0px, 15px); @@ -93,6 +106,7 @@ premiumIconGroups: icon {{ "limits/groups", settingsIconFg }}; premiumIconLinks: icon {{ "limits/links", settingsIconFg }}; premiumIconPins: icon {{ "limits/pins", settingsIconFg }}; premiumIconAccounts: icon {{ "limits/accounts", settingsIconFg }}; +premiumIconBoost: icon {{ "limits/boost", settingsIconFg }}; premiumAccountsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { imageRadius: 27px; @@ -176,3 +190,43 @@ premiumGiftTerms: FlatLabel(defaultFlatLabel) { premiumGiftBox: Box(premiumPreviewBox) { buttonPadding: margins(12px, 12px, 12px, 12px); } + +boostSkipTop: 37px; +boostLimits: PremiumLimits(defaultPremiumLimits) { + gradientFromLeft: true; +} +boostBubble: PremiumBubble(defaultPremiumBubble) { + height: 32px; + padding: margins(7px, 0px, 11px, 0px); + skip: 5px; + textSkip: 2px; + tailSize: size(14px, 6px); + font: font(16px); +} +boostTitleSkip: 32px; +boostTitle: FlatLabel(defaultFlatLabel) { + minWidth: 40px; + textFg: windowBoldFg; + align: align(top); + maxHeight: 24px; + style: TextStyle(boxTextStyle) { + font: font(17px semibold); + linkFont: font(17px semibold); + linkFontOver: font(17px semibold); + } +} +boostTextSkip: 5px; +boostText: FlatLabel(defaultFlatLabel) { + minWidth: 40px; + align: align(top); +} +boostBottomSkip: 6px; +boostBox: Box(premiumPreviewDoubledLimitsBox) { + buttonPadding: margins(22px, 22px, 22px, 22px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + } +} diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index 693279537..a84933641 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -38,6 +38,7 @@ using TextFactory = Fn; constexpr auto kBubbleRadiusSubtractor = 2; constexpr auto kDeflectionSmall = 20.; constexpr auto kDeflection = 30.; +constexpr auto kSlideDuration = crl::time(1000); constexpr auto kStepBeforeDeflection = 0.75; constexpr auto kStepAfterDeflection = kStepBeforeDeflection @@ -185,6 +186,7 @@ public: using EdgeProgress = float64; Bubble( + const style::PremiumBubble &st, Fn updateCallback, TextFactory textFactory, const style::icon *icon, @@ -206,14 +208,13 @@ public: private: [[nodiscard]] int filledWidth() const; + const style::PremiumBubble &_st; + const Fn _updateCallback; const TextFactory _textFactory; - const style::font &_font; - const style::margins &_padding; const style::icon *_icon; NumbersAnimation _numberAnimation; - const QSize _tailSize; const int _height; const int _textTop; const bool _premiumPossible; @@ -227,19 +228,18 @@ private: }; Bubble::Bubble( + const style::PremiumBubble &st, Fn updateCallback, TextFactory textFactory, const style::icon *icon, bool premiumPossible) -: _updateCallback(std::move(updateCallback)) +: _st(st) +, _updateCallback(std::move(updateCallback)) , _textFactory(std::move(textFactory)) -, _font(st::premiumBubbleFont) -, _padding(st::premiumBubblePadding) , _icon(icon) -, _numberAnimation(_font, _updateCallback) -, _tailSize(st::premiumBubbleTailSize) -, _height(st::premiumBubbleHeight + _tailSize.height()) -, _textTop((_height - _tailSize.height() - _font->height) / 2) +, _numberAnimation(_st.font, _updateCallback) +, _height(_st.height + _st.tailSize.height()) +, _textTop((_height - _st.tailSize.height() - _st.font->height) / 2) , _premiumPossible(premiumPossible) { _numberAnimation.setDisabledMonospace(true); _numberAnimation.setWidthChangedCallback([=] { @@ -258,14 +258,14 @@ int Bubble::height() const { } int Bubble::bubbleRadius() const { - return (_height - _tailSize.height()) / 2 - kBubbleRadiusSubtractor; + return (_height - _st.tailSize.height()) / 2 - kBubbleRadiusSubtractor; } int Bubble::filledWidth() const { - return _padding.left() + return _st.padding.left() + _icon->width() - + st::premiumBubbleTextSkip - + _padding.right(); + + _st.textSkip + + _st.padding.right(); } int Bubble::width() const { @@ -273,7 +273,7 @@ int Bubble::width() const { } int Bubble::countMaxWidth(int maxCounter) const { - auto numbers = Ui::NumbersAnimation(_font, [] {}); + auto numbers = Ui::NumbersAnimation(_st.font, [] {}); numbers.setDisabledMonospace(true); numbers.setDuration(0); numbers.setText(_textFactory(0), 0); @@ -302,18 +302,18 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { return; } - const auto penWidth = st::premiumBubblePenWidth; + const auto penWidth = _st.penWidth; const auto penWidthHalf = penWidth / 2; const auto bubbleRect = r - style::margins( penWidthHalf, penWidthHalf, penWidthHalf, - _tailSize.height() + penWidthHalf); + _st.tailSize.height() + penWidthHalf); { const auto radius = bubbleRadius(); auto pathTail = QPainterPath(); - const auto tailWHalf = _tailSize.width() / 2.; + const auto tailWHalf = _st.tailSize.width() / 2.; const auto progress = _tailEdge; const auto tailTop = bubbleRect.y() + bubbleRect.height(); @@ -326,7 +326,7 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { const auto tailCenter = tailLeft + tailWHalf; const auto tailRight = [&] { const auto max = bubbleRect.x() + bubbleRect.width(); - const auto right = tailLeft + _tailSize.width(); + const auto right = tailLeft + _st.tailSize.width(); const auto bottomMax = max - radius; return (right > bottomMax) ? std::max(float64(tailCenter), float64(bottomMax)) @@ -335,7 +335,7 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { if (_premiumPossible) { pathTail.moveTo(tailLeftFull, tailTop); pathTail.lineTo(tailLeft, tailTop); - pathTail.lineTo(tailCenter, tailTop + _tailSize.height()); + pathTail.lineTo(tailCenter, tailTop + _st.tailSize.height()); pathTail.lineTo(tailRight, tailTop); pathTail.lineTo(tailRight, tailTop - radius); pathTail.moveTo(tailLeftFull, tailTop); @@ -365,8 +365,8 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { } } p.setPen(st::activeButtonFg); - p.setFont(_font); - const auto iconLeft = r.x() + _padding.left(); + p.setFont(_st.font); + const auto iconLeft = r.x() + _st.padding.left(); _icon->paint( p, iconLeft, @@ -374,7 +374,7 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { bubbleRect.width()); _numberAnimation.paint( p, - iconLeft + _icon->width() + st::premiumBubbleTextSkip, + iconLeft + _icon->width() + _st.textSkip, r.y() + _textTop, width() / 2); } @@ -387,8 +387,9 @@ class BubbleWidget final : public Ui::RpWidget { public: BubbleWidget( not_null parent, + const style::PremiumBubble &st, TextFactory textFactory, - int current, + rpl::producer state, int maxCounter, bool premiumPossible, rpl::producer<> showFinishes, @@ -398,7 +399,12 @@ protected: void paintEvent(QPaintEvent *e) override; private: - const int _currentCounter; + void animateTo(BubbleRowState state); + + const style::PremiumBubble &_st; + BubbleRowState _animatingFrom; + float64 _animatingFromResultRatio = 0.; + rpl::variable _state; const int _maxCounter; Bubble _bubble; const int _maxBubbleWidth; @@ -419,28 +425,33 @@ private: BubbleWidget::BubbleWidget( not_null parent, + const style::PremiumBubble &st, TextFactory textFactory, - int current, + rpl::producer state, int maxCounter, bool premiumPossible, rpl::producer<> showFinishes, const style::icon *icon) : RpWidget(parent) -, _currentCounter(current) +, _st(st) +, _state(std::move(state)) , _maxCounter(maxCounter) -, _bubble([=] { update(); }, std::move(textFactory), icon, premiumPossible) +, _bubble( + _st, + [=] { update(); }, + std::move(textFactory), + icon, + premiumPossible) , _maxBubbleWidth(_bubble.countMaxWidth(_maxCounter)) , _premiumPossible(premiumPossible) , _deflection(kDeflection) , _stepBeforeDeflection(kStepBeforeDeflection) , _stepAfterDeflection(kStepAfterDeflection) { const auto resizeTo = [=](int w, int h) { - _deflection = (w > st::premiumBubbleWidthLimit) + _deflection = (w > _st.widthLimit) ? kDeflectionSmall : kDeflection; - _spaceForDeflection = QSize( - st::premiumBubbleSkip, - st::premiumBubbleSkip); + _spaceForDeflection = QSize(_st.skip, _st.skip); resize(QSize(w, h) + _spaceForDeflection); }; @@ -450,97 +461,113 @@ BubbleWidget::BubbleWidget( resizeTo(_bubble.width(), _bubble.height()); }, lifetime()); - const auto moveEndPoint = _currentCounter / float64(_maxCounter); + std::move( + showFinishes + ) | rpl::take(1) | rpl::start_with_next([=] { + _state.value( + ) | rpl::start_with_next([=](BubbleRowState state) { + animateTo(state); + }, lifetime()); + }, lifetime()); +} + +void BubbleWidget::animateTo(BubbleRowState state) { + const auto parent = parentWidget(); const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { const auto &padding = st::boxRowPadding; const auto halfWidth = (_maxBubbleWidth / 2); const auto left = padding.left(); const auto right = padding.right(); - return ((parent->width() - left - right) - * pointRatio - * animProgress) - - halfWidth - + left; + const auto available = parent->width() - left - right; + const auto delta = (pointRatio - _animatingFromResultRatio); + const auto center = available + * (_animatingFromResultRatio + delta * animProgress); + return center - halfWidth + left; }; - - std::move( - showFinishes - ) | rpl::take(1) | rpl::start_with_next([=] { - const auto computeEdge = [=] { - return parent->width() - - st::boxRowPadding.right() - - _maxBubbleWidth; - }; - struct LeftEdge final { - float64 goodPointRatio = 0.; - float64 bubbleLeftEdge = 0.; - }; - const auto leftEdge = [&]() -> LeftEdge { - const auto finish = computeLeft(moveEndPoint, 1.); - const auto &padding = st::boxRowPadding; - if (finish <= padding.left()) { - const auto halfWidth = (_maxBubbleWidth / 2); - const auto goodPointRatio = float64(halfWidth) - / (parent->width() - padding.left() - padding.right()); - const auto bubbleLeftEdge = (padding.left() - finish) - / (_maxBubbleWidth / 2.); - return { goodPointRatio, bubbleLeftEdge }; - } - return {}; - }(); - const auto checkBubbleRightEdge = [&]() -> Bubble::EdgeProgress { - const auto finish = computeLeft(moveEndPoint, 1.); - const auto edge = computeEdge(); - return (finish >= edge) - ? (finish - edge) / (_maxBubbleWidth / 2.) - : 0.; - }; - const auto bubbleRightEdge = checkBubbleRightEdge(); - _ignoreDeflection = bubbleRightEdge || leftEdge.goodPointRatio; - if (_ignoreDeflection) { - _stepBeforeDeflection = 1.; - _stepAfterDeflection = 1.; + const auto moveEndPoint = state.ratio; + const auto computeEdge = [=] { + return parent->width() + - st::boxRowPadding.right() + - _maxBubbleWidth; + }; + struct LeftEdge final { + float64 goodPointRatio = 0.; + float64 bubbleLeftEdge = 0.; + }; + const auto leftEdge = [&]() -> LeftEdge { + const auto finish = computeLeft(moveEndPoint, 1.); + const auto &padding = st::boxRowPadding; + if (finish <= padding.left()) { + const auto halfWidth = (_maxBubbleWidth / 2); + const auto goodPointRatio = float64(halfWidth) + / (parent->width() - padding.left() - padding.right()); + const auto bubbleLeftEdge = (padding.left() - finish) + / (_maxBubbleWidth / 2.); + return { goodPointRatio, bubbleLeftEdge }; } - const auto resultMoveEndPoint = leftEdge.goodPointRatio - ? leftEdge.goodPointRatio - : moveEndPoint; - _bubble.setFlipHorizontal(leftEdge.bubbleLeftEdge); + return {}; + }(); + const auto checkBubbleRightEdge = [&]() -> Bubble::EdgeProgress { + const auto finish = computeLeft(moveEndPoint, 1.); + const auto edge = computeEdge(); + return (finish >= edge) + ? (finish - edge) / (_maxBubbleWidth / 2.) + : 0.; + }; + const auto bubbleRightEdge = checkBubbleRightEdge(); + _ignoreDeflection = !_state.current().dynamic + && (bubbleRightEdge || leftEdge.goodPointRatio); + if (_ignoreDeflection) { + _stepBeforeDeflection = 1.; + _stepAfterDeflection = 1.; + } + const auto resultMoveEndPoint = leftEdge.goodPointRatio + ? leftEdge.goodPointRatio + : moveEndPoint; + _bubble.setFlipHorizontal(leftEdge.bubbleLeftEdge); - _appearanceAnimation.start([=](float64 value) { - const auto moveProgress = std::clamp( - (value / _stepBeforeDeflection), - 0., - 1.); - const auto counterProgress = std::clamp( - (value / _stepAfterDeflection), - 0., - 1.); - moveToLeft( - computeLeft(resultMoveEndPoint, moveProgress) - - (_maxBubbleWidth / 2.) * bubbleRightEdge, - 0); + const auto duration = kSlideDuration + * (_ignoreDeflection ? kStepBeforeDeflection : 1.) + * ((_state.current().ratio < 0.001) ? 0.5 : 1.); + _appearanceAnimation.start([=](float64 value) { + if (!_appearanceAnimation.animating()) { + _animatingFrom = state; + _animatingFromResultRatio = resultMoveEndPoint; + } + const auto moveProgress = std::clamp( + (value / _stepBeforeDeflection), + 0., + 1.); + const auto counterProgress = std::clamp( + (value / _stepAfterDeflection), + 0., + 1.); + moveToLeft( + std::max( + int(base::SafeRound( + (computeLeft(resultMoveEndPoint, moveProgress) + - (_maxBubbleWidth / 2.) * bubbleRightEdge))), + 0), + 0); - const auto counter = int(0 + counterProgress * _currentCounter); - // if (!(counter % 4) || counterProgress > 0.8) { - _bubble.setCounter(counter); - // } + const auto now = _animatingFrom.counter + + counterProgress * (state.counter - _animatingFrom.counter); + _bubble.setCounter(int(base::SafeRound(now))); - const auto edgeProgress = (leftEdge.bubbleLeftEdge - ? leftEdge.bubbleLeftEdge - : bubbleRightEdge) * value; - _bubble.setTailEdge(edgeProgress); - update(); - }, - 0., - 1., - st::premiumBubbleSlideDuration - * (_ignoreDeflection ? kStepBeforeDeflection : 1.), - anim::easeOutCirc); - }, lifetime()); + const auto edgeProgress = leftEdge.bubbleLeftEdge + ? leftEdge.bubbleLeftEdge + : (bubbleRightEdge * value); + _bubble.setTailEdge(edgeProgress); + update(); + }, + 0., + 1., + duration, + anim::easeOutCirc); } void BubbleWidget::paintEvent(QPaintEvent *e) { - if (_bubble.counter() <= 0) { + if (_bubble.counter() < 0) { return; } @@ -561,10 +588,11 @@ void BubbleWidget::paintEvent(QPaintEvent *e) { _cachedGradient = std::move(gradient); const auto progress = _appearanceAnimation.value(1.); - const auto scaleProgress = std::clamp( - (progress / _stepBeforeDeflection), - 0., - 1.); + const auto finalScale = (_animatingFromResultRatio > 0.) + || (_state.current().ratio < 0.001); + const auto scaleProgress = finalScale + ? 1. + : std::clamp((progress / _stepBeforeDeflection), 0., 1.); const auto scale = scaleProgress; const auto rotationProgress = std::clamp( (progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection), @@ -608,6 +636,12 @@ public: QString min, float64 ratio); + Line( + not_null parent, + const style::PremiumLimits &st, + LimitRowLabels labels, + rpl::producer ratio); + void setColorOverride(QBrush brush); protected: @@ -618,16 +652,16 @@ private: const style::PremiumLimits &_st; - int _leftWidth = 0; - int _rightWidth = 0; - QPixmap _leftPixmap; QPixmap _rightPixmap; - Ui::Text::String _leftText; - Ui::Text::String _rightText; - Ui::Text::String _rightLabel; + float64 _ratio = 0.; + Ui::Animations::Simple _animation; Ui::Text::String _leftLabel; + Ui::Text::String _leftText; + Ui::Text::String _rightLabel; + Ui::Text::String _rightText; + bool _dynamic = false; std::optional _overrideBrush; @@ -654,24 +688,47 @@ Line::Line( QString max, QString min, float64 ratio) +: Line(parent, st, LimitRowLabels{ + .leftLabel = tr::lng_premium_free(tr::now), + .leftCount = min, + .rightLabel = tr::lng_premium(tr::now), + .rightCount = max, +}, rpl::single(ratio)) { +} + +Line::Line( + not_null parent, + const style::PremiumLimits &st, + LimitRowLabels labels, + rpl::producer ratio) : Ui::RpWidget(parent) , _st(st) -, _leftText(st::semiboldTextStyle, tr::lng_premium_free(tr::now)) -, _rightText(st::semiboldTextStyle, tr::lng_premium(tr::now)) -, _rightLabel(st::semiboldTextStyle, max) -, _leftLabel(st::semiboldTextStyle, min) { +, _leftLabel(st::semiboldTextStyle, labels.leftLabel) +, _leftText(st::semiboldTextStyle, labels.leftCount) +, _rightLabel(st::semiboldTextStyle, labels.rightLabel) +, _rightText(st::semiboldTextStyle, labels.rightCount) +, _dynamic(labels.dynamic) { resize(width(), st::requestsAcceptButton.height); - sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - if (s.isEmpty()) { - return; + std::move(ratio) | rpl::start_with_next([=](float64 ratio) { + if (width() > 0) { + const auto from = _animation.value(_ratio); + const auto duration = kSlideDuration * kStepBeforeDeflection; + _animation.start([=] { + update(); + }, from, ratio, duration, anim::easeOutCirc); } - _leftWidth = int(base::SafeRound(s.width() * ratio)); - _rightWidth = (s.width() - _leftWidth); - recache(s); + _ratio = ratio; + }, lifetime()); + + sizeValue( + ) | rpl::filter([](QSize size) { + return !size.isEmpty(); + }) | rpl::start_with_next([=](QSize size) { + recache(size); update(); }, lifetime()); + } void Line::setColorOverride(QBrush brush) { @@ -685,52 +742,64 @@ void Line::setColorOverride(QBrush brush) { void Line::paintEvent(QPaintEvent *event) { Painter p(this); - p.drawPixmap(0, 0, _leftPixmap); - p.drawPixmap(_leftWidth, 0, _rightPixmap); + const auto ratio = _animation.value(_ratio); + const auto left = int(base::SafeRound(ratio * width())); + const auto dpr = int(_leftPixmap.devicePixelRatio()); + const auto height = _leftPixmap.height() / dpr; + p.drawPixmap( + QRect(0, 0, left, height), + _leftPixmap, + QRect(0, 0, left * dpr, height * dpr)); + p.drawPixmap( + QRect(left, 0, width() - left, height), + _rightPixmap, + QRect(left * dpr, 0, (width() - left) * dpr, height * dpr)); p.setFont(st::normalFont); const auto textPadding = st::premiumLineTextSkip; - const auto textTop = (height() - _leftText.minHeight()) / 2; + const auto textTop = (height - _leftLabel.minHeight()) / 2; const auto leftMinWidth = _leftLabel.maxWidth() + _leftText.maxWidth() + 3 * textPadding; - if (_leftWidth >= leftMinWidth) { - p.setPen(_st.nonPremiumFg); - _leftLabel.drawRight( + const auto pen = [&](bool gradient) { + return gradient ? st::activeButtonFg : _st.nonPremiumFg; + }; + if (!_dynamic && left >= leftMinWidth) { + p.setPen(pen(_st.gradientFromLeft)); + _leftLabel.drawLeft( p, textPadding, textTop, - _leftWidth - textPadding, - _leftWidth, + left - textPadding, + left); + _leftText.drawRight( + p, + textPadding, + textTop, + left - textPadding, + left, style::al_right); - _leftText.drawLeft( - p, - textPadding, - textTop, - _leftWidth - textPadding, - _leftWidth); } - const auto rightMinWidth = 2 * _rightLabel.maxWidth() + const auto right = width() - left; + const auto rightMinWidth = 2 * _rightText.maxWidth() + 3 * textPadding; - if (_rightWidth >= rightMinWidth) { - p.setPen(st::activeButtonFg); - _rightLabel.drawRight( + if (!_dynamic && right >= rightMinWidth) { + p.setPen(pen(!_st.gradientFromLeft)); + _rightLabel.drawLeftElided( + p, + left + textPadding, + textTop, + (right - _rightText.countWidth(right) - textPadding * 2), + right); + _rightText.drawRight( p, textPadding, textTop, - _rightWidth - textPadding, + right - textPadding, width(), style::al_right); - _rightText.drawLeftElided( - p, - _leftWidth + textPadding, - textTop, - (_rightWidth - - _rightLabel.countWidth(_rightWidth) - - textPadding * 2), - _rightWidth); } } @@ -750,40 +819,46 @@ void Line::recache(const QSize &s) { result.addRoundedRect(r(width), st::buttonRadius, st::buttonRadius); return result; }; + const auto width = s.width(); + const auto fill = [&](QPainter &p, QPainterPath path, bool gradient) { + if (!gradient) { + p.fillPath(path, _st.nonPremiumBg); + } else if (_overrideBrush) { + p.fillPath(path, *_overrideBrush); + } else { + p.fillPath(path, QBrush(ComputeGradient(this, 0, width))); + } + }; + const auto textPadding = st::premiumLineTextSkip; + const auto textTop = (s.height() - _leftLabel.minHeight()) / 2; + const auto rwidth = _rightLabel.maxWidth(); + const auto pen = [&](bool gradient) { + return gradient ? st::activeButtonFg : _st.nonPremiumFg; + }; { - auto leftPixmap = pixmap(_leftWidth); - auto p = QPainter(&leftPixmap); + auto leftPixmap = pixmap(width); + auto p = Painter(&leftPixmap); PainterHighQualityEnabler hq(p); - auto pathRect = QPainterPath(); - auto halfRect = r(_leftWidth); - halfRect.setLeft(halfRect.center().x()); - pathRect.addRect(halfRect); - - p.fillPath(pathRound(_leftWidth) + pathRect, _st.nonPremiumBg); - + fill(p, pathRound(width), _st.gradientFromLeft); + if (_dynamic) { + p.setFont(st::normalFont); + p.setPen(pen(_st.gradientFromLeft)); + _leftLabel.drawLeft(p, textPadding, textTop, width, width); + _rightLabel.drawRight(p, textPadding, textTop, rwidth, width); + } _leftPixmap = std::move(leftPixmap); } { - auto rightPixmap = pixmap(_rightWidth); - auto p = QPainter(&rightPixmap); + auto rightPixmap = pixmap(width); + auto p = Painter(&rightPixmap); PainterHighQualityEnabler hq(p); - auto pathRect = QPainterPath(); - auto halfRect = r(_rightWidth); - halfRect.setRight(halfRect.center().x()); - pathRect.addRect(halfRect); - - if (_overrideBrush) { - p.fillPath(pathRound(_rightWidth) + pathRect, *_overrideBrush); - } else { - auto gradient = ComputeGradient( - this, - _leftPixmap.width() / style::DevicePixelRatio(), - _rightWidth); - p.fillPath( - pathRound(_rightWidth) + pathRect, - QBrush(std::move(gradient))); + fill(p, pathRound(width), !_st.gradientFromLeft); + if (_dynamic) { + p.setFont(st::normalFont); + p.setPen(pen(!_st.gradientFromLeft)); + _leftLabel.drawLeft(p, textPadding, textTop, width, width); + _rightLabel.drawRight(p, textPadding, textTop, rwidth, width); } - _rightPixmap = std::move(rightPixmap); } } @@ -792,6 +867,7 @@ void Line::recache(const QSize &s) { void AddBubbleRow( not_null parent, + const style::PremiumBubble &st, rpl::producer<> showFinishes, int min, int current, @@ -799,12 +875,36 @@ void AddBubbleRow( bool premiumPossible, std::optional> phrase, const style::icon *icon) { + AddBubbleRow( + parent, + st, + std::move(showFinishes), + rpl::single(BubbleRowState{ + .counter = current, + .ratio = (current - min) / float64(max - min), + }), + max, + premiumPossible, + ProcessTextFactory(phrase), + icon); +} + +void AddBubbleRow( + not_null parent, + const style::PremiumBubble &st, + rpl::producer<> showFinishes, + rpl::producer state, + int max, + bool premiumPossible, + Fn text, + const style::icon *icon) { const auto container = parent->add( object_ptr(parent, 0)); const auto bubble = Ui::CreateChild( container, - ProcessTextFactory(phrase), - current, + st, + text ? std::move(text) : ProcessTextFactory(std::nullopt), + std::move(state), max, premiumPossible, std::move(showFinishes), @@ -844,6 +944,16 @@ void AddLimitRow( ratio); } +void AddLimitRow( + not_null parent, + const style::PremiumLimits &st, + LimitRowLabels labels, + rpl::producer ratio) { + parent->add( + object_ptr(parent, st, std::move(labels), std::move(ratio)), + st::boxRowPadding); +} + void AddAccountsRow( not_null parent, AccountsRowArgs &&args) { diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h index 7f932a9bd..eefd9a885 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.h +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -28,6 +28,7 @@ namespace style { struct RoundImageCheckbox; struct PremiumOption; struct TextStyle; +struct PremiumBubble; } // namespace style namespace Ui { @@ -42,6 +43,7 @@ inline constexpr auto kLimitRowRatio = 0.5; void AddBubbleRow( not_null parent, + const style::PremiumBubble &st, rpl::producer<> showFinishes, int min, int current, @@ -50,6 +52,21 @@ void AddBubbleRow( std::optional> phrase, const style::icon *icon); +struct BubbleRowState { + int counter = 0; + float64 ratio = 0.; + bool dynamic = false; +}; +void AddBubbleRow( + not_null parent, + const style::PremiumBubble &st, + rpl::producer<> showFinishes, + rpl::producer state, + int max, + bool premiumPossible, + Fn text, + const style::icon *icon); + void AddLimitRow( not_null parent, const style::PremiumLimits &st, @@ -65,6 +82,19 @@ void AddLimitRow( int min = 0, float64 ratio = kLimitRowRatio); +struct LimitRowLabels { + QString leftLabel; + QString leftCount; + QString rightLabel; + QString rightCount; + bool dynamic = false; +}; +void AddLimitRow( + not_null parent, + const style::PremiumLimits &st, + LimitRowLabels labels, + rpl::producer ratio); + struct AccountsRowArgs final { std::shared_ptr group; const style::RoundImageCheckbox &st; diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index c6c34a0d9..7ed644064 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -316,7 +316,10 @@ base::unique_qptr FiltersMenu::prepareButton( if (_reordering) { return; } else if (raw->locked()) { - _session->show(Box(FiltersLimitBox, &_session->session())); + _session->show(Box( + FiltersLimitBox, + &_session->session(), + std::nullopt)); } else if (id >= 0) { _session->setActiveChatsFilter(id); } else { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 61bc472d9..714eca63b 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/text/format_values.h" // Ui::FormatPhone. #include "ui/delayed_activation.h" +#include "ui/boxes/boost_box.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -80,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/themes/window_theme.h" #include "window/window_peer_menu.h" #include "settings/settings_main.h" +#include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "styles/style_window.h" #include "styles/style_dialogs.h" @@ -554,6 +556,8 @@ void SessionNavigation::showPeerByLinkResolved( } else { showPeerInfo(peer, params); } + } else if (resolveType == ResolveType::Boost && peer->isBroadcast()) { + resolveBoostState(peer->asChannel()); } else { // Show specific posts only in channels / supergroups. const auto msgId = peer->isChannel() @@ -614,6 +618,145 @@ void SessionNavigation::showPeerByLinkResolved( } } +void SessionNavigation::resolveBoostState(not_null channel) { + if (_boostStateResolving == channel) { + return; + } + _boostStateResolving = channel; + _api.request(MTPstories_GetBoostsStatus( + channel->input + )).done([=](const MTPstories_BoostsStatus &result) { + _boostStateResolving = nullptr; + const auto &data = result.data(); + const auto submit = [=](Fn done) { + applyBoost(channel, done); + }; + const auto next = data.vnext_level_boosts().value_or_empty(); + uiShow()->show(Box(Ui::BoostBox, Ui::BoostBoxData{ + .name = channel->name(), + .boost = { + .level = data.vlevel().v, + .boosts = data.vboosts().v, + .thisLevelBoosts = data.vcurrent_level_boosts().v, + .nextLevelBoosts = next, + }, + }, submit)); + }).fail([=](const MTP::Error &error) { + _boostStateResolving = nullptr; + showToast(u"Error: "_q + error.type()); + }).send(); +} + +void SessionNavigation::applyBoost( + not_null channel, + Fn done) { + _api.request(MTPstories_CanApplyBoost( + channel->input + )).done([=](const MTPstories_CanApplyBoostResult &result) { + result.match([&](const MTPDstories_canApplyBoostOk &) { + applyBoostChecked(channel, done); + }, [&](const MTPDstories_canApplyBoostReplace &data) { + _session->data().processChats(data.vchats()); + const auto peer = _session->data().peer( + peerFromMTP(data.vcurrent_boost())); + replaceBoostConfirm(peer, channel, done); + }); + }).fail([=](const MTP::Error &error) { + const auto type = error.type(); + if (type == u"PREMIUM_ACCOUNT_REQUIRED"_q) { + const auto jumpToPremium = [=] { + const auto id = peerToChannel(channel->id).bare; + Settings::ShowPremium( + parentController(), + "channel_boost__" + QString::number(id)); + }; + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_premium_text( + Ui::Text::RichLangValue), + .confirmed = jumpToPremium, + .confirmText = tr::lng_boost_error_premium_yes(), + .title = tr::lng_boost_error_premium_title(), + })); + } else if (type == u"PREMIUM_GIFTED_NOT_ALLOWED"_q) { + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_gifted_text( + Ui::Text::RichLangValue), + .title = tr::lng_boost_error_gifted_title(), + .inform = true, + })); + } else if (type == u"BOOST_NOT_MODIFIED"_q) { + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_already_text( + Ui::Text::RichLangValue), + .title = tr::lng_boost_error_already_title(), + .inform = true, + })); + } else if (type.startsWith(u"FLOOD_WAIT_"_q)) { + const auto seconds = type.mid(u"FLOOD_WAIT_"_q.size()).toInt(); + const auto days = seconds / 86400; + const auto hours = seconds / 3600; + const auto minutes = seconds / 60; + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_flood_text( + lt_left, + rpl::single(Ui::Text::Bold((days > 1) + ? tr::lng_days(tr::now, lt_count, days) + : (hours > 1) + ? tr::lng_hours(tr::now, lt_count, hours) + : (minutes > 1) + ? tr::lng_minutes(tr::now, lt_count, minutes) + : tr::lng_seconds(tr::now, lt_count, seconds))), + Ui::Text::RichLangValue), + .title = tr::lng_boost_error_flood_title(), + .inform = true, + })); + } else { + showToast(u"Error: "_q + type); + } + done(false); + }).handleFloodErrors().send(); +} + +void SessionNavigation::replaceBoostConfirm( + not_null from, + not_null channel, + Fn done) { + const auto forwarded = std::make_shared(false); + const auto confirmed = [=](Fn close) { + *forwarded = true; + applyBoostChecked(channel, done); + close(); + }; + const auto box = uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_now_instead( + lt_channel, + rpl::single(Ui::Text::Bold(from->name())), + lt_other, + rpl::single(Ui::Text::Bold(channel->name())), + Ui::Text::WithEntities), + .confirmed = confirmed, + .confirmText = tr::lng_boost_now_replace(), + })); + box->boxClosing() | rpl::filter([=] { + return !*forwarded; + }) | rpl::start_with_next([=] { + done(false); + }, box->lifetime()); +} + +void SessionNavigation::applyBoostChecked( + not_null channel, + Fn done) { + _api.request(MTPstories_ApplyBoost( + channel->input + )).done([=](const MTPBool &result) { + done(true); + }).fail([=](const MTP::Error &error) { + showToast(u"Error: "_q + error.type()); + done(false); + }).send(); +} + void SessionNavigation::joinVoiceChatFromLink( not_null peer, const PeerByLinkInfo &info) { diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 8001aef05..367be098f 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -100,6 +100,7 @@ enum class ResolveType { AddToChannel, ShareGame, Mention, + Boost, }; struct PeerThemeOverride { @@ -311,6 +312,16 @@ private: not_null peer, const PeerByLinkInfo &info); + void resolveBoostState(not_null channel); + void applyBoost(not_null channel, Fn done); + void replaceBoostConfirm( + not_null from, + not_null channel, + Fn done); + void applyBoostChecked( + not_null channel, + Fn done); + const not_null _session; MTP::Sender _api; @@ -321,6 +332,8 @@ private: MsgId _showingRepliesRootId = 0; mtpRequestId _showingRepliesRequestId = 0; + ChannelData *_boostStateResolving = nullptr; + }; class SessionController : public SessionNavigation { diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index d7816d5d0..f7617ca22 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -160,6 +160,8 @@ PRIVATE ui/boxes/auto_delete_settings.cpp ui/boxes/auto_delete_settings.h + ui/boxes/boost_box.cpp + ui/boxes/boost_box.h ui/boxes/calendar_box.cpp ui/boxes/calendar_box.h ui/boxes/choose_date_time.cpp