From 1cb4413869c3065db6eeb2ff64df323e3e42236d Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 17 Jul 2025 11:33:06 +0400 Subject: [PATCH] Lang pack rating / premium gift features. --- Telegram/Resources/langs/lang.strings | 12 ++ Telegram/SourceFiles/boxes/star_gift_box.cpp | 20 +- .../peer_gifts/info_peer_gifts_common.cpp | 7 +- .../SourceFiles/settings/settings_premium.cpp | 193 ++++++++++++++---- .../SourceFiles/settings/settings_premium.h | 7 + .../SourceFiles/ui/controls/stars_rating.cpp | 36 ++-- 6 files changed, 217 insertions(+), 58 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7ba789dab5..06857aae47 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1876,6 +1876,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_star_ref_revoked_title" = "Link removed"; "lng_star_ref_revoked_text" = "It will no longer work."; +"lng_stars_rating_tooltip" = "Profile level reflects the user's payment reliability."; +"lng_stars_rating_learn_more" = "Learn More"; +"lng_stars_rating_about_url" = "https://telegram.org/"; + "lng_manage_discussion_group" = "Discussion"; "lng_manage_discussion_group_add" = "Add a group"; "lng_manage_linked_channel" = "Linked channel"; @@ -2589,6 +2593,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_unlock_emoji" = "Unlock Animated Emoji"; "lng_premium_unlock_status" = "Unlock Emoji Status"; +"lng_premium_subscribe_months_24" = "2-Year"; "lng_premium_subscribe_months_12" = "Annual"; "lng_premium_subscribe_months_6" = "Semiannual"; "lng_premium_subscribe_months_1" = "Monthly"; @@ -3508,6 +3513,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_ton_amount#one" = "{count} TON"; "lng_gift_ton_amount#other" = "{count} TON"; +"lng_gift_premium_title" = "Premium Gift"; +"lng_gift_premium_text#one" = "Subscribe to **Telegram Premium** to send up to **{count}** of these gifts and unlock access to multiple additional features."; +"lng_gift_premium_text#other" = "Subscribe to **Telegram Premium** to send up to **{count}** of these gifts and unlock access to multiple additional features."; "lng_gift_premium_or_stars" = "Gift Premium or Stars"; "lng_gift_premium_subtitle" = "Gift Premium"; "lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}"; @@ -3559,6 +3567,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_sent_resale_done_self" = "{gift} is now yours."; "lng_gift_sent_about#one" = "You spent **{count}** Star from your balance."; "lng_gift_sent_about#other" = "You spent **{count}** Stars from your balance."; +"lng_gift_sent_finished#one" = "You've already sent **{count}** of these gifts, and it's the limit."; +"lng_gift_sent_finished#other" = "You've already sent **{count}** of these gifts, and it's the limit."; +"lng_gift_sent_remains#one" = "You can send **{count}** more."; +"lng_gift_sent_remains#other" = "You can send **{count}** more."; "lng_gift_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; "lng_gift_collectible_tag" = "gift"; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index c63319b912..b3cf7bf156 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -777,6 +777,19 @@ void ShowSentToast( tr::now, Text::RichLangValue); }, [&](const GiftTypeStars &gift) { + if (gift.info.perUserTotal && gift.info.perUserRemains < 2) { + return tr::lng_gift_sent_finished( + tr::now, + lt_count, + gift.info.perUserTotal, + Text::RichLangValue); + } else if (gift.info.perUserTotal) { + return tr::lng_gift_sent_remains( + tr::now, + lt_count, + gift.info.perUserRemains - 1, + Text::RichLangValue); + } const auto amount = gift.info.stars + (details.upgraded ? gift.info.starsToUpgrade : 0); return tr::lng_gift_sent_about( @@ -2749,7 +2762,12 @@ void SendGiftBox( button->setClickedCallback([=] { const auto star = std::get_if(&descriptor); const auto unique = star ? star->info.unique : nullptr; - if (unique && star->mine && !peer->isSelf()) { + if (star + && star->info.requirePremium + && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium(window, star->info); + return; + } else if (unique && star->mine && !peer->isSelf()) { if (ShowTransferGiftLater(window->uiShow(), unique)) { return; } diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index da39de9903..5ae749173b 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -421,6 +421,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { const auto onsale = (unique && unique->starsForResale && _small); const auto requirePremium = v::is(_descriptor) && !v::get(_descriptor).userpic + && !v::get(_descriptor).info.unique && v::get(_descriptor).info.requirePremium; const auto hidden = v::is(_descriptor) && v::get(_descriptor).hidden; @@ -539,7 +540,9 @@ void GiftButton::paintEvent(QPaintEvent *e) { ? tr::lng_gift_stars_resale(tr::now) : soldOut ? tr::lng_gift_stars_sold_out(tr::now) - : (!data.userpic && data.info.requirePremium) + : (!data.userpic + && !data.info.unique + && data.info.requirePremium) ? tr::lng_gift_stars_premium(tr::now) : (!data.userpic && !data.info.unique) ? tr::lng_gift_stars_limited(tr::now) @@ -559,7 +562,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { ? st::boxTextFgGood->c : soldOut ? st::attentionButtonFg->c - : data.info.requirePremium + : (!data.userpic && data.info.requirePremium) ? st::creditsFg->c : st::windowActiveTextFg->c), .bg2 = (onsale diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 2b04f6db57..5ca2783923 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -91,6 +91,8 @@ using SectionCustomTopBarData = Info::Settings::SectionCustomTopBarData; 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); + } else if (option.duration == tr::lng_years(tr::now, lt_count, 2)) { + option.duration = tr::lng_premium_subscribe_months_24(tr::now); } } return result; @@ -164,6 +166,45 @@ struct Data { } } // namespace EmojiStatus + +namespace PremiumGift { + +struct Data { + DocumentId documentId = 0; + int perUserTotal = 0; + + explicit operator bool() const { + return documentId != 0; + } +}; + +[[nodiscard]] QString Serialize(const Data &gift) { + return u"premiumgift_:%1,peruser_:%2"_q + .arg(gift.documentId) + .arg(gift.perUserTotal); +} + +[[nodiscard]] Data Parse(QStringView data) { + if (data.startsWith(u"premiumgift_:"_q)) { + const auto components = data.split(','); + if (components.size() != 2) { + return {}; + } + const auto first = components[0].split(':'); + const auto second = components[1].split(':'); + if (first.size() != 2 || second.size() != 2) { + return {}; + } + return { + .documentId = DocumentId(first[1].toULongLong()), + .perUserTotal = second[1].toInt(), + }; + } + return {}; +} + +} // namespace PremiumGift + } // namespace Ref struct Entry { @@ -374,7 +415,6 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_business(), tr::lng_premium_summary_about_business(), PremiumFeature::Business, - true, }, }, { @@ -393,6 +433,7 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_todo_lists(), tr::lng_premium_summary_about_todo_lists(), PremiumFeature::TodoLists, + true, }, }, }; @@ -542,13 +583,30 @@ void EmojiStatusTopBar::paint(QPainter &p) { } } -class TopBarUser final : public Ui::Premium::TopBarAbstract { +enum class TopBarWithStickerType { + EmojiStatus, + PremiumGift, +}; + +struct TopBarWithStickerArgs { + rpl::producer stickerValue; + rpl::producer nameValue; + rpl::producer aboutValue; + TopBarWithStickerType type = TopBarWithStickerType::EmojiStatus; +}; + +class TopBarWithSticker final : public Ui::Premium::TopBarAbstract { public: - TopBarUser( + TopBarWithSticker( not_null parent, not_null controller, not_null peer, rpl::producer<> showFinished); + TopBarWithSticker( + not_null parent, + not_null controller, + TopBarWithStickerArgs args, + rpl::producer<> showFinished); void setPaused(bool paused) override; void setTextPosition(int x, int y) override; @@ -562,10 +620,13 @@ protected: private: void updateTitle( DocumentData *document, - TextWithEntities name, + const TextWithEntities &name, not_null controller); - void updateAbout(DocumentData *document) const; + void updateAbout( + DocumentData *document, + const TextWithEntities &about) const; + TopBarWithStickerType _type = TopBarWithStickerType::EmojiStatus; object_ptr _content; object_ptr _title; object_ptr _about; @@ -586,12 +647,35 @@ private: }; -TopBarUser::TopBarUser( +TopBarWithSticker::TopBarWithSticker( not_null parent, not_null controller, not_null peer, rpl::producer<> showFinished) +: TopBarWithSticker(parent, controller, { + .stickerValue = Info::Profile::EmojiStatusIdValue( + peer + ) | rpl::map([=](EmojiStatusId id) -> DocumentData* { + const auto documentId = id.collectible + ? id.collectible->documentId + : id.documentId; + const auto document = documentId + ? controller->session().data().document(documentId).get() + : nullptr; + return (document && document->sticker()) ? document : nullptr; + }), + .nameValue = Info::Profile::NameValue(peer), + .type = TopBarWithStickerType::EmojiStatus, +}, std::move(showFinished)) { +} + +TopBarWithSticker:: TopBarWithSticker( + not_null parent, + not_null controller, + TopBarWithStickerArgs args, + rpl::producer<> showFinished) : TopBarAbstract(parent, st::userPremiumCover) +, _type(args.type) , _content(this) , _title(_content, st::settingsPremiumUserTitle) , _about(_content, st::userPremiumCover.about) @@ -604,6 +688,10 @@ TopBarUser::TopBarUser( }) { _starRect = TopBarAbstract::starRect(1., 1.); + if (_type == TopBarWithStickerType::PremiumGift) { + _ministars.setColorOverride(Ui::Premium::CreditsIconGradientStops()); + } + rpl::single() | rpl::then( style::PaletteChanged() ) | rpl::start_with_next([=] { @@ -611,24 +699,14 @@ TopBarUser::TopBarUser( update(); }, lifetime()); - auto documentValue = Info::Profile::EmojiStatusIdValue( - peer - ) | rpl::map([=](EmojiStatusId id) -> DocumentData* { - const auto documentId = id.collectible - ? id.collectible->documentId - : id.documentId; - const auto document = documentId - ? controller->session().data().document(documentId).get() - : nullptr; - return (document && document->sticker()) ? document : nullptr; - }); - rpl::combine( - std::move(documentValue), - Info::Profile::NameValue(peer) + std::move(args.stickerValue), + std::move(args.nameValue), + std::move(args.aboutValue) ) | rpl::start_with_next([=]( DocumentData *document, - const QString &name) { + const QString &name, + const TextWithEntities &about) { if (document) { _emojiStatus = std::make_unique( document, @@ -641,7 +719,7 @@ TopBarUser::TopBarUser( } updateTitle(document, { name }, controller); - updateAbout(document); + updateAbout(document, about); auto event = QResizeEvent(size(), size()); resizeEvent(&event); @@ -746,14 +824,15 @@ TopBarUser::TopBarUser( p.drawImage(_starRect.topLeft(), _imageStar); } }, lifetime()); - } -void TopBarUser::updateTitle( +void TopBarWithSticker::updateTitle( DocumentData *document, - TextWithEntities name, + const TextWithEntities &name, not_null controller) { - if (!document) { + if (_type == TopBarWithStickerType::PremiumGift) { + return _title->setMarkedText(name); + } else if (!document) { return _title->setMarkedText( tr::lng_premium_summary_user_title( tr::now, @@ -816,36 +895,40 @@ void TopBarUser::updateTitle( _title->setLink(linkIndex, std::move(link)); } -void TopBarUser::updateAbout(DocumentData *document) const { - _about->setMarkedText((document - ? tr::lng_premium_emoji_status_about - : tr::lng_premium_summary_user_about)( - tr::now, - Ui::Text::RichLangValue)); +void TopBarWithSticker::updateAbout( + DocumentData *document, + const TextWithEntities &about) const { + _about->setMarkedText((_type == TopBarWithStickerType::PremiumGift) + ? about + : (document + ? tr::lng_premium_emoji_status_about + : tr::lng_premium_summary_user_about)( + tr::now, + Ui::Text::RichLangValue)); } -void TopBarUser::setPaused(bool paused) { +void TopBarWithSticker::setPaused(bool paused) { _ministars.setPaused(paused); if (_emojiStatus) { _emojiStatus->setPaused(paused); } } -void TopBarUser::setTextPosition(int x, int y) { +void TopBarWithSticker::setTextPosition(int x, int y) { _smallTop.position = { x, y }; } -rpl::producer TopBarUser::additionalHeight() const { +rpl::producer TopBarWithSticker::additionalHeight() const { return rpl::never(); } -void TopBarUser::paintEvent(QPaintEvent *e) { +void TopBarWithSticker::paintEvent(QPaintEvent *e) { auto p = QPainter(this); TopBarAbstract::paintEdges(p); } -void TopBarUser::resizeEvent(QResizeEvent *e) { +void TopBarWithSticker::resizeEvent(QResizeEvent *e) { _starRect = TopBarAbstract::starRect(1., 1.); _ministars.setCenter(_starRect.toRect()); @@ -1053,7 +1136,9 @@ base::weak_qptr Premium::createPinnedToTop( }(); const auto emojiStatusData = Ref::EmojiStatus::Parse(_ref); + const auto premiumGiftData = Ref::PremiumGift::Parse(_ref); const auto isEmojiStatus = (!!emojiStatusData); + const auto isPremiumGift = (!!premiumGiftData); auto peerWithPremium = [&]() -> PeerData* { if (isEmojiStatus) { @@ -1064,14 +1149,35 @@ base::weak_qptr Premium::createPinnedToTop( } return nullptr; }(); + auto premiumGift = [&]() -> DocumentData* { + if (isPremiumGift) { + auto &data = _controller->session().data(); + return data.document(premiumGiftData.documentId); + } + return nullptr; + }(); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { if (peerWithPremium) { - return Ui::CreateChild( + return Ui::CreateChild( parent.get(), _controller, peerWithPremium, _showFinished.events()); + } else if (premiumGift) { + return Ui::CreateChild( + parent.get(), + _controller, + TopBarWithStickerArgs{ + .stickerValue = rpl::single(premiumGift), + .nameValue = tr::lng_gift_premium_title(), + .aboutValue = tr::lng_gift_premium_text( + lt_count, + rpl::single(premiumGiftData.perUserTotal * 1.), + Ui::Text::RichLangValue), + .type = TopBarWithStickerType::PremiumGift, + }, + _showFinished.events()); } const auto weak = base::make_weak(_controller); const auto clickContextOther = [=] { @@ -1102,7 +1208,7 @@ base::weak_qptr Premium::createPinnedToTop( }, content->lifetime()); const auto calculateMaximumHeight = [=] { - return isEmojiStatus + return (isEmojiStatus || isPremiumGift) ? st::settingsPremiumUserHeight + TopTransitionSkip() : st::settingsPremiumTopHeight; }; @@ -1352,6 +1458,15 @@ void ShowEmojiStatusPremium( } } +void ShowPremiumGiftPremium( + not_null controller, + const Data::StarGift &gift) { + ShowPremium(controller, Ref::PremiumGift::Serialize({ + .documentId = gift.document->id, + .perUserTotal = gift.perUserTotal, + })); +} + void StartPremiumPayment( not_null controller, const QString &ref) { diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 2effb1e1a4..19333ddee2 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -20,6 +20,10 @@ namespace ChatHelpers { class Show; } // namespace ChatHelpers +namespace Data { +struct StarGift; +} // namespace Data + namespace Ui { class RpWidget; class RoundButton; @@ -52,6 +56,9 @@ void ShowGiftPremium( void ShowEmojiStatusPremium( not_null controller, not_null peer); +void ShowPremiumGiftPremium( + not_null controller, + const Data::StarGift &gift); void StartPremiumPayment( not_null controller, diff --git a/Telegram/SourceFiles/ui/controls/stars_rating.cpp b/Telegram/SourceFiles/ui/controls/stars_rating.cpp index 2fa02a2269..fb047ded9e 100644 --- a/Telegram/SourceFiles/ui/controls/stars_rating.cpp +++ b/Telegram/SourceFiles/ui/controls/stars_rating.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/stars_rating.h" #include "lang/lang_keys.h" +#include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -42,7 +43,8 @@ StarsRating::StarsRating( StarsRating::~StarsRating() = default; void StarsRating::init() { - _expanded.value() | rpl::start_with_next([=](bool expanded) { + _widget->setPointerCursor(true); + _expanded.changes() | rpl::start_with_next([=](bool expanded) { _widget->setPointerCursor(!expanded); const auto from = expanded ? 0. : 1.; const auto till = expanded ? 1. : 0.; @@ -168,15 +170,15 @@ void StarsRating::toggleTooltips(bool shown) { stars->toggleAnimated(true); _aboutSt = std::make_unique(st::defaultMultilineToast); - const auto learn = u"Learn More"_q; + const auto learn = tr::lng_stars_rating_learn_more(tr::now); _aboutSt->padding.setRight( (st::infoStarsRatingLearn.style.font->width(learn) - st::infoStarsRatingLearn.width)); _about = Ui::Toast::Show(parent, { - .text = TextWithEntities{ - u"Profile level reflects the user's payment reliability."_q, - }, + .text = tr::lng_stars_rating_tooltip( + tr::now, + Ui::Text::WithEntities), .st = _aboutSt.get(), .attach = RectPart::Top, .dark = true, @@ -211,7 +213,7 @@ void StarsRating::toggleTooltips(bool shown) { outer.width()); }, widget->lifetime()); button->setClickedCallback([=] { - UrlClickHandler::Open(u"https://telegram.org/"_q); + UrlClickHandler::Open(tr::lng_stars_rating_about_url(tr::now)); }); } @@ -283,16 +285,18 @@ void StarsRating::paint(QPainter &p) { ? ((value.currentStars - value.levelStars) / float64(value.nextLevelStars - value.levelStars)) : 1.; - const auto expandedFilled = _st.padding.left() - + _expandedText.maxWidth() - + _st.padding.right() - + expandedRatio * (middle.width() - - _st.padding.left() - - _expandedText.maxWidth() - - _st.padding.right() - - _st.padding.left() - - _nextText.maxWidth() - - _st.padding.right()); + const auto expandedFilled = (expandedRatio < 1.) + ? (_st.padding.left() + + _expandedText.maxWidth() + + _st.padding.right() + + expandedRatio * (middle.width() + - _st.padding.left() + - _expandedText.maxWidth() + - _st.padding.right() + - _st.padding.left() + - _nextText.maxWidth() + - _st.padding.right())) + : middle.width(); const auto collapsedFilled = _collapsedWidthValue.current() - _st.margin.right() - 2 * _st.border;