diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a9daf106e2..e698cbb9be 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -240,6 +240,8 @@ PRIVATE boxes/edit_color_box.h boxes/edit_privacy_box.cpp boxes/edit_privacy_box.h + boxes/gift_premium_box.cpp + boxes/gift_premium_box.h boxes/language_box.cpp boxes/language_box.h boxes/local_storage_box.cpp diff --git a/Telegram/Resources/icons/menu/gift_premium.png b/Telegram/Resources/icons/menu/gift_premium.png new file mode 100644 index 0000000000..cb18442df9 Binary files /dev/null and b/Telegram/Resources/icons/menu/gift_premium.png differ diff --git a/Telegram/Resources/icons/menu/gift_premium@2x.png b/Telegram/Resources/icons/menu/gift_premium@2x.png new file mode 100644 index 0000000000..e7472fdf19 Binary files /dev/null and b/Telegram/Resources/icons/menu/gift_premium@2x.png differ diff --git a/Telegram/Resources/icons/menu/gift_premium@3x.png b/Telegram/Resources/icons/menu/gift_premium@3x.png new file mode 100644 index 0000000000..61be0204d9 Binary files /dev/null and b/Telegram/Resources/icons/menu/gift_premium@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ac31a33a9f..522ede9e66 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -167,8 +167,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channels_limit_title" = "Too Many Communities"; "lng_channels_limit1#one" = "You are a member of **{count}** groups and channels."; "lng_channels_limit1#other" = "You are a member of **{count}** groups and channels."; -"lng_channels_limit2#one" = "Please leave some before joining a new one - or upgrade to **Telegram Premium** to doulbe the limit to **{count}** groups and channels."; -"lng_channels_limit2#other" = "Please leave some before joining a new one - or upgrade to **Telegram Premium** to doulbe the limit to **{count}** groups and channels."; +"lng_channels_limit2#one" = "Please leave some before joining a new one - or upgrade to **Telegram Premium** to double the limit to **{count}** groups and channels."; +"lng_channels_limit2#other" = "Please leave some before joining a new one - or upgrade to **Telegram Premium** to double the limit to **{count}** groups and channels."; "lng_channels_limit2_final" = "Please leave some before joining a new one."; "lng_channels_leave_title" = "Least active communities"; "lng_channels_leave_status" = "{type}, inactive {time}"; @@ -1140,6 +1140,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_unblock_user" = "Unblock user"; "lng_profile_export_chat" = "Export history"; "lng_profile_export_channel" = "Export history"; +"lng_profile_gift_premium" = "Gift Premium"; "lng_media_selected_photo#one" = "{count} Photo"; "lng_media_selected_photo#other" = "{count} Photos"; "lng_media_selected_gif#one" = "{count} GIF"; @@ -1446,7 +1447,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_you_theme_disabled" = "You disabled chat theme"; "lng_action_theme_disabled" = "{from} disabled chat theme"; "lng_action_proximity_distance_m#one" = "{count} meter"; -"lng_action_proximity_distance_m#other" = "{count} metres"; +"lng_action_proximity_distance_m#other" = "{count} meters"; "lng_action_proximity_distance_km#one" = "{count} km"; "lng_action_proximity_distance_km#other" = "{count} km"; "lng_action_webview_data_done" = "You have just successfully transferred data from the «{text}» button to the bot."; @@ -1678,7 +1679,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_subtitle_double_limits" = "Doubled Limits"; "lng_premium_summary_about_double_limits" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more."; "lng_premium_summary_subtitle_more_upload" = "4Gb Upload Size"; -"lng_premium_summary_about_more_upload" = "Increased upload size from 2Gb to 4Gb to per document, unlimited storage overall."; +"lng_premium_summary_about_more_upload" = "Increased upload size from 2Gb to 4Gb per document, unlimited storage overall."; "lng_premium_summary_subtitle_faster_download" = "Faster Download Speed"; "lng_premium_summary_about_faster_download" = "No more limits on the speed with which media and documents are downloaded."; "lng_premium_summary_subtitle_voice_to_text" = "Voice-to-Text Conversion"; @@ -1742,6 +1743,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_double_limits_about_accounts#other" = "Connect {count} accounts with different mobile numbers"; // +"lng_premium_gift_title" = "Gift Telegram Premium"; +"lng_premium_gift_about" = "Let **{user}** enjoy exclusive features of Telegram with **Telegram Premium**."; +"lng_premium_gift_button" = "Gift Subscription for {cost}"; +"lng_premium_gift_per" = "{cost} / month"; +"lng_premium_gift_terms" = "You can review the list of features and terms of use for Telegram Premium {link}."; +"lng_premium_gift_terms_link" = "here"; + "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts."; "lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts."; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp new file mode 100644 index 0000000000..f8ff6ec7c7 --- /dev/null +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -0,0 +1,298 @@ +/* +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 "boxes/gift_premium_box.h" + +#include "api/api_premium.h" +#include "apiwrap.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" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_premium.h" +#include "ui/basic_click_handlers.h" // UrlClickHandler::Open. +#include "ui/effects/premium_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/special_buttons.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/wrap/padding_wrap.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_info.h" +#include "styles/style_premium.h" + +namespace { + +constexpr auto kDiscountDivider = 5.; + +struct GiftOption final { + QString url; + Ui::Premium::GiftInfo info; +}; +using GiftOptions = std::vector; + +GiftOptions GiftOptionFromTL( + const MTPDuserFull &data, + int monthlyAmount, + QString monthlyCurrency) { + auto result = GiftOptions(); + const auto gifts = data.vpremium_gifts(); + if (!gifts) { + return result; + } + result.reserve(gifts->v.size()); + for (const auto &gift : gifts->v) { + const auto &option = gift.match([](const MTPDpremiumGiftOption &d + ) -> const MTPDpremiumGiftOption & { + return d; + }); + const auto botUrl = qs(option.vbot_url()); + const auto months = option.vmonths().v; + const auto amount = option.vamount().v; + const auto currency = qs(option.vcurrency()); + if (monthlyCurrency != currency) { + monthlyAmount = 500; // 5 USD. + } + const auto discount = [&] { + const auto percent = monthlyAmount * months / float64(amount) + - 1.; + return std::round(percent * 100. / kDiscountDivider) + * kDiscountDivider; + }(); + auto info = Ui::Premium::GiftInfo{ + .duration = Ui::FormatTTL(months * 86400 * 31), + .discount = QString("\u2212%1%").arg(discount), + .perMonth = tr::lng_premium_gift_per( + tr::now, + lt_cost, + Ui::FillAmountAndCurrency( + amount / float64(months), + currency)), + .total = Ui::FillAmountAndCurrency(amount, currency), + }; + result.push_back({ .url = botUrl, .info = std::move(info) }); + } + return result; +} + +void GiftBox( + not_null box, + not_null controller, + not_null user, + GiftOptions options) { + const auto boxWidth = st::boxWideWidth; + box->setWidth(boxWidth); + box->setNoContentMargin(true); + const auto buttonsParent = box->verticalLayout().get(); + + struct State { + rpl::event_stream buttonText; + }; + const auto state = box->lifetime().make_state(); + + const auto userpicPadding = st::premiumGiftUserpicPadding; + const auto top = box->addRow(object_ptr( + buttonsParent, + userpicPadding.top() + + userpicPadding.bottom() + + st::defaultUserpicButton.size.height())); + + const auto userpic = Ui::CreateChild( + top, + user, + Ui::UserpicButton::Role::Custom, + st::defaultUserpicButton); + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + top->widthValue( + ) | rpl::start_with_next([=](int width) { + userpic->moveToLeft( + (width - userpic->width()) / 2, + userpicPadding.top()); + }, userpic->lifetime()); + + const auto close = Ui::CreateChild( + buttonsParent, + st::infoTopBarClose); + close->setClickedCallback([=] { box->closeBox(); }); + + buttonsParent->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0, width); + }, close->lifetime()); + + // Header. + const auto &padding = st::premiumGiftAboutPadding; + const auto available = boxWidth - padding.left() - padding.right(); + const auto &stTitle = st::premiumPreviewAboutTitle; + auto titleLabel = object_ptr( + box, + tr::lng_premium_gift_title(), + stTitle); + titleLabel->resizeToWidth(available); + box->addRow( + object_ptr>( + box, + std::move(titleLabel)), + st::premiumGiftTitlePadding); + + auto textLabel = object_ptr( + box, + tr::lng_premium_gift_about( + lt_user, + user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::Name + ) | rpl::map([=] { return TextWithEntities{ user->firstName }; }), + Ui::Text::RichLangValue), + st::premiumPreviewAbout); + textLabel->setTextColorOverride(stTitle.textFg->c); + textLabel->resizeToWidth(available); + box->addRow( + object_ptr>(box, std::move(textLabel)), + padding); + + // List. + const auto group = std::make_shared(); + group->setChangedCallback([=](int value) { + Expects(value < options.size() && value >= 0); + auto text = tr::lng_premium_gift_button( + tr::now, + lt_cost, + options[value].info.total); + state->buttonText.fire(std::move(text)); + }); + Ui::Premium::AddGiftOptions( + buttonsParent, + group, + ranges::views::all( + options + ) | ranges::views::transform([](const GiftOption &option) { + return option.info; + }) | ranges::to_vector); + + // Footer. + auto terms = object_ptr( + box, + tr::lng_premium_gift_terms( + lt_link, + tr::lng_premium_gift_terms_link( + ) | rpl::map([=](const QString &t) { + return Ui::Text::Link(t, 1); + }), + Ui::Text::WithEntities), + st::premiumGiftTerms); + terms->setLink(1, std::make_shared([=] { + box->closeBox(); + Settings::ShowPremium(&user->session(), QString()); + })); + terms->resizeToWidth(available); + box->addRow( + object_ptr>(box, std::move(terms)), + st::premiumGiftTermsPadding); + + // Button. + const auto &stButton = st::premiumGiftBox; + box->setStyle(stButton); + auto raw = Settings::CreateSubscribeButton({ + controller, + box, + [] { return QString("gift"); }, + state->buttonText.events(), + Ui::Premium::GiftGradientStops(), + }); + 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].url); + if (local.isEmpty()) { + return; + } + UrlClickHandler::Open( + local, + QVariant::fromValue(ClickHandlerContext{ + .botStartAutoSubmit = true, + .sessionWindow = base::make_weak(controller.get()), + })); + }); + box->setShowFinishedCallback([raw = button.data()]{ + raw->startGlareAnimation(); + }); + box->addButton(std::move(button)); + + group->setValue(0); + + Data::PeerPremiumValue( + user + ) | rpl::skip(1) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); +} + +} // namespace + +GiftPremiumValidator::GiftPremiumValidator( + not_null controller) +: _controller(controller) +, _api(&_controller->session().mtp()) { +} + +void GiftPremiumValidator::cancel() { + _requestId = 0; +} + +void GiftPremiumValidator::showBox(not_null user) { + if (_requestId) { + return; + } + _controller->session().api().premium().reload(); + _requestId = _api.request(MTPusers_GetFullUser( + user->inputUser + )).done([=](const MTPusers_UserFull &result) { + if (!_requestId) { + // Canceled. + return; + } + _requestId = 0; +// _controller->api().processFullPeer(peer, result); + const auto &data = result.match([]( + const MTPDusers_userFull &d) -> const MTPDusers_userFull & { + return d; + }); + _controller->session().data().processUsers(data.vusers()); + _controller->session().data().processChats(data.vchats()); + + const auto &fullUser = data.vfull_user().match( + [](const MTPDuserFull &d) -> const MTPDuserFull & { + return d; + }); + auto options = GiftOptionFromTL( + fullUser, + _controller->session().api().premium().monthlyAmount(), + _controller->session().api().premium().monthlyCurrency()); + if (!options.empty()) { + _controller->show( + Box(GiftBox, _controller, user, std::move(options))); + } + }).fail([=] { + _requestId = 0; + }).send(); +} diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h new file mode 100644 index 0000000000..cc11b23886 --- /dev/null +++ b/Telegram/SourceFiles/boxes/gift_premium_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 + +#include "mtproto/sender.h" + +class UserData; + +namespace Window { +class SessionController; +} // namespace Window + +class GiftPremiumValidator final { +public: + GiftPremiumValidator(not_null controller); + + void showBox(not_null user); + void cancel(); + +private: + const not_null _controller; + MTP::Sender _api; + + mtpRequestId _requestId = 0; + +}; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 1e1a7c38d1..51bce614ab 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -253,6 +253,10 @@ bool UserData::canAddContact() const { return canShareThisContact() && !isContact(); } +bool UserData::canReceiveGifts() const { + return flags() & UserDataFlag::CanReceiveGifts; +} + bool UserData::canShareThisContactFast() const { return !_phone.isEmpty(); } @@ -309,14 +313,19 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { if (const auto pinned = update.vpinned_msg_id()) { SetTopPinnedMessageId(user, pinned->v); } + const auto canReceiveGifts = (update.vflags().v + & MTPDuserFull::Flag::f_premium_gifts) + && update.vpremium_gifts(); using Flag = UserDataFlag; const auto mask = Flag::Blocked | Flag::HasPhoneCalls | Flag::PhoneCallsPrivate + | Flag::CanReceiveGifts | Flag::CanPinMessages; user->setFlags((user->flags() & ~mask) | (update.is_phone_calls_private() ? Flag::PhoneCallsPrivate : Flag()) | (update.is_phone_calls_available() ? Flag::HasPhoneCalls : Flag()) + | (canReceiveGifts ? Flag::CanReceiveGifts : Flag()) | (update.is_can_pin_message() ? Flag::CanPinMessages : Flag()) | (update.is_blocked() ? Flag::Blocked : Flag())); user->setIsBlocked(update.is_blocked()); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 1528f4b087..cabd43681d 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -51,6 +51,7 @@ enum class UserDataFlag { DiscardMinPhoto = (1 << 12), Self = (1 << 13), Premium = (1 << 14), + CanReceiveGifts = (1 << 15), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -106,6 +107,8 @@ public: [[nodiscard]] bool canShareThisContact() const; [[nodiscard]] bool canAddContact() const; + [[nodiscard]] bool canReceiveGifts() const; + // In Data::Session::processUsers() we check only that. // When actually trying to share contact we perform // a full check by canShareThisContact() call. diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index fbaea38341..24ab0663e3 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ using "ui/basic.style"; using "ui/widgets/widgets.style"; +using "settings/settings.style"; // Preview. premiumPreviewBox: Box(defaultBox) { @@ -96,6 +97,7 @@ premiumAccountsNameTop: 13px; premiumAccountsPadding: margins(0px, 20px, 0px, 14px); premiumAccountsHeight: 105px; +// Gift. premiumGiftRowHeight: 56px; premiumGiftRowBorderWidth: 2px; premiumGiftRowBorderRadius: 9px; @@ -107,3 +109,18 @@ premiumGiftRowBadgeHeight: 18px; premiumGiftRowBadgeRadius: 4px; premiumGiftRowBadgeMargins: margins(5px, 1px, 5px, 0px); +premiumGiftUserpicPadding: margins(10px, 27px, 18px, 13px); +premiumGiftTitlePadding: margins(18px, 0px, 18px, 0px); +premiumGiftAboutPadding: margins(18px, 5px, 18px, 23px); +premiumGiftTermsPadding: margins(18px, 27px, 18px, 0px); + +premiumGiftTerms: FlatLabel(settingLocalPasscodeDescription) { + style: TextStyle(defaultTextStyle) { + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px underline); + } +} +premiumGiftBox: Box(premiumPreviewBox) { + buttonPadding: margins(12px, 12px, 12px, 12px); +} diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 08e360f827..10a503c139 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -87,6 +87,7 @@ menuIconFile: icon {{ "menu/file", menuIconColor }}; menuIconPhoto: icon {{ "menu/image", menuIconColor }}; menuIconAddToFolder: icon {{ "menu/add_to_folder", menuIconColor }}; menuIconLeave: icon {{ "menu/leave", menuIconColor }}; +menuIconGiftPremium: icon {{ "menu/gift_premium", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 66908d7247..d9210d99a5 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -202,6 +202,7 @@ private: void addNewMembers(); void addDeleteContact(); void addTTLSubmenu(bool addSeparator); + void addGiftPremium(); not_null _controller; Dialogs::EntryState _request; @@ -806,6 +807,24 @@ void Filler::addTTLSubmenu(bool addSeparator) { } } +void Filler::addGiftPremium() { + const auto user = _peer->asUser(); + if (!user + || user->isInaccessible() + || user->isSelf() + || user->isBot() + || user->isNotificationsUser() + || !user->canReceiveGifts() + || user->isRepliesChat()) { + return; + } + + const auto navigation = _controller; + _addAction(tr::lng_profile_gift_premium(tr::now), [=] { + navigation->showGiftPremiumBox(user); + }, &st::menuIconGiftPremium); +} + void Filler::fill() { if (_folder) { fillArchiveActions(); @@ -862,6 +881,7 @@ void Filler::fillProfileActions() { addNewContact(); addShareContact(); addEditContact(); + addGiftPremium(); addBotToGroup(); addNewMembers(); addManageChat(); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 8b6b2512b1..40a5961ff6 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -617,7 +617,8 @@ SessionController::SessionController( , _activeChatsFilter(session->data().chatsFilters().defaultId()) , _defaultChatTheme(std::make_shared()) , _chatStyle(std::make_unique()) -, _cachedReactionIconFactory(std::make_unique()) { +, _cachedReactionIconFactory(std::make_unique()) +, _giftPremiumValidator(GiftPremiumValidator(this)) { init(); _chatStyleTheme = _defaultChatTheme; @@ -748,6 +749,14 @@ void SessionController::showEditPeerBox(PeerData *peer) { session().api().requestFullPeer(peer); } +void SessionController::showGiftPremiumBox(UserData *user) { + if (user) { + _giftPremiumValidator.showBox(user); + } else { + _giftPremiumValidator.cancel(); + } +} + void SessionController::init() { if (session().supportMode()) { initSupportMode(); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index f6a6733ea8..bd696e229e 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/observer.h" #include "base/weak_ptr.h" #include "base/timer.h" +#include "boxes/gift_premium_box.h" // GiftPremiumValidator. #include "data/data_chat_participant_status.h" #include "dialogs/dialogs_key.h" #include "ui/layers/layer_widget.h" @@ -354,6 +355,7 @@ public: Dialogs::RowDescriptor from = {}) const; void showEditPeerBox(PeerData *peer); + void showGiftPremiumBox(UserData *user); void enableGifPauseReason(GifPauseReason reason); void disableGifPauseReason(GifPauseReason reason); @@ -599,6 +601,8 @@ private: using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; std::unique_ptr _cachedReactionIconFactory; + GiftPremiumValidator _giftPremiumValidator; + QString _premiumRef; rpl::lifetime _lifetime;