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