Added ability to create invite links with subscription.

This commit is contained in:
23rd 2024-08-08 14:17:55 +03:00 committed by John Preston
parent 44c1109798
commit 8158c52e82
10 changed files with 306 additions and 23 deletions

View file

@ -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

View file

@ -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.";

View file

@ -737,6 +737,12 @@ auto InviteLinks::parse(
return std::optional<Link>(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(),

View file

@ -16,6 +16,7 @@ namespace Api {
struct InviteLink {
QString link;
QString label;
Data::PeerSubscription subscription;
not_null<UserData*> admin;
TimeId date = 0;
TimeId startDate = 0;

View file

@ -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<Ui::BoxContent> InviteLinkQrBox(
object_ptr<Ui::BoxContent> EditLinkBox(
not_null<PeerData*> 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<QPointer<Ui::GenericBox>>();
using Fields = Ui::InviteLinkFields;
@ -1266,6 +1269,9 @@ object_ptr<Ui::BoxContent> 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<Ui::BoxContent> EditLinkBox(
result.expireDate,
result.usageLimit,
result.requestApproval,
{ uint64(result.subscriptionCredits), period },
});
} else {
peer->session().api().inviteLinks().edit(
@ -1288,26 +1295,31 @@ object_ptr<Ui::BoxContent> 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<Ui::GenericBox*> 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<Ui::BoxContent> RevokeLinkBox(

View file

@ -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) {

View file

@ -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<GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done) {
using namespace rpl::mappers;
@ -95,14 +99,17 @@ void EditInviteLinkBox(
int expireValue = 0;
int usageValue = 0;
rpl::variable<bool> requestApproval = false;
rpl::variable<bool> subscription = false;
};
const auto state = box->lifetime().make_state<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<SettingsButton>(
@ -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<Ui::InputField>(
@ -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<GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
bool isGroup,
bool isPublic,
Fn<void(InviteLinkFields)> done) {
EditInviteLinkBox(
box,
std::move(fillSubscription),
InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },
std::move(done));
}

View file

@ -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<Ui::SettingsButton*> button;
not_null<Ui::NumberInput*> amount;
};
void EditInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done);
void CreateInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
bool isGroup,
bool isPublic,
Fn<void(InviteLinkFields)> done);

View file

@ -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<Ui::GenericBox*> box,
not_null<PeerData*> peer) {
struct State final {
float64 usdRate = 0;
};
const auto state = box->lifetime().make_state<State>();
const auto currency = u"USD"_q;
const auto container = box->verticalLayout();
const auto toggle = container->add(
object_ptr<SettingsButton>(
container,
tr::lng_group_invite_subscription(),
st::settingsButtonNoIconLocked),
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
const auto maxCredits = peer->session().appConfig().get<int>(
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<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
box->setShowFinishedCallback([=] {
wrap->toggleOn(toggle->toggledValue());
wrap->finishAnimating();
});
const auto inputContainer = wrap->entity()->add(
CreateSkipWidget(container, st.heightMin - skip));
const auto input = CreateChild<NumberInput>(
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<Ui::RpWidget>(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<Api::CreditsEarnStatistics>(
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<Ui::DividerLabel>(
container,
std::move(about),
st::defaultBoxDividerLabelPadding,
RectPart::Top | RectPart::Bottom));
return { toggle, input };
}
} // namespace Ui

View file

@ -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<Ui::GenericBox*> box,
not_null<PeerData*> peer);
} // namespace Ui