diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e49a14109..cfac07e74 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1702,6 +1702,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_unlock_stickers" = "Unlock Premium Stickers"; "lng_premium_unlock_emoji" = "Unlock Animated Emoji"; +"lng_premium_subscribe_months_12" = "Annual"; +"lng_premium_subscribe_months_6" = "Semiannual"; +"lng_premium_subscribe_months_1" = "Monthly"; +"lng_premium_subscribe_total" = "{cost} per year"; +"lng_premium_subscribe_button" = "Subscribe for {cost} per month"; + "lng_premium_emoji_status_title" = "{user} set this emoji from {link} as their current status."; "lng_premium_emoji_status_about" = "Emoji status is a premium feature. Other features included in **Telegram Premium**:"; "lng_premium_emoji_status_button" = "Unlock Emoji Status"; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index f400ce4fd..7f305bd5c 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -10,8 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_premium_option.h" #include "base/weak_ptr.h" -#include "core/click_handler_types.h" // ClickHandlerContext. -#include "core/local_url_handlers.h" // TryConvertUrlToLocal. #include "data/data_changes.h" #include "data/data_peer_values.h" // Data::PeerPremiumValue. #include "data/data_session.h" @@ -167,7 +165,11 @@ void GiftBox( options[value].total); state->buttonText.fire(std::move(text)); }); - Ui::Premium::AddGiftOptions(buttonsParent, group, options); + Ui::Premium::AddGiftOptions( + buttonsParent, + group, + options, + st::premiumGiftOption); // Footer. auto terms = object_ptr( @@ -198,26 +200,17 @@ void GiftBox( [] { return QString("gift"); }, state->buttonText.events(), Ui::Premium::GiftGradientStops(), + [=] { + const auto value = group->value(); + return (value < options.size() && value >= 0) + ? options[value].botUrl + : QString(); + }, }); auto button = object_ptr::fromRaw(raw); button->resizeToWidth(boxWidth - stButton.buttonPadding.left() - stButton.buttonPadding.right()); - button->setClickedCallback([=] { - const auto value = group->value(); - Assert(value < options.size() && value >= 0); - - const auto local = Core::TryConvertUrlToLocal(options[value].botUrl); - if (local.isEmpty()) { - return; - } - UrlClickHandler::Open( - local, - QVariant::fromValue(ClickHandlerContext{ - .sessionWindow = base::make_weak(controller.get()), - .botStartAutoSubmit = true, - })); - }); box->setShowFinishedCallback([raw = button.data()]{ raw->startGlareAnimation(); }); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 419aa2029..207a0f500 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -489,6 +489,7 @@ settingsPremiumArrow: icon{{ "fast_to_original", menuIconFg }}; settingsPremiumArrowOver: icon{{ "fast_to_original", menuIconFgOver }}; settingsPremiumStatusPadding: margins(22px, 8px, 22px, 2px); +settingsPremiumOptionsPadding: margins(0px, 9px, 0px, 2px); settingsPremiumTopHeight: 220px; settingsPremiumUserHeight: 223px; settingsPremiumUserTitlePadding: margins(0px, 16px, 0px, 6px); diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index ce2f05c1c..9b5260244 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_lottie.h" // LottiePlayerFromDocument. #include "core/application.h" #include "core/click_handler_types.h" +#include "core/local_url_handlers.h" // Core::TryConvertUrlToLocal. #include "core/ui_integration.h" // MarkedTextContext. #include "data/data_document.h" #include "data/data_document_media.h" @@ -39,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/checkbox.h" // Ui::RadiobuttonGroup. #include "ui/widgets/gradient_round_button.h" #include "ui/widgets/labels.h" #include "ui/wrap/fade_wrap.h" @@ -69,6 +71,33 @@ constexpr auto kTitleAdditionalScale = 0.15; return u":/gui/icons/settings/star.svg"_q; } +[[nodiscard]] Data::SubscriptionOptions SubscriptionOptionsForRows( + Data::SubscriptionOptions result) { + for (auto &option : result) { + const auto total = option.costTotal; + const auto perMonth = option.costPerMonth; + + option.costTotal = tr::lng_premium_gift_per( + tr::now, + lt_cost, + perMonth); + option.costPerMonth = tr::lng_premium_subscribe_total( + tr::now, + lt_cost, + total); + + if (option.duration == tr::lng_months(tr::now, lt_count, 1)) { + option.costPerMonth = QString(); + option.duration = tr::lng_premium_subscribe_months_1(tr::now); + } else if (option.duration == tr::lng_months(tr::now, lt_count, 6)) { + option.duration = tr::lng_premium_subscribe_months_6(tr::now); + } else if (option.duration == tr::lng_years(tr::now, lt_count, 1)) { + option.duration = tr::lng_premium_subscribe_months_12(tr::now); + } + } + return result; +} + namespace Ref { namespace Gift { @@ -968,6 +997,9 @@ public: private: void setupContent(); + void setupSubscriptionOptions( + not_null container, + int lastSkip); const not_null _controller; const QString _ref; @@ -979,8 +1011,11 @@ private: rpl::variable _wrap; Fn _setPaused; + std::shared_ptr _radioGroup; + rpl::event_stream<> _showBack; rpl::event_stream<> _showFinished; + rpl::event_stream _buttonText; }; @@ -989,7 +1024,8 @@ Premium::Premium( not_null controller) : Section(parent) , _controller(controller) -, _ref(ResolveRef(controller->premiumRef())) { +, _ref(ResolveRef(controller->premiumRef())) +, _radioGroup(std::make_shared()) { setupContent(); _controller->session().api().premium().reload(); } @@ -1016,6 +1052,47 @@ void Premium::setStepDataReference(std::any &data) { } } +void Premium::setupSubscriptionOptions( + not_null container, + int lastSkip) { + const auto options = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto skip = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto content = options->entity(); + + AddSkip(content, st::settingsPremiumOptionsPadding.top()); + + Ui::Premium::AddGiftOptions( + content, + _radioGroup, + SubscriptionOptionsForRows( + _controller->session().api().premium().subscriptionOptions()), + st::premiumSubscriptionOption, + true); + + AddSkip(content, st::settingsPremiumOptionsPadding.bottom()); + AddDivider(content); + + AddSkip(content, lastSkip - st::settingsSectionSkip); + AddSkip(skip->entity(), lastSkip); + + auto toggleOn = rpl::combine( + Data::AmPremiumValue(&_controller->session()), + rpl::single(!!(Ref::EmojiStatus::Parse(_ref))) + ) | rpl::map([=](bool premium, bool isEmojiStatus) { + return !premium && !isEmojiStatus; + }); + options->toggleOn(rpl::duplicate(toggleOn), anim::type::instant); + skip->toggleOn(std::move( + toggleOn + ) | rpl::map([](bool value) { return !value; }), anim::type::instant); +} + void Premium::setupContent() { const auto content = Ui::CreateChild(this); @@ -1025,7 +1102,9 @@ void Premium::setupContent() { const auto &titlePadding = st::settingsPremiumRowTitlePadding; const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; - AddSkip(content, stDefault.padding.top() + titlePadding.top()); + setupSubscriptionOptions( + content, + stDefault.padding.top() + titlePadding.top()); auto entryMap = EntryMap(); auto iconContainers = std::vector(); @@ -1385,10 +1464,11 @@ QPointer Premium::createPinnedToBottom( } const auto emojiStatusData = Ref::EmojiStatus::Parse(_ref); + const auto session = &_controller->session(); auto buttonText = [&]() -> std::optional> { if (emojiStatusData) { - auto &data = _controller->session().data(); + auto &data = session->data(); if (const auto peer = data.peer(emojiStatusData.peerId)) { return Info::Profile::EmojiStatusIdValue( peer @@ -1399,7 +1479,7 @@ QPointer Premium::createPinnedToBottom( }) | rpl::flatten_latest(); } } - return std::nullopt; + return _buttonText.events(); }(); _subscribe = CreateSubscribeButton({ @@ -1407,6 +1487,13 @@ QPointer Premium::createPinnedToBottom( content, [ref = _ref] { return ref; }, std::move(buttonText), + std::nullopt, + [=, options = session->api().premium().subscriptionOptions()] { + const auto value = _radioGroup->value(); + return (value < options.size() && value >= 0) + ? options[value].botUrl + : QString(); + }, }); if (emojiStatusData) { // "Learn More" should open the general Premium Settings @@ -1419,6 +1506,18 @@ QPointer Premium::createPinnedToBottom( ShowPremium(controller, QString()); controller->setPremiumRef(ref); }); + } else { + _radioGroup->setChangedCallback([=](int value) { + const auto options = + _controller->session().api().premium().subscriptionOptions(); + Expects(value < options.size() && value >= 0); + auto text = tr::lng_premium_subscribe_button( + tr::now, + lt_cost, + options[value].costPerMonth); + _buttonText.fire(std::move(text)); + }); + _radioGroup->setValue(0); } _showFinished.events( @@ -1432,7 +1531,6 @@ QPointer Premium::createPinnedToBottom( _subscribe->resizeToWidth(width - padding.left() - padding.right()); }, _subscribe->lifetime()); - const auto session = &_controller->session(); rpl::combine( _subscribe->heightValue(), Data::AmPremiumValue(session), @@ -1543,9 +1641,24 @@ not_null CreateSubscribeButton( result->setClickedCallback([ controller = args.controller, - computeRef = args.computeRef] { - SendScreenAccept(controller); - StartPremiumPayment(controller, computeRef()); + computeRef = args.computeRef, + computeBotUrl = args.computeBotUrl] { + const auto url = computeBotUrl ? QString() : computeBotUrl(); + if (!url.isEmpty()) { + const auto local = Core::TryConvertUrlToLocal(url); + if (local.isEmpty()) { + return; + } + UrlClickHandler::Open( + local, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = base::make_weak(controller.get()), + .botStartAutoSubmit = true, + })); + } else { + SendScreenAccept(controller); + StartPremiumPayment(controller, computeRef()); + } }); const auto &st = st::premiumPreviewBox.button; diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 570749471..e66c294f4 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -53,6 +53,7 @@ struct SubscribeButtonArgs final { Fn computeRef; std::optional> text; std::optional gradientStops; + Fn computeBotUrl; // nullable }; [[nodiscard]] not_null CreateSubscribeButton( diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 24ab0663e..711e3fa16 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -97,17 +97,56 @@ premiumAccountsNameTop: 13px; premiumAccountsPadding: margins(0px, 20px, 0px, 14px); premiumAccountsHeight: 105px; +PremiumOption { + rowPadding: margins; + rowMargins: margins; + rowHeight: pixels; + + borderWidth: pixels; + borderRadius: pixels; + + subtitleTop: pixels; + textLeft: pixels; + + badgeHeight: pixels; + badgeRadius: pixels; + badgeMargins: margins; + badgeShift: point; +} + +premiumSubscriptionOption: PremiumOption { + rowPadding: margins(9px, 2px, 17px, 3px); + rowMargins: margins(14px, 0px, 5px, 0px); + rowHeight: 39px; + + borderWidth: 0px; + borderRadius: 0px; + + subtitleTop: 1px; + textLeft: 51px; + + badgeHeight: 15px; + badgeRadius: 4px; + badgeMargins: margins(3px, 1px, 3px, 0px); + badgeShift: point(9px, 0px); +} + // Gift. -premiumGiftRowHeight: 56px; -premiumGiftRowBorderWidth: 2px; -premiumGiftRowBorderRadius: 9px; -premiumGiftRowPaddings: margins(19px, 2px, 17px, 2px); -premiumGiftRowMargins: margins(14px, 0px, 15px, 0px); -premiumGiftRowSubtitleTop: 7px; -premiumGiftRowTextLeft: 53px; -premiumGiftRowBadgeHeight: 18px; -premiumGiftRowBadgeRadius: 4px; -premiumGiftRowBadgeMargins: margins(5px, 1px, 5px, 0px); +premiumGiftOption: PremiumOption { + rowPadding: margins(19px, 2px, 17px, 2px); + rowMargins: margins(14px, 0px, 15px, 0px); + rowHeight: 56px; + + borderWidth: 2px; + borderRadius: 9px; + + subtitleTop: 7px; + textLeft: 53px; + + badgeHeight: 18px; + badgeRadius: 4px; + badgeMargins: margins(5px, 1px, 5px, 0px); +} premiumGiftUserpicPadding: margins(10px, 27px, 18px, 13px); premiumGiftTitlePadding: margins(18px, 0px, 18px, 0px); diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index dbe851769..a57fc182d 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -938,7 +938,9 @@ void ShowListBox( void AddGiftOptions( not_null parent, std::shared_ptr group, - std::vector gifts) { + std::vector gifts, + const style::PremiumOption &st, + bool topBadges) { struct Edges { Ui::RpWidget *top = nullptr; @@ -956,8 +958,8 @@ void AddGiftOptions( const auto addRow = [&](const Data::SubscriptionOption &info, int index) { const auto row = parent->add( object_ptr(parent), - st::premiumGiftRowPaddings); - row->resize(row->width(), st::premiumGiftRowHeight); + st.rowPadding); + row->resize(row->width(), st.rowHeight); { if (!index) { edges->top = row; @@ -981,7 +983,7 @@ void AddGiftOptions( - margins.top() - margins.bottom(); radio->moveToLeft( - st::premiumGiftRowMargins.left(), + st.rowMargins.left(), (s.height() - radioHeight) / 2); }, radio->lifetime()); @@ -992,30 +994,45 @@ void AddGiftOptions( p.fillRect(r, Qt::transparent); - const auto left = st::premiumGiftRowTextLeft; - const auto borderWidth = st::premiumGiftRowBorderWidth; + const auto left = st.textLeft; const auto halfHeight = row->height() / 2; const auto titleFont = st::semiboldFont; p.setFont(titleFont); p.setPen(st::boxTextFg); - p.drawText( - left, - st::premiumGiftRowSubtitleTop + titleFont->ascent, - info.duration); + if (info.costPerMonth.isEmpty() && info.discount.isEmpty()) { + const auto r = row->rect().translated( + -row->rect().left() + left, + 0); + p.drawText(r, info.duration, style::al_left); + } else { + p.drawText( + left, + st.subtitleTop + titleFont->ascent, + info.duration); + } const auto discountFont = st::windowFiltersButton.badgeStyle.font; const auto discountWidth = discountFont->width(info.discount); const auto &discountMargins = discountWidth - ? st::premiumGiftRowBadgeMargins + ? st.badgeMargins : style::margins(); - const auto discountRect = QRect( + + const auto bottomLeftRect = QRect( left, halfHeight + discountMargins.top(), discountWidth + discountMargins.left() + discountMargins.right(), - st::premiumGiftRowBadgeHeight); + st.badgeHeight); + const auto discountRect = topBadges + ? bottomLeftRect.translated( + titleFont->width(info.duration) + st.badgeShift.x(), + -bottomLeftRect.top() + + st.badgeShift.y() + + st.subtitleTop + + (titleFont->height - bottomLeftRect.height()) / 2) + : bottomLeftRect; { const auto from = edges->top->y(); const auto to = edges->bottom->y() + edges->bottom->height(); @@ -1023,17 +1040,17 @@ void AddGiftOptions( p.setPen(Qt::NoPen); p.setBrush(partialGradient.compute(row->y(), row->height())); - const auto round = st::premiumGiftRowBadgeRadius; + const auto round = st.badgeRadius; p.drawRoundedRect(discountRect, round, round); } - if (animation->nowIndex == index) { + if (st.borderWidth && (animation->nowIndex == index)) { const auto progress = animation->animation.value(1.); const auto w = row->width(); auto gradient = QLinearGradient(w - w * progress, 0, w * 2, 0); gradient.setSpread(QGradient::Spread::RepeatSpread); gradient.setStops(stops); - const auto pen = QPen(QBrush(gradient), borderWidth); + const auto pen = QPen(QBrush(gradient), st.borderWidth); p.setPen(pen); p.setBrush(Qt::NoBrush); const auto borderRect = row->rect() @@ -1042,7 +1059,7 @@ void AddGiftOptions( pen.width() / 2, pen.width() / 2, pen.width() / 2); - const auto round = st::premiumGiftRowBorderRadius; + const auto round = st.borderRadius; p.drawRoundedRect(borderRect, round, round); } @@ -1051,15 +1068,17 @@ void AddGiftOptions( p.drawText(discountRect, info.discount, style::al_center); const auto perRect = QMargins(0, 0, row->width(), 0) - + discountRect.translated( - discountRect.width() + discountMargins.left(), + + bottomLeftRect.translated( + topBadges + ? 0 + : bottomLeftRect.width() + discountMargins.left(), 0); p.setPen(st::windowSubTextFg); p.setFont(st::shareBoxListItem.nameStyle.font); p.drawText(perRect, info.costPerMonth, style::al_left); const auto totalRect = row->rect() - - QMargins(0, 0, st::premiumGiftRowMargins.right(), 0); + - QMargins(0, 0, st.rowMargins.right(), 0); p.setFont(st::normalFont); p.drawText(totalRect, info.costTotal, style::al_right); }, row->lifetime()); diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h index 12988c045..158c0d12f 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.h +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -22,6 +22,7 @@ struct SubscriptionOption; namespace style { struct RoundImageCheckbox; +struct PremiumOption; struct TextStyle; } // namespace style @@ -90,7 +91,9 @@ void ShowListBox( void AddGiftOptions( not_null parent, std::shared_ptr group, - std::vector gifts); + std::vector gifts, + const style::PremiumOption &st, + bool topBadges = false); } // namespace Premium } // namespace Ui