diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cb934282b..4dafca555 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1474,6 +1474,8 @@ PRIVATE support/support_preload.h support/support_templates.cpp support/support_templates.h + ui/boxes/edit_invite_link_session.cpp + ui/boxes/edit_invite_link_session.h ui/chat/attach/attach_item_single_file_preview.cpp ui/chat/attach/attach_item_single_file_preview.h ui/chat/attach/attach_item_single_media_preview.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cb07fac5b..a77206560 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2021,6 +2021,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_about_no_approve" = "New users will be able to join the group without being approved by the admins."; "lng_group_invite_about_approve_channel" = "New users will be able to join the channel only after having been approved by the admins."; "lng_group_invite_about_no_approve_channel" = "New users will be able to join the channel without being approved by the admins."; +"lng_group_invite_subscription" = "Require Monthly Fee"; +"lng_group_invite_subscription_ph" = "Stars Amount per month"; +"lng_group_invite_subscription_price" = "~{cost} / month"; +"lng_group_invite_subscription_toast" = "Sorry, you cannot change the number of Stars for an already created invite link."; +"lng_group_invite_subscription_about" = "Charge a subscription fee from people joining your channel via this link. {link}"; +"lng_group_invite_subscription_about_link" = "Learn more {emoji}"; +"lng_group_invite_subscription_about_url" = "https://telegram.org/tos/stars"; "lng_group_request_to_join" = "Request to Join"; "lng_group_request_about" = "This group accepts new members only after they are approved by its admins."; diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index 23a11a507..511cc5650 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -737,6 +737,12 @@ auto InviteLinks::parse( return std::optional(Link{ .link = qs(data.vlink()), .label = qs(data.vtitle().value_or_empty()), + .subscription = data.vsubscription_pricing() + ? Data::PeerSubscription{ + data.vsubscription_pricing()->data().vamount().v, + data.vsubscription_pricing()->data().vperiod().v, + } + : Data::PeerSubscription(), .admin = peer->session().data().user(data.vadmin_id()), .date = data.vdate().v, .startDate = data.vstart_date().value_or_empty(), diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index 572960223..892cd56f0 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -16,6 +16,7 @@ namespace Api { struct InviteLink { QString link; QString label; + Data::PeerSubscription subscription; not_null admin; TimeId date = 0; TimeId startDate = 0; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index e6173c1d1..1cb38ced1 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/text/text_utilities.h" #include "ui/boxes/edit_invite_link.h" +#include "ui/boxes/edit_invite_link_session.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "boxes/share_box.h" @@ -1251,6 +1252,8 @@ object_ptr InviteLinkQrBox( object_ptr EditLinkBox( not_null peer, const Api::InviteLink &data) { + constexpr auto kPeriod = 3600 * 24 * 30; + constexpr auto kTestModePeriod = 300; const auto creating = data.link.isEmpty(); const auto box = std::make_shared>(); using Fields = Ui::InviteLinkFields; @@ -1266,6 +1269,9 @@ object_ptr EditLinkBox( }; if (creating) { Assert(data.admin->isSelf()); + const auto period = peer->session().isTestMode() + ? kTestModePeriod + : kPeriod; peer->session().api().inviteLinks().create({ peer, finish, @@ -1273,6 +1279,7 @@ object_ptr EditLinkBox( result.expireDate, result.usageLimit, result.requestApproval, + { uint64(result.subscriptionCredits), period }, }); } else { peer->session().api().inviteLinks().edit( @@ -1288,26 +1295,31 @@ object_ptr EditLinkBox( }; const auto isGroup = !peer->isBroadcast(); const auto isPublic = peer->isChannel() && peer->asChannel()->isPublic(); - if (creating) { - auto object = Box(Ui::CreateInviteLinkBox, isGroup, isPublic, done); - *box = Ui::MakeWeak(object.data()); - return object; - } else { - auto object = Box( - Ui::EditInviteLinkBox, - Fields{ - .link = data.link, - .label = data.label, - .expireDate = data.expireDate, - .usageLimit = data.usageLimit, - .requestApproval = data.requestApproval, - .isGroup = isGroup, - .isPublic = isPublic, - }, - done); - *box = Ui::MakeWeak(object.data()); - return object; - } + auto object = Box([=](not_null box) { + const auto fill = [=] { + return Ui::FillCreateInviteLinkSubscriptionToggle(box, peer); + }; + if (creating) { + Ui::CreateInviteLinkBox(box, fill, isGroup, isPublic, done); + } else { + Ui::EditInviteLinkBox( + box, + fill, + Fields{ + .link = data.link, + .label = data.label, + .expireDate = data.expireDate, + .usageLimit = data.usageLimit, + .subscriptionCredits = data.subscription.period, + .requestApproval = data.requestApproval, + .isGroup = isGroup, + .isPublic = isPublic, + }, + done); + } + }); + *box = Ui::MakeWeak(object.data()); + return object; } object_ptr RevokeLinkBox( diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 4d5a9bc04..1eaa10069 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -992,6 +992,24 @@ inviteLinkQrSkip: 24px; inviteLinkQrMargin: margins(0px, 0px, 0px, 13px); inviteLinkQrValuePadding: margins(22px, 0px, 22px, 12px); +inviteLinkCreditsField: InputField(defaultInputField) { + textMargins: margins(23px, 10px, 0px, 0px); + textAlign: align(left); + textFg: historyComposeAreaFg; + textBg: historyComposeAreaBg; + heightMin: 36px; + heightMax: 36px; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(0px, 0px, 2px, 0px); + placeholderAlign: align(left); + placeholderScale: 0.; + placeholderFont: normalFont; + placeholderShift: -50px; + duration: 100; +} + usernamesReorderIcon: icon {{ "stickers_reorder", dialogsMenuIconFg }}; infoAboutGigagroup: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp index c536c116d..3e09d254b 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp @@ -11,9 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/boxes/choose_date_time.h" #include "ui/layers/generic_box.h" +#include "ui/vertical_list.h" +#include "ui/text/format_values.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/number_input.h" +#include "ui/effects/credits_graphics.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_settings.h" @@ -44,6 +47,7 @@ constexpr auto kMaxLabelLength = 32; void EditInviteLinkBox( not_null box, + Fn fillSubscription, const InviteLinkFields &data, Fn done) { using namespace rpl::mappers; @@ -95,14 +99,17 @@ void EditInviteLinkBox( int expireValue = 0; int usageValue = 0; rpl::variable requestApproval = false; + rpl::variable subscription = false; }; const auto state = box->lifetime().make_state(State{ .expireValue = expire, .usageValue = usage, .requestApproval = (data.requestApproval && !isPublic), + .subscription = false, }); - const auto requestApproval = isPublic + const auto subscriptionLocked = data.subscriptionCredits > 0; + const auto requestApproval = (isPublic || subscriptionLocked) ? nullptr : container->add( object_ptr( @@ -111,8 +118,11 @@ void EditInviteLinkBox( st::settingsButtonNoIcon), style::margins{ 0, 0, 0, st::defaultVerticalListSkip }); if (requestApproval) { - requestApproval->toggleOn(state->requestApproval.value()); - state->requestApproval = requestApproval->toggledValue(); + requestApproval->toggleOn(state->requestApproval.value(), true); + requestApproval->setClickedCallback([=] { + state->requestApproval.force_assign(!requestApproval->toggled()); + state->subscription.force_assign(false); + }); addDivider(container, rpl::conditional( state->requestApproval.value(), (isGroup @@ -122,6 +132,30 @@ void EditInviteLinkBox( ? tr::lng_group_invite_about_no_approve() : tr::lng_group_invite_about_no_approve_channel()))); } + auto credits = (Ui::NumberInput*)(nullptr); + if (!isPublic && fillSubscription) { + Ui::AddSkip(container); + const auto &[subscription, input] = fillSubscription(); + credits = input.get(); + subscription->toggleOn(state->subscription.value(), true); + if (subscriptionLocked) { + input->setText(QString::number(data.subscriptionCredits)); + input->setReadOnly(true); + state->subscription.force_assign(true); + state->requestApproval.force_assign(false); + subscription->setToggleLocked(true); + subscription->finishAnimating(); + } + subscription->setClickedCallback([=, show = box->uiShow()] { + if (subscriptionLocked) { + show->showToast( + tr::lng_group_invite_subscription_toast(tr::now)); + return; + } + state->subscription.force_assign(!subscription->toggled()); + state->requestApproval.force_assign(false); + }); + } const auto labelField = container->add( object_ptr( @@ -327,6 +361,9 @@ void EditInviteLinkBox( .label = label, .expireDate = expireDate, .usageLimit = usageLimit, + .subscriptionCredits = credits + ? credits->getLastText().toInt() + : 0, .requestApproval = state->requestApproval.current(), .isGroup = isGroup, .isPublic = isPublic, @@ -337,11 +374,13 @@ void EditInviteLinkBox( void CreateInviteLinkBox( not_null box, + Fn fillSubscription, bool isGroup, bool isPublic, Fn done) { EditInviteLinkBox( box, + std::move(fillSubscription), InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic }, std::move(done)); } diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link.h b/Telegram/SourceFiles/ui/boxes/edit_invite_link.h index 5860c7054..0b9805cd6 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link.h +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class GenericBox; +class NumberInput; class SettingsButton; struct InviteLinkFields { @@ -17,18 +18,26 @@ struct InviteLinkFields { QString label; TimeId expireDate = 0; int usageLimit = 0; + int subscriptionCredits = 0; bool requestApproval = false; bool isGroup = false; bool isPublic = false; }; +struct InviteLinkSubscriptionToggle final { + not_null button; + not_null amount; +}; + void EditInviteLinkBox( not_null box, + Fn fillSubscription, const InviteLinkFields &data, Fn done); void CreateInviteLinkBox( not_null box, + Fn fillSubscription, bool isGroup, bool isPublic, Fn done); diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp new file mode 100644 index 000000000..a46e25811 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp @@ -0,0 +1,166 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/boxes/edit_invite_link_session.h" + +#include "api/api_credits.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "ui/boxes/edit_invite_link.h" // InviteLinkSubscriptionToggle +#include "ui/effects/credits_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/widgets/label_with_custom_emoji.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_channel_earn.h" +#include "styles/style_chat.h" +#include "styles/style_settings.h" +#include "styles/style_layers.h" +#include "styles/style_info.h" + +namespace Ui { + +InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle( + not_null box, + not_null peer) { + struct State final { + float64 usdRate = 0; + }; + const auto state = box->lifetime().make_state(); + const auto currency = u"USD"_q; + + const auto container = box->verticalLayout(); + const auto toggle = container->add( + object_ptr( + container, + tr::lng_group_invite_subscription(), + st::settingsButtonNoIconLocked), + style::margins{ 0, 0, 0, st::defaultVerticalListSkip }); + + const auto maxCredits = peer->session().appConfig().get( + u"stars_subscription_amount_max"_q, + 2500); + + const auto &st = st::inviteLinkCreditsField; + const auto skip = st.textMargins.top() / 2; + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + box->setShowFinishedCallback([=] { + wrap->toggleOn(toggle->toggledValue()); + wrap->finishAnimating(); + }); + const auto inputContainer = wrap->entity()->add( + CreateSkipWidget(container, st.heightMin - skip)); + const auto input = CreateChild( + inputContainer, + st, + tr::lng_group_invite_subscription_ph(), + QString(), + maxCredits); + wrap->toggledValue() | rpl::start_with_next([=](bool shown) { + if (shown) { + input->setFocus(); + } + }, input->lifetime()); + const auto icon = CreateSingleStarWidget( + inputContainer, + st.style.font->height); + const auto priceOverlay = Ui::CreateChild(inputContainer); + priceOverlay->setAttribute(Qt::WA_TransparentForMouseEvents); + inputContainer->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + input->resize( + size.width() - rect::m::sum::h(st::boxRowPadding), + st.heightMin); + input->moveToLeft(st::boxRowPadding.left(), -skip); + icon->moveToLeft( + st::boxRowPadding.left(), + input->pos().y() + st.textMargins.top()); + priceOverlay->resize(size); + }, input->lifetime()); + ToggleChildrenVisibility(inputContainer, true); + QObject::connect(input, &Ui::MaskedInputField::changed, [=] { + priceOverlay->update(); + }); + priceOverlay->paintRequest( + ) | rpl::start_with_next([=, right = st::boxRowPadding.right()] { + if (state->usdRate <= 0) { + return; + } + const auto amount = input->getLastText().toDouble(); + if (amount <= 0) { + return; + } + const auto text = tr::lng_group_invite_subscription_price( + tr::now, + lt_cost, + Ui::FillAmountAndCurrency(amount * state->usdRate, currency)); + auto p = QPainter(priceOverlay); + p.setFont(st.placeholderFont); + p.setPen(st.placeholderFg); + p.setBrush(Qt::NoBrush); + const auto textWidth = st.placeholderFont->width(text); + const auto m = QMargins(0, skip, right, 0); + p.drawText(priceOverlay->rect() - m, text, style::al_right); + }, priceOverlay->lifetime()); + + { + auto &lifetime = priceOverlay->lifetime(); + const auto api = lifetime.make_state( + peer); + api->request( + ) | rpl::start_with_done([=] { + state->usdRate = api->data().usdRate; + priceOverlay->update(); + }, priceOverlay->lifetime()); + } + + const auto arrow = Ui::Text::SingleCustomEmoji( + peer->owner().customEmojiManager().registerInternalEmoji( + st::topicButtonArrow, + st::channelEarnLearnArrowMargins, + false)); + auto about = Ui::CreateLabelWithCustomEmoji( + container, + tr::lng_group_invite_subscription_about( + lt_link, + tr::lng_group_invite_subscription_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + tr::lng_group_invite_subscription_about_url(tr::now)); + }), + Ui::Text::RichLangValue), + { .session = &peer->session() }, + st::boxDividerLabel); + Ui::AddSkip(wrap->entity()); + Ui::AddSkip(wrap->entity()); + container->add(object_ptr( + container, + std::move(about), + st::defaultBoxDividerLabelPadding, + RectPart::Top | RectPart::Bottom)); + return { toggle, input }; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h new file mode 100644 index 000000000..c4dae66c7 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h @@ -0,0 +1,23 @@ +/* +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 + +class PeerData; + +namespace Ui { + +class GenericBox; +class SettingsButton; + +struct InviteLinkSubscriptionToggle; + +InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle( + not_null box, + not_null peer); + +} // namespace Ui