diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f6adf0c140..e2078f7e62 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -816,20 +816,6 @@ PRIVATE history/history_view_highlight_manager.h history/history_widget.cpp history/history_widget.h - info/info_content_widget.cpp - info/info_content_widget.h - info/info_controller.cpp - info/info_controller.h - info/info_layer_widget.cpp - info/info_layer_widget.h - info/info_memento.cpp - info/info_memento.h - info/info_section_widget.cpp - info/info_section_widget.h - info/info_top_bar.cpp - info/info_top_bar.h - info/info_wrap_widget.cpp - info/info_wrap_widget.h info/boosts/info_boosts_inner_widget.cpp info/boosts/info_boosts_inner_widget.h info/boosts/info_boosts_widget.cpp @@ -916,6 +902,20 @@ PRIVATE info/userpic/info_userpic_emoji_builder_preview.h info/userpic/info_userpic_emoji_builder_widget.cpp info/userpic/info_userpic_emoji_builder_widget.h + info/info_content_widget.cpp + info/info_content_widget.h + info/info_controller.cpp + info/info_controller.h + info/info_layer_widget.cpp + info/info_layer_widget.h + info/info_memento.cpp + info/info_memento.h + info/info_section_widget.cpp + info/info_section_widget.h + info/info_top_bar.cpp + info/info_top_bar.h + info/info_wrap_widget.cpp + info/info_wrap_widget.h inline_bots/bot_attach_web_view.cpp inline_bots/bot_attach_web_view.h inline_bots/inline_bot_layout_internal.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a2ac91c507..5a7c0a7a13 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1640,6 +1640,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_story_mention_button" = "View Story"; "lng_action_story_mention_me_unavailable" = "The story where you mentioned {user} is no longer available."; "lng_action_story_mention_unavailable" = "The story where {user} mentioned you is no longer available."; +"lng_action_giveaway_started" = "{from} just started a giveaway of Telegram Premium subscriptions to its followers."; "lng_premium_gift_duration_months#one" = "for {count} month"; "lng_premium_gift_duration_months#other" = "for {count} months"; @@ -2034,6 +2035,88 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_now_instead" = "You currently boost {channel}. Do you want to boost {other} instead?"; "lng_boost_now_replace" = "Replace"; +"lng_giveaway_new_title" = "Boosts via Gifts"; +"lng_giveaway_new_about" = "Get more boosts for your channel by gifting Premium to your subscribers."; +"lng_giveaway_create_option" = "Create Giveaway"; +"lng_giveaway_create_subtitle" = "winners are chosen randomly"; +"lng_giveaway_award_option" = "Award Specific Users"; +"lng_giveaway_award_subtitle" = "Select recipients >"; +"lng_giveaway_award_chosen#one" = "{count} recipient >"; +"lng_giveaway_award_chosen#other" = "{count} recipients >"; +"lng_giveaway_quantity_title" = "Quantity of prizes / boosts"; +"lng_giveaway_quantity#one" = "{count} Subscription / Boost"; +"lng_giveaway_quantity#other" = "{count} Subscriptions / Boosts"; +"lng_giveaway_quantity_about" = "Choose how many Premium subscriptions to give away and boosts to receive."; +"lng_giveaway_channels_title" = "Channels included in the giveaway"; +"lng_giveaway_channels_this#one" = "this channel will receive {count} boost"; +"lng_giveaway_channels_this#other" = "this channel will receive {count} boosts"; +"lng_giveaway_channels_add" = "Add Channel"; +"lng_giveaway_channels_about" = "Choose the channels the users need to join to take part in the giveaway."; +"lng_giveaway_users_title" = "Users eligible for the giveaway"; +"lng_giveaway_users_all" = "All subscribers"; +"lng_giveaway_users_new" = "Only new subscribers"; +"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to the newly joined subscribers."; +"lng_giveaway_start" = "Start Giveaway"; +"lng_giveaway_award" = "Gift Premium"; +"lng_giveaway_date_title" = "Date when giveaway ends"; +"lng_giveaway_date" = "Date and Time"; +"lng_giveaway_date_about#one" = "Choose when {count} subscriber of your channel will be randomly selected to receive Telegram Premium."; +"lng_giveaway_date_about#other" = "Choose when {count} subscribers of your channel will be randomly selected to receive Telegram Premium."; +"lng_giveaway_duration_title#one" = "Duration of Premium subscription"; +"lng_giveaway_duration_title#other" = "Duration of Premium subscriptions"; +"lng_giveaway_duration_price" = "{price} x {amount}"; +"lng_giveaway_duration_about" = "You can review the list of features and terms of use for Telegram Premium {link}."; +"lng_giveaway_duration_about_link" = "here"; +"lng_giveaway_date_select" = "Select Date and Time"; +"lng_giveaway_date_confirm" = "Confirm"; +"lng_giveaway_channels_select#one" = "Select up to {count} channel"; +"lng_giveaway_channels_select#other" = "Select up to {count} channels"; +"lng_giveaway_recipients_save" = "Save Recipients"; +"lng_giveaway_recipients_deselect" = "Deselect All"; + +"lng_prize_title" = "Congratulations!"; +"lng_prize_about" = "You won a prize in a giveaway organized by {channel}."; +"lng_prize_duration" = "Your prize is **Telegram Premium** subscription for {duration}."; +"lng_prize_open" = "Open Gift Link"; + +"lng_prizes_title#one" = "Giveaway Prize"; +"lng_prizes_title#other" = "Giveaway Prizes"; +"lng_prizes_about#one" = "{count} Telegram Premium Subscription for {duration}."; +"lng_prizes_about#other" = "{count} Telegram Premium Subscriptions for {duration}."; +"lng_prizes_participants" = "Participants"; +"lng_prizes_participants_all#one" = "All subscribers of the channel:"; +"lng_prizes_participants_all#other" = "All subscribers of the channels:"; +"lng_prizes_participants_new#one" = "All users who joined the channel below after this date:"; +"lng_prizes_participants_new#other" = "All users who joined the channels below after this date:"; +"lng_prizes_date" = "Winners Selection Date"; +"lng_prizes_how_works" = "How does it work?"; +"lng_prizes_how_text#one" = "This giveaway is sponsored by the admins of {channel}, who aquired **{count} Telegram Premium** subscription for {duration} for its followers."; +"lng_prizes_how_text#other" = "This giveaway is sponsored by the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions for {duration} for its followers."; +"lng_prizes_how_when_all_of_one#one" = "On {date}, Telegram will automatically select {count} random subscribers of {channel}."; +"lng_prizes_how_when_all_of_one#other" = "On {date}, Telegram will automatically select {count} random subscribers of {channel}."; +"lng_prizes_how_when_all_of_many#one" = "On {date}, Telegram will automatically select {count} random subscribers of {channel} or other listed channels."; +"lng_prizes_how_when_all_of_many#other" = "On {date}, Telegram will automatically select {count} random subscribers of {channel} or other listed channels."; +"lng_prizes_how_when_new_of_one#one" = "On {date}, Telegram will automatically select {count} random user that joined {channel} after {start_date}"; +"lng_prizes_how_when_new_of_one#other" = "On {date}, Telegram will automatically select {count} random users that joined {channel} after {start_date}"; +"lng_prizes_how_when_new_of_many#one" = "On {date}, Telegram will automatically select {count} random user that joined {channel} or other listed channels after {start_date}"; +"lng_prizes_how_when_new_of_many#other" = "On {date}, Telegram will automatically select {count} random users that joined {channel} or other listed channels after {start_date}"; + +"lng_gift_link_title" = "Gift Link"; +"lng_gift_link_about" = "This link allows you to activate\na **Telegram Premium** subscription."; +"lng_gift_link_label_from" = "From"; +"lng_gift_link_label_to" = "To"; +"lng_gift_link_label_gift" = "Gift"; +"lng_gift_link_gift_premium" = "Telegram Premium for {duration}"; +"lng_gift_link_label_reason" = "Reason"; +"lng_gift_link_reason_giveaway" = "Giveaway"; +"lng_gift_link_label_date" = "Date"; +"lng_gift_link_also_send" = "You can also {link} to a friend as a gift."; +"lng_gift_link_also_send_link" = "send this link"; +"lng_gift_link_use" = "Use Link"; +"lng_gift_link_used_title" = "Used Gift Link"; +"lng_gift_link_used_about" = "This link was used to activate\na **Telegram Premium** subscription."; +"lng_gift_link_used_footer" = "This link was used on {date}."; + "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/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 975d36a656..936715834b 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -17,6 +17,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" namespace Api { +namespace { + +[[nodiscard]] GiftCode Parse(const MTPDpayments_checkedGiftCode &data) { + return { + .from = peerFromMTP(data.vfrom_id()), + .to = data.vto_id() ? peerFromUser(*data.vto_id()) : PeerId(), + .date = data.vdate().v, + .used = false,// data.vused_date().value_or_empty(), + .months = data.vmonths().v, + }; +} + +} // namespace Premium::Premium(not_null api) : _session(&api->session()) @@ -183,6 +196,52 @@ void Premium::reloadCloudSet() { }).send(); } +void Premium::checkGiftCode( + const QString &slug, + Fn done) { + if (_giftCodeRequestId) { + if (_giftCodeSlug == slug) { + return; + } + _api.request(_giftCodeRequestId).cancel(); + } + _giftCodeSlug = slug; + _giftCodeRequestId = _api.request(MTPpayments_CheckGiftCode( + MTP_string(slug) + )).done([=](const MTPpayments_CheckedGiftCode &result) { + _giftCodeRequestId = 0; + + const auto &data = result.data(); + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + done(updateGiftCode(slug, Parse(data))); + }).fail([=](const MTP::Error &error) { + _giftCodeRequestId = 0; + + done(updateGiftCode(slug, {})); + }).send(); +} + +GiftCode Premium::updateGiftCode( + const QString &slug, + const GiftCode &code) { + auto &now = _giftCodes[slug]; + if (now != code) { + now = code; + _giftCodeUpdated.fire_copy(slug); + } + return code; +} + +rpl::producer Premium::giftCodeValue(const QString &slug) const { + return _giftCodeUpdated.events_starting_with_copy( + slug + ) | rpl::filter(rpl::mappers::_1 == slug) | rpl::map([=] { + const auto i = _giftCodes.find(slug); + return (i != end(_giftCodes)) ? i->second : GiftCode(); + }); +} + const Data::SubscriptionOptions &Premium::subscriptionOptions() const { return _subscriptionOptions; } diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 3582833607..6fe1c9d700 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -18,6 +18,22 @@ class Session; namespace Api { +struct GiftCode { + PeerId from = 0; + PeerId to = 0; + TimeId date = 0; + TimeId used = 0; // 0 if not used. + int months = 0; + + explicit operator bool() const { + return months != 0; + } + + friend inline bool operator==( + const GiftCode&, + const GiftCode&) = default; +}; + class Premium final { public: explicit Premium(not_null api); @@ -40,6 +56,13 @@ public: [[nodiscard]] int64 monthlyAmount() const; [[nodiscard]] QString monthlyCurrency() const; + void checkGiftCode( + const QString &slug, + Fn done); + GiftCode updateGiftCode(const QString &slug, const GiftCode &code); + [[nodiscard]] rpl::producer giftCodeValue( + const QString &slug) const; + [[nodiscard]] auto subscriptionOptions() const -> const Data::SubscriptionOptions &; @@ -71,6 +94,11 @@ private: int64 _monthlyAmount = 0; QString _monthlyCurrency; + mtpRequestId _giftCodeRequestId = 0; + QString _giftCodeSlug; + base::flat_map _giftCodes; + rpl::event_stream _giftCodeUpdated; + Data::SubscriptionOptions _subscriptionOptions; }; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 6720339c28..96be412ec7 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -8,8 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/gift_premium_box.h" #include "apiwrap.h" +#include "api/api_premium.h" #include "api/api_premium_option.h" +#include "base/unixtime.h" #include "base/weak_ptr.h" +#include "boxes/peers/prepare_short_info_box.h" #include "data/data_changes.h" #include "data/data_peer_values.h" // Data::PeerPremiumValue. #include "data/data_session.h" @@ -22,12 +25,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/userpic_button.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" +#include "ui/effects/premium_top_bar.h" #include "ui/layers/generic_box.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 "ui/wrap/table_layout.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" @@ -35,6 +40,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_info.h" #include "styles/style_premium.h" +#include + namespace { constexpr auto kDiscountDivider = 5.; @@ -225,6 +232,175 @@ void GiftBox( }, box->lifetime()); } +struct GiftCodeLink { + QString text; + QString link; +}; +[[nodiscard]] GiftCodeLink MakeGiftCodeLink( + not_null session, + const QString &slug) { + const auto path = u"giftcode/"_q + slug; + return { + session->createInternalLink(path), + session->createInternalLinkFull(path), + }; +} + +[[nodiscard]] object_ptr MakeLinkLabel( + not_null parent, + rpl::producer text, + rpl::producer link, + std::shared_ptr show) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + struct State { + State( + not_null parent, + rpl::producer value, + rpl::producer link) + : text(std::move(value)) + , link(std::move(link)) + , label(parent, text.value(), st::giveawayGiftCodeLink) + , bg(st::roundRadiusLarge, st::windowBgOver) { + } + + rpl::variable text; + rpl::variable link; + Ui::FlatLabel label; + Ui::RoundRect bg; + }; + + const auto state = raw->lifetime().make_state( + raw, + rpl::duplicate(text), + std::move(link)); + state->label.setSelectable(true); + + rpl::combine( + raw->widthValue(), + std::move(text) + ) | rpl::start_with_next([=](int outer, const auto&) { + const auto textWidth = state->label.textMaxWidth(); + const auto skipLeft = st::giveawayGiftCodeLink.margin.left(); + const auto skipRight = st::giveawayGiftCodeLinkCopyWidth; + const auto available = outer - skipRight - skipLeft; + const auto use = std::min(textWidth, available); + state->label.resizeToWidth(use); + const auto left = (outer >= 2 * skipRight + textWidth) + ? ((outer - textWidth) / 2) + : (outer - skipRight - use - skipLeft); + state->label.move(left, 0); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + state->bg.paint(p, raw->rect()); + const auto outer = raw->width(); + const auto width = st::giveawayGiftCodeLinkCopyWidth; + const auto &icon = st::giveawayGiftCodeLinkCopy; + const auto left = outer - width + (width - icon.width()) / 2; + const auto top = (raw->height() - icon.height()) / 2; + icon.paint(p, left, top, raw->width()); + }, raw->lifetime()); + + state->label.setAttribute(Qt::WA_TransparentForMouseEvents); + + raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight); + raw->setClickedCallback([=] { + QGuiApplication::clipboard()->setText(state->link.current()); + show->showToast(tr::lng_username_copied(tr::now)); + }); + + return result; +} + +[[nodiscard]] rpl::producer DurationValue(int months) { + return (months < 12) + ? tr::lng_months(lt_count, rpl::single(float64(months))) + : tr::lng_years(lt_count, rpl::single(float64(months / 12))); +} + +[[nodiscard]] object_ptr MakePeerTableValue( + not_null parent, + not_null controller, + PeerId id) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto &st = st::giveawayGiftCodeUserpic; + raw->resize(raw->width(), st.photoSize); + + const auto peer = controller->session().data().peer(id); + const auto userpic = Ui::CreateChild(raw, peer, st); + const auto label = Ui::CreateChild( + raw, + peer->name(), + st::giveawayGiftCodeValue); + raw->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto position = st::giveawayGiftCodeNamePosition; + label->resizeToNaturalWidth(width - position.x()); + label->moveToLeft(position.x(), position.y(), width); + const auto top = (raw->height() - userpic->height()) / 2; + userpic->moveToLeft(0, top, width); + }, label->lifetime()); + + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setTextColorOverride(st::windowActiveTextFg->c); + + raw->setClickedCallback([=] { + controller->show(PrepareShortInfoBox(peer, controller)); + }); + + return result; +} + +void AddTableRow( + not_null table, + rpl::producer label, + object_ptr value, + style::margins valueMargins) { + table->addRow( + object_ptr( + table, + std::move(label), + st::giveawayGiftCodeLabel), + std::move(value), + st::giveawayGiftCodeLabelMargin, + valueMargins); +} + +void AddTableRow( + not_null table, + rpl::producer label, + rpl::producer value) { + AddTableRow( + table, + std::move(label), + object_ptr( + table, + std::move(value), + st::giveawayGiftCodeValue), + st::giveawayGiftCodeValueMargin); +} + +void AddTableRow( + not_null table, + rpl::producer label, + not_null controller, + PeerId id) { + if (!id) { + return; + } + AddTableRow( + table, + std::move(label), + MakePeerTableValue(table, controller, id), + st::giveawayGiftCodePeerMargin); +} + } // namespace GiftPremiumValidator::GiftPremiumValidator( @@ -263,3 +439,148 @@ void GiftPremiumValidator::showBox(not_null user) { _requestId = 0; }).send(); } + +void GiftCodeBox( + not_null box, + not_null controller, + const QString &slug) { + struct State { + rpl::variable data; + rpl::variable used; + }; + const auto session = &controller->session(); + const auto state = box->lifetime().make_state(State{}); + state->data = session->api().premium().giftCodeValue(slug); + state->used = state->data.value( + ) | rpl::map([=](const Api::GiftCode &data) { + return data.used; + }); + + box->setWidth(st::boxWideWidth); + box->setStyle(st::giveawayGiftCodeBox); + box->setNoContentMargin(true); + + const auto bar = box->setPinnedToTopContent( + object_ptr( + box, + st::giveawayGiftCodeCover, + nullptr, + rpl::conditional( + state->used.value(), + tr::lng_gift_link_used_title(), + tr::lng_gift_link_title()), + rpl::conditional( + state->used.value(), + tr::lng_gift_link_used_about(Ui::Text::RichLangValue), + tr::lng_gift_link_about(Ui::Text::RichLangValue)), + true)); + + const auto max = st::giveawayGiftCodeTopHeight; + bar->setMaximumHeight(max); + bar->setMinimumHeight(st::infoLayerTopBarHeight); + + bar->resize(bar->width(), bar->maximumHeight()); + + const auto link = MakeGiftCodeLink(&controller->session(), slug); + box->addRow( + MakeLinkLabel( + box, + rpl::single(link.text), + rpl::single(link.link), + box->uiShow()), + st::giveawayGiftCodeLinkMargin); + + auto table = box->addRow( + object_ptr( + box, + st::giveawayGiftCodeTable), + st::giveawayGiftCodeTableMargin); + const auto current = state->data.current(); + AddTableRow( + table, + tr::lng_gift_link_label_from(), + controller, + current.from); + AddTableRow( + table, + tr::lng_gift_link_label_to(), + controller, + current.to); + AddTableRow( + table, + tr::lng_gift_link_label_gift(), + tr::lng_gift_link_gift_premium( + lt_duration, + DurationValue(current.months))); + AddTableRow( + table, + tr::lng_gift_link_label_reason(), + tr::lng_gift_link_reason_giveaway()); + AddTableRow( + table, + tr::lng_gift_link_label_date(), + rpl::single(langDateTime(base::unixtime::parse(current.date)))); + + auto shareLink = tr::lng_gift_link_also_send_link( + ) | rpl::map([](const QString &text) { + return Ui::Text::Link(text); + }); + auto richDate = [](const Api::GiftCode &data) { + return TextWithEntities{ + langDateTime(base::unixtime::parse(data.used)), + }; + }; + const auto footer = box->addRow( + object_ptr( + box, + rpl::conditional( + state->used.value(), + tr::lng_gift_link_used_footer( + lt_date, + state->data.value() | rpl::map(richDate), + Ui::Text::WithEntities), + tr::lng_gift_link_also_send( + lt_link, + std::move(shareLink), + Ui::Text::WithEntities)), + st::giveawayGiftCodeFooter), + st::giveawayGiftCodeFooterMargin); + footer->setClickHandlerFilter([=](const auto &...) { + box->uiShow()->showToast(u"Sharing..."_q); + return false; + }); + + const auto close = Ui::CreateChild( + box.get(), + st::boxTitleClose); + close->setClickedCallback([=] { + box->closeBox(); + }); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0); + }, box->lifetime()); + + const auto button = box->addButton(rpl::conditional( + state->used.value(), + tr::lng_box_ok(), + tr::lng_gift_link_use() + ), [=] { + if (state->used.current()) { + box->closeBox(); + } else { + auto copy = state->data.current(); + copy.used = base::unixtime::now(); + state->data = std::move(copy); + } + }); + const auto buttonPadding = st::giveawayGiftCodeBox.buttonPadding; + const auto buttonWidth = st::boxWideWidth + - buttonPadding.left() + - buttonPadding.right(); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); +} diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index cc11b23886..9fe4c972e5 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -11,6 +11,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class UserData; +namespace Api { +struct GiftCode; +} // namespace Api + +namespace Ui { +class GenericBox; +} // namespace Ui + namespace Window { class SessionController; } // namespace Window @@ -29,3 +37,8 @@ private: mtpRequestId _requestId = 0; }; + +void GiftCodeBox( + not_null box, + not_null controller, + const QString &slug); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 3defcfb6ae..799a192ec4 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "api/api_chat_filters.h" #include "api/api_chat_invite.h" +#include "api/api_premium.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" #include "lang/lang_cloud_manager.h" @@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "boxes/share_box.h" #include "boxes/connection_box.h" +#include "boxes/gift_premium_box.h" #include "boxes/sticker_set_box.h" #include "boxes/sessions_box.h" #include "boxes/language_box.h" @@ -348,6 +350,21 @@ bool ResolveUsernameOrPhone( const auto domainParam = params.value(u"domain"_q); const auto appnameParam = params.value(u"appname"_q); + if (domainParam == u"giftcode"_q && !appnameParam.isEmpty()) { + const auto done = [=](Api::GiftCode code) { + if (!code) { + controller->showToast(u"Gift code link expired"_q); + } else { + controller->show( + Box(GiftCodeBox, controller, appnameParam)); + } + }; + controller->session().api().premium().checkGiftCode( + appnameParam, + crl::guard(controller, done)); + return true; + } + // Fix t.me/s/username links. const auto webChannelPreviewLink = (domainParam == u"s"_q) && !appnameParam.isEmpty(); @@ -1078,7 +1095,7 @@ QString TryConvertUrlToLocal(QString url) { "(" "/?\\?|" "/?$|" - "/[a-zA-Z0-9\\.\\_]+/?(\\?|$)|" + "/[a-zA-Z0-9\\.\\_\\-]+/?(\\?|$)|" "/\\d+/?(\\?|$)|" "/s/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" @@ -1103,7 +1120,7 @@ QString TryConvertUrlToLocal(QString url) { added = u"&post="_q + postMatch->captured(1); } else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&story="_q + storyMatch->captured(1); - } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { + } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_\\-]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&appname="_q + appNameMatch->captured(1); } return base + added + (params.isEmpty() ? QString() : '&' + params); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 0bfb715a8a..02d1f75e16 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -60,8 +60,8 @@ const style::InfoTopBar &TopBarStyle(Wrap wrap) { [[nodiscard]] bool HasCustomTopBar(not_null controller) { const auto section = controller->section(); - return (section.type() == Section::Type::Settings - && (section.settingsType() == ::Settings::PremiumId())); + return (section.type() == Section::Type::Settings) + && section.settingsType()->hasCustomTopBar(); } } // namespace @@ -788,8 +788,8 @@ void WrapWidget::showNewContent( newController->takeStepData(_controller.get()); } auto newContent = object_ptr(nullptr); - const auto enableBackButton = hasBackButton(); - const auto createInAdvance = needAnimation || enableBackButton; + const auto withBackButton = willHaveBackButton(params); + const auto createInAdvance = needAnimation || withBackButton; if (createInAdvance) { newContent = createContent(memento, newController.get()); } @@ -823,7 +823,7 @@ void WrapWidget::showNewContent( _historyStack.clear(); } - if (enableBackButton) { + if (withBackButton) { newContent->enableBackButton(); } @@ -969,6 +969,17 @@ bool WrapWidget::hasBackButton() const { return (wrap() == Wrap::Narrow || hasStackHistory()); } +bool WrapWidget::willHaveBackButton( + const Window::SectionShow ¶ms) const { + using Way = Window::SectionShow::Way; + const auto willSaveToStack = (_content != nullptr) + && (params.way == Way::Forward); + const auto willClearStack = (params.way == Way::ClearStack); + const auto willHaveStack = !willClearStack + && (hasStackHistory() || willSaveToStack); + return (wrap() == Wrap::Narrow) || willHaveStack; +} + WrapWidget::~WrapWidget() = default; } // namespace Info diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 5445185e1c..d16108114d 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -176,6 +176,8 @@ private: void setupShortcuts(); [[nodiscard]] bool hasBackButton() const; + [[nodiscard]] bool willHaveBackButton( + const Window::SectionShow ¶ms) const; not_null topWidget() const; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 84cb8960ac..3ea1b81dd1 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -424,8 +424,6 @@ notifyPreviewBottomSkip: 9px; settingsPremiumButtonPadding: margins(11px, 11px, 11px, 3px); settingsPremiumTopBarBackIcon: icon {{ "info/info_back", premiumButtonFg }}; settingsPremiumTopBarBackIconOver: icon {{ "info/info_back", premiumButtonFg }}; -settingsPremiumStarSize: size(84px, 81px); -settingsPremiumStarTopSkip: 37px; settingsPremiumTopBarBack: IconButton(infoTopBarBack) { icon: settingsPremiumTopBarBackIcon; iconOver: settingsPremiumTopBarBackIconOver; @@ -467,21 +465,6 @@ settingsPremiumPreviewIconTitlePadding: margins(62px, 13px, 24px, 1px); settingsPremiumPreviewIconAboutPadding: margins(62px, 0px, 24px, 0px); settingsPremiumPreviewIconPosition: point(20px, 7px); -settingsPremiumTitlePadding: margins(0px, 18px, 0px, 11px); -settingsPremiumAboutTextStyle: TextStyle(defaultTextStyle) { - font: font(12px); - linkUnderline: kLinkUnderlineAlways; - lineHeight: 18px; -} -settingsPremiumAbout: FlatLabel(defaultFlatLabel) { - style: settingsPremiumAboutTextStyle; - palette: TextPalette(defaultTextPalette) { - linkFg: premiumButtonFg; - } - align: align(top); - textFg: premiumButtonFg; - minWidth: 190px; -} settingsPremiumArrowShift: point(-5px, -1px); settingsPremiumArrow: icon{{ "settings/premium/arrow", menuIconFg }}; settingsPremiumArrowOver: icon{{ "settings/premium/arrow", menuIconFgOver }}; @@ -499,12 +482,6 @@ settingsPremiumUserTitle: FlatLabel(boxTitle) { maxHeight: 0px; align: align(top); } -settingsPremiumUserAbout: FlatLabel(boxDividerLabel) { - style: settingsPremiumAboutTextStyle; - minWidth: 315px; - maxHeight: 0px; - align: align(top); -} settingsPremiumLock: icon{{ "emoji/premium_lock", windowActiveTextFg, point(0px, 1px) }}; settingsPremiumLockSkip: 3px; diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 0a7a48384b..c0973b3d63 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -56,6 +56,9 @@ struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr create( not_null parent, not_null controller) const = 0; + [[nodiscard]] virtual bool hasCustomTopBar() const { + return false; + } virtual ~AbstractSectionFactory() = default; }; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index ba0acc981e..3dd3120748 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -518,10 +518,10 @@ TopBarUser::TopBarUser( not_null controller, not_null peer, rpl::producer<> showFinished) -: TopBarAbstract(parent) +: TopBarAbstract(parent, st::userPremiumCover) , _content(this) , _title(_content, st::settingsPremiumUserTitle) -, _about(_content, st::settingsPremiumUserAbout) +, _about(_content, st::userPremiumCover.about) , _ministars(_content) , _smallTop({ .widget = object_ptr(this), @@ -1173,6 +1173,7 @@ QPointer Premium::createPinnedToTop( }; return Ui::CreateChild( parent.get(), + st::defaultPremiumCover, clickContextOther, std::move(title), std::move(about)); @@ -1371,6 +1372,24 @@ QPointer Premium::createPinnedToBottom( } // namespace +template <> +struct SectionFactory : AbstractSectionFactory { + object_ptr create( + not_null parent, + not_null controller + ) const final override { + return object_ptr(parent, controller); + } + bool hasCustomTopBar() const final override { + return true; + } + + [[nodiscard]] static const std::shared_ptr &Instance() { + static const auto result = std::make_shared(); + return result; + } +}; + Type PremiumId() { return Premium::Id(); } diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 55e89602ad..b3442e8a70 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -26,6 +26,43 @@ PremiumBubble { tailSize: size; font: font; } +PremiumCover { + starSize: size; + starTopSkip: pixels; + titlePadding: margins; + titleFont: font; + about: FlatLabel; +} + +premiumAboutTextStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineAlways; + lineHeight: 18px; +} +defaultPremiumCover: PremiumCover { + starSize: size(84px, 81px); + starTopSkip: 37px; + titlePadding: margins(0px, 18px, 0px, 11px); + titleFont: boxTitleFont; + about: FlatLabel(defaultFlatLabel) { + style: premiumAboutTextStyle; + palette: TextPalette(defaultTextPalette) { + linkFg: premiumButtonFg; + } + align: align(top); + textFg: premiumButtonFg; + minWidth: 190px; + } +} +userPremiumCoverAbout: FlatLabel(boxDividerLabel) { + style: premiumAboutTextStyle; + minWidth: 315px; + maxHeight: 0px; + align: align(top); +} +userPremiumCover: PremiumCover(defaultPremiumCover) { + about: userPremiumCoverAbout; +} defaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) { minWidth: 220px; @@ -239,3 +276,69 @@ boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px); boostReplaceArrow: icon{{ "mediaview/next", windowSubTextFg }}; + +giveawayGiftCodeTopHeight: 195px; +giveawayGiftCodeLink: FlatLabel(defaultFlatLabel) { + margin: margins(10px, 12px, 10px, 8px); + textFg: menuIconColor; + maxHeight: 24px; +} +giveawayGiftCodeLinkCopy: icon{{ "menu/copy", menuIconColor }}; +giveawayGiftCodeLinkHeight: 42px; +giveawayGiftCodeLinkCopyWidth: 40px; +giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px); + +giveawayGiftCodeTable: Table(defaultTable) { + labelMinWidth: 91px; +} +giveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px); +giveawayGiftCodeLabel: FlatLabel(defaultFlatLabel) { + textFg: menuIconColor; + maxHeight: 24px; + style: TextStyle(semiboldTextStyle) { + font: font(12px semibold); + } +} +giveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px); +giveawayGiftCodeValue: FlatLabel(defaultFlatLabel) { + maxHeight: 24px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineNever; + } +} +giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px); +giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px); +giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) { + size: size(24px, 24px); + photoSize: 24px; + photoPosition: point(-1px, -1px); +} +giveawayGiftCodeNamePosition: point(32px, 4px); +giveawayGiftCodeCover: PremiumCover(userPremiumCover) { + starSize: size(92px, 90px); + starTopSkip: 20px; + titlePadding: margins(0px, 15px, 0px, 17px); + titleFont: font(15px semibold); + about: FlatLabel(userPremiumCoverAbout) { + textFg: windowBoldFg; + style: TextStyle(premiumAboutTextStyle) { + lineHeight: 17px; + } + } +} +giveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) { + align: align(top); + textFg: windowBoldFg; +} +giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px); +giveawayGiftCodeBox: Box(defaultBox) { + buttonPadding: margins(22px, 11px, 22px, 22px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + } + shadowIgnoreTopSkip: true; +} diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 309db7a6a7..951eb8f219 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/fade_wrap.h" #include "styles/style_layers.h" #include "styles/style_settings.h" +#include "styles/style_premium.h" #include @@ -90,6 +91,13 @@ QImage GenerateStarForLightTopBar(QRectF rect) { return frame; } +TopBarAbstract::TopBarAbstract( + QWidget *parent, + const style::PremiumCover &st) +: RpWidget(parent) +, _st(st) { +} + void TopBarAbstract::setRoundEdges(bool value) { _roundEdges = value; update(); @@ -122,11 +130,11 @@ void TopBarAbstract::paintEdges(QPainter &p) const { QRectF TopBarAbstract::starRect( float64 topProgress, float64 sizeProgress) const { - const auto starSize = st::settingsPremiumStarSize * sizeProgress; + const auto starSize = _st.starSize * sizeProgress; return QRectF( QPointF( (width() - starSize.width()) / 2, - st::settingsPremiumStarTopSkip * topProgress), + _st.starTopSkip * topProgress), starSize); }; @@ -143,18 +151,16 @@ void TopBarAbstract::computeIsDark() { TopBar::TopBar( not_null parent, + const style::PremiumCover &st, Fn clickContextOther, rpl::producer title, rpl::producer about, bool light) -: TopBarAbstract(parent) +: TopBarAbstract(parent, st) , _light(light) -, _titleFont(st::boxTitle.style.font) -, _titlePadding(st::settingsPremiumTitlePadding) -, _about( - this, - std::move(about), - _light ? st::settingsPremiumUserAbout : st::settingsPremiumAbout) +, _titleFont(st.titleFont) +, _titlePadding(st.titlePadding) +, _about(this, std::move(about), st.about) , _ministars(this) { std::move( title @@ -219,7 +225,7 @@ void TopBar::setTextPosition(int x, int y) { rpl::producer TopBar::additionalHeight() const { return _about->heightValue( - ) | rpl::map([l = st::settingsPremiumAbout.style.lineHeight](int height) { + ) | rpl::map([l = st().about.style.lineHeight](int height) { return std::max(height - l * 2, 0); }); } diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index 0cef38dc58..c0749df00a 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -11,6 +11,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "ui/effects/premium_stars_colored.h" +namespace style { +struct PremiumCover; +} // namespace style + +namespace st { +extern const style::PremiumCover &defaultPremiumCover; +} // namespace st + namespace Ui { class FlatLabel; } // namespace Ui @@ -23,7 +31,9 @@ namespace Ui::Premium { class TopBarAbstract : public RpWidget { public: - using RpWidget::RpWidget; + TopBarAbstract( + QWidget *parent = nullptr, + const style::PremiumCover &st = st::defaultPremiumCover); void setRoundEdges(bool value); @@ -32,6 +42,10 @@ public: [[nodiscard]] virtual rpl::producer additionalHeight() const = 0; + [[nodiscard]] const style::PremiumCover &st() const { + return _st; + } + protected: void paintEdges(QPainter &p, const QBrush &brush) const; void paintEdges(QPainter &p) const; @@ -44,6 +58,7 @@ protected: void computeIsDark(); private: + const style::PremiumCover &_st; bool _roundEdges = true; bool _isDark = false; @@ -53,6 +68,7 @@ class TopBar final : public TopBarAbstract { public: TopBar( not_null parent, + const style::PremiumCover &st, Fn clickContextOther, rpl::producer title, rpl::producer about,