Allow editing charge-for-message privacy.

This commit is contained in:
John Preston 2025-02-11 19:26:20 +04:00
parent 909b01241b
commit f2aa3afbbb
8 changed files with 327 additions and 42 deletions

View file

@ -1218,6 +1218,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_privacy_contacts" = "My contacts";
"lng_edit_privacy_close_friends" = "Close friends";
"lng_edit_privacy_contacts_and_premium" = "Contacts & Premium";
"lng_edit_privacy_paid" = "Paid";
"lng_edit_privacy_contacts_and_miniapps" = "Contacts & Mini Apps";
"lng_edit_privacy_nobody" = "Nobody";
"lng_edit_privacy_premium" = "Premium users";
@ -1356,6 +1357,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_messages_privacy_premium_about" = "Subscribe now to change this setting and get access to other exclusive features of Telegram Premium.";
"lng_messages_privacy_premium" = "Only subscribers of {link} can select this option.";
"lng_messages_privacy_premium_link" = "Telegram Premium";
"lng_messages_privacy_charge" = "Charge for messages";
"lng_messages_privacy_charge_about" = "Charge a fee for messages from people outside your contacts or those you haven't messaged first.";
"lng_messages_privacy_price" = "Set your price per message";
"lng_messages_privacy_price_about" = "You will receive {percent} of the selected fee ({amount}) for each incoming message.";
"lng_messages_privacy_exceptions" = "Exceptions";
"lng_messages_privacy_remove_fee" = "Remove Fee";
"lng_messages_privacy_remove_about" = "Add users or entire groups who won't be charged for sending messages to you.";
"lng_self_destruct_title" = "Account self-destruction";
"lng_self_destruct_description" = "If you don't come online at least once within this period, your account will be deleted along with all groups, messages and contacts.";
@ -2158,6 +2166,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_boost_apply#other" = "{from} boosted the group {count} times";
"lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?";
"lng_action_payment_refunded" = "{peer} refunded {amount}";
"lng_action_paid_message_sent#one" = "You paid {count} Star to send a message";
"lng_action_paid_message_sent#other" = "You paid {count} Stars to send a message";
"lng_action_paid_message_got#one" = "You received {count} Star from {name}";
"lng_action_paid_message_got#other" = "You received {count} Stars from {name}";
"lng_similar_channels_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all";
@ -4798,6 +4810,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_boosts_no_restrict" = "Do not restrict boosters";
"lng_rights_boosts_about" = "Turn this on to always allow users who boosted your group to send messages and media.";
"lng_rights_boosts_about_on" = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages.";
"lng_rights_charge_stars" = "Charge Stars for Messages";
"lng_rights_charge_stars_about" = "If you turn this on, regular members of the group will have to pay Stars to send messages.";
"lng_rights_charge_price" = "Set price per message";
"lng_rights_charge_price_about" = "Your group will receive {percent} of the selected fee ({amount}) for each incoming message.";
"lng_slowmode_enabled" = "Slow Mode is active.\nYou can send your next message in {left}.";
"lng_slowmode_no_many" = "Slow mode is enabled. You can't send more than one message at a time.";
@ -4805,6 +4821,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_slowmode_seconds#one" = "{count} second";
"lng_slowmode_seconds#other" = "{count} seconds";
"lng_payment_for_message#one" = "Pay {star}{count} for 1 Message";
"lng_payment_for_message#other" = "Pay {star}{count} for 1 Message";
"lng_payment_confirm_title" = "Confirm payment";
"lng_payment_confirm_text#one" = "{name} charges **{count}** Star per message.";
"lng_payment_confirm_text#other" = "{name} charges **{count}** Stars per message.";
"lng_payment_confirm_sure#one" = "Would you like to pay **{count}** Star to send one message?";
"lng_payment_confirm_sure#other" = "Would you like to pay **{count}** Stars to send one message?";
"lng_payment_confirm_dont_ask" = "Don't ask me again";
"lng_payment_confirm_button" = "Pay for 1 Message";
"lng_payment_bar_text#one" = "{name} must pay {star}{count} for each message to you.";
"lng_payment_bar_text#other" = "{name} must pay {star}{count} for each message to you.";
"lng_payment_bar_button" = "Remove Fee";
"lng_payment_refund_title" = "Remove Fee";
"lng_payment_refund_text" = "Are you sure you want to allow {name} to message you for free?";
"lng_payment_refund_also#one" = "Refund already paid {count} Star";
"lng_payment_refund_also#other" = "Refund already paid {count} Stars";
"lng_payment_refund_confirm" = "Confirm";
"lng_rights_gigagroup_title" = "Broadcast group";
"lng_rights_gigagroup_convert" = "Convert to Broadcast Group";
"lng_rights_gigagroup_about" = "Broadcast groups can have over 200,000 members, but only admins can send messages in them.";

View file

@ -114,7 +114,8 @@ void GlobalPrivacy::updateHideReadTime(bool hide) {
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hide,
newRequirePremiumCurrent());
newRequirePremiumCurrent(),
newChargeStarsCurrent());
}
bool GlobalPrivacy::hideReadTimeCurrent() const {
@ -125,14 +126,6 @@ rpl::producer<bool> GlobalPrivacy::hideReadTime() const {
return _hideReadTime.value();
}
void GlobalPrivacy::updateNewRequirePremium(bool value) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
value);
}
bool GlobalPrivacy::newRequirePremiumCurrent() const {
return _newRequirePremium.current();
}
@ -141,6 +134,25 @@ rpl::producer<bool> GlobalPrivacy::newRequirePremium() const {
return _newRequirePremium.value();
}
int GlobalPrivacy::newChargeStarsCurrent() const {
return _newChargeStars.current();
}
rpl::producer<int> GlobalPrivacy::newChargeStars() const {
return _newChargeStars.value();
}
void GlobalPrivacy::updateMessagesPrivacy(
bool requirePremium,
int chargeStars) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
requirePremium,
chargeStars);
}
void GlobalPrivacy::loadPaidReactionShownPeer() {
if (_paidReactionShownPeerLoaded) {
return;
@ -169,7 +181,8 @@ void GlobalPrivacy::updateArchiveAndMute(bool value) {
value,
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
newRequirePremiumCurrent());
newRequirePremiumCurrent(),
newChargeStarsCurrent());
}
void GlobalPrivacy::updateUnarchiveOnNewMessage(
@ -178,14 +191,16 @@ void GlobalPrivacy::updateUnarchiveOnNewMessage(
archiveAndMuteCurrent(),
value,
hideReadTimeCurrent(),
newRequirePremiumCurrent());
newRequirePremiumCurrent(),
newChargeStarsCurrent());
}
void GlobalPrivacy::update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium) {
bool newRequirePremium,
int newChargeStars) {
using Flag = MTPDglobalPrivacySettings::Flag;
_api.request(_requestId).cancel();
@ -204,38 +219,44 @@ void GlobalPrivacy::update(
| (hideReadTime ? Flag::f_hide_read_marks : Flag())
| ((newRequirePremium && newRequirePremiumAllowed)
? Flag::f_new_noncontact_peers_require_premium
: Flag());
auto nonContactPaidStars = int64(0);
: Flag())
| Flag::f_noncontact_peers_paid_stars;
_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
MTP_globalPrivacySettings(
MTP_flags(flags),
MTP_long(nonContactPaidStars))
MTP_long(newChargeStars))
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
}).fail([=](const MTP::Error &error) {
_requestId = 0;
if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
update(archiveAndMute, unarchiveOnNewMessage, hideReadTime, {});
update(
archiveAndMute,
unarchiveOnNewMessage,
hideReadTime,
false,
0);
}
}).send();
_archiveAndMute = archiveAndMute;
_unarchiveOnNewMessage = unarchiveOnNewMessage;
_hideReadTime = hideReadTime;
_newRequirePremium = newRequirePremium;
_newChargeStars = newChargeStars;
}
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) {
data.match([&](const MTPDglobalPrivacySettings &data) {
_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
_unarchiveOnNewMessage = data.is_keep_archived_unmuted()
? UnarchiveOnNewMessage::None
: data.is_keep_archived_folders()
? UnarchiveOnNewMessage::NotInFoldersUnmuted
: UnarchiveOnNewMessage::AnyUnmuted;
_hideReadTime = data.is_hide_read_marks();
_newRequirePremium = data.is_new_noncontact_peers_require_premium();
});
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
const auto &data = settings.data();
_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
_unarchiveOnNewMessage = data.is_keep_archived_unmuted()
? UnarchiveOnNewMessage::None
: data.is_keep_archived_folders()
? UnarchiveOnNewMessage::NotInFoldersUnmuted
: UnarchiveOnNewMessage::AnyUnmuted;
_hideReadTime = data.is_hide_read_marks();
_newRequirePremium = data.is_new_noncontact_peers_require_premium();
_newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty();
}
} // namespace Api

View file

@ -49,23 +49,28 @@ public:
[[nodiscard]] bool hideReadTimeCurrent() const;
[[nodiscard]] rpl::producer<bool> hideReadTime() const;
void updateNewRequirePremium(bool value);
[[nodiscard]] bool newRequirePremiumCurrent() const;
[[nodiscard]] rpl::producer<bool> newRequirePremium() const;
[[nodiscard]] int newChargeStarsCurrent() const;
[[nodiscard]] rpl::producer<int> newChargeStars() const;
void updateMessagesPrivacy(bool requirePremium, int chargeStars);
void loadPaidReactionShownPeer();
void updatePaidReactionShownPeer(PeerId shownPeer);
[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
[[nodiscard]] rpl::producer<PeerId> paidReactionShownPeer() const;
private:
void apply(const MTPGlobalPrivacySettings &data);
void apply(const MTPGlobalPrivacySettings &settings);
void update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium);
bool newRequirePremium,
int newChargeStars);
const not_null<Main::Session*> _session;
MTP::Sender _api;
@ -76,6 +81,7 @@ private:
rpl::variable<bool> _showArchiveAndMute = false;
rpl::variable<bool> _hideReadTime = false;
rpl::variable<bool> _newRequirePremium = false;
rpl::variable<int> _newChargeStars = 0;
rpl::variable<PeerId> _paidReactionShownPeer = false;
std::vector<Fn<void()>> _callbacks;
bool _paidReactionShownPeerLoaded = false;

View file

@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/shadow.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/wrap/slide_wrap.h"
@ -42,6 +44,10 @@ namespace {
constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;
constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;
constexpr auto kGetPercent = 85;
constexpr auto kStarsMin = 10;
constexpr auto kStarsMax = 9000;
constexpr auto kDefaultChargeStars = 200;
using Exceptions = Api::UserPrivacy::Exceptions;
@ -452,6 +458,109 @@ auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeChargeStarsSlider(
QWidget *parent,
not_null<const style::MediaSlider*> sliderStyle,
not_null<const style::FlatLabel*> labelStyle,
int valuesCount,
Fn<int(int)> valueByIndex,
int value,
Fn<void(int)> valueProgress,
Fn<void(int)> valueFinished) {
auto result = object_ptr<Ui::VerticalLayout>(parent);
const auto raw = result.data();
const auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));
const auto min = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(kStarsMin),
*labelStyle);
const auto max = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(kStarsMax),
*labelStyle);
const auto current = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(value),
*labelStyle);
min->setTextColorOverride(st::windowSubTextFg->c);
max->setTextColorOverride(st::windowSubTextFg->c);
const auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(
raw,
*sliderStyle));
labels->resize(
labels->width(),
current->height() + st::defaultVerticalListSkip);
struct State {
int indexMin = 0;
int index = 0;
};
const auto state = raw->lifetime().make_state<State>();
const auto updateByIndex = [=] {
const auto outer = labels->width();
const auto minWidth = min->width();
const auto maxWidth = max->width();
const auto currentWidth = current->width();
if (minWidth + maxWidth + currentWidth > outer) {
return;
}
min->moveToLeft(0, 0, outer);
max->moveToRight(0, 0, outer);
current->moveToLeft((outer - current->width()) / 2, 0, outer);
};
const auto updateByValue = [=](int value) {
current->setText(
tr::lng_action_gift_for_stars(tr::now, lt_count, value));
state->index = 0;
auto maxIndex = valuesCount - 1;
while (state->index < maxIndex) {
const auto mid = (state->index + maxIndex) / 2;
const auto midValue = valueByIndex(mid);
if (midValue == value) {
state->index = mid;
break;
} else if (midValue < value) {
state->index = mid + 1;
} else {
maxIndex = mid - 1;
}
}
updateByIndex();
};
const auto progress = [=](int value) {
updateByValue(value);
valueProgress(value);
};
const auto finished = [=](int value) {
updateByValue(value);
valueFinished(value);
};
style::PaletteChanged() | rpl::start_with_next([=] {
min->setTextColorOverride(st::windowSubTextFg->c);
max->setTextColorOverride(st::windowSubTextFg->c);
}, raw->lifetime());
updateByValue(value);
state->indexMin = 0;
slider->setPseudoDiscrete(
valuesCount,
valueByIndex,
value,
progress,
finished,
state->indexMin);
slider->resize(slider->width(), sliderStyle->seekSize.height());
raw->widthValue() | rpl::start_with_next([=](int width) {
labels->resizeToWidth(width);
updateByIndex();
}, slider->lifetime());
return result;
}
} // namespace
bool EditPrivacyController::hasOption(Option option) const {
@ -812,6 +921,7 @@ void EditMessagesPrivacyBox(
constexpr auto kOptionAll = 0;
constexpr auto kOptionPremium = 1;
constexpr auto kOptionCharge = 2;
const auto allowed = [=] {
return controller->session().premium()
@ -824,7 +934,11 @@ void EditMessagesPrivacyBox(
Ui::AddSkip(inner, st::messagePrivacyTopSkip);
Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle());
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
privacy->newRequirePremiumCurrent() ? kOptionPremium : kOptionAll);
(privacy->newRequirePremiumCurrent()
? kOptionPremium
: privacy->newChargeStarsCurrent()
? kOptionCharge
: kOptionAll));
inner->add(
object_ptr<Ui::Radiobutton>(
inner,
@ -846,6 +960,107 @@ void EditMessagesPrivacyBox(
0,
st::messagePrivacyBottomSkip));
Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
const auto charged = inner->add(
object_ptr<Ui::Radiobutton>(
inner,
group,
kOptionCharge,
tr::lng_messages_privacy_charge(tr::now),
st::messagePrivacyCheck),
st::settingsSendTypePadding + style::margins(
0,
st::messagePrivacyBottomSkip,
0,
st::messagePrivacyBottomSkip));
Ui::AddDividerText(inner, tr::lng_messages_privacy_charge_about());
const auto chargeWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto chargeInner = chargeWrap->entity();
Ui::AddSkip(chargeInner);
Ui::AddSubsectionTitle(chargeInner, tr::lng_messages_privacy_price());
struct State {
rpl::variable<int> stars;
};
const auto state = std::make_shared<State>();
const auto savedValue = privacy->newChargeStarsCurrent();
const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars;
state->stars = chargeStars;
auto values = std::vector<int>();
if (chargeStars < kStarsMin) {
values.push_back(chargeStars);
}
for (auto i = kStarsMin; i < 100; ++i) {
values.push_back(i);
}
for (auto i = 100; i < 1000; i += 10) {
if (i < chargeStars + 10 && chargeStars < i) {
values.push_back(chargeStars);
}
values.push_back(i);
}
for (auto i = 1000; i < kStarsMax + 1; i += 100) {
if (i < chargeStars + 100 && chargeStars < i) {
values.push_back(chargeStars);
}
values.push_back(i);
}
const auto valuesCount = int(values.size());
const auto setStars = [=](int value) {
state->stars = value;
};
chargeInner->add(
MakeChargeStarsSlider(
chargeInner,
&st::settingsScale,
&st::settingsScaleLabel,
valuesCount,
[=](int index) { return values[index]; },
chargeStars,
setStars,
setStars),
st::boxRowPadding);
Ui::AddSkip(chargeInner);
auto dollars = state->stars.value() | rpl::map([=](int stars) {
const auto ratio = controller->session().appConfig().get<float64>(
u"stars_usd_withdraw_rate_x1000"_q,
1200);
const auto dollars = int(base::SafeRound(stars * (ratio / 1000.)));
return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q);
});
Ui::AddDividerText(
chargeInner,
tr::lng_messages_privacy_price_about(
lt_percent,
rpl::single(QString::number(kGetPercent) + '%'),
lt_amount,
std::move(dollars)));
Ui::AddSkip(chargeInner);
Ui::AddSubsectionTitle(
chargeInner,
tr::lng_messages_privacy_exceptions());
const auto exceptions = Settings::AddButtonWithLabel(
chargeInner,
tr::lng_messages_privacy_remove_fee(),
rpl::single(u""_q),
st::settingsButtonNoIcon);
Ui::AddSkip(chargeInner);
Ui::AddDividerText(chargeInner, tr::lng_messages_privacy_remove_about());
using namespace rpl::mappers;
chargeWrap->toggleOn(group->value() | rpl::map(_1 == kOptionCharge));
chargeWrap->finishAnimating();
using WeakToast = base::weak_ptr<Ui::Toast::Instance>;
const auto toast = std::make_shared<WeakToast>();
const auto showToast = [=] {
@ -875,19 +1090,18 @@ void EditMessagesPrivacyBox(
}),
});
};
if (!allowed()) {
CreateRadiobuttonLock(restricted, st::messagePrivacyCheck);
CreateRadiobuttonLock(charged, st::messagePrivacyCheck);
group->setChangedCallback([=](int value) {
if (value == kOptionPremium) {
if (value == kOptionPremium || value == kOptionCharge) {
group->setValue(kOptionAll);
showToast();
}
});
}
Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
if (!allowed()) {
Ui::AddSkip(inner);
Settings::AddButtonWithIcon(
inner,
@ -907,8 +1121,12 @@ void EditMessagesPrivacyBox(
} else {
box->addButton(tr::lng_settings_save(), [=] {
if (allowed()) {
privacy->updateNewRequirePremium(
group->current() == kOptionPremium);
const auto value = group->current();
const auto premiumRequired = (value == kOptionPremium);
const auto chargeStars = (value == kOptionCharge)
? state->stars.current()
: 0;
privacy->updateMessagesPrivacy(premiumRequired, chargeStars);
box->closeBox();
} else {
showToast();

View file

@ -610,7 +610,6 @@ void UserData::setCallsStatus(CallsStatus callsStatus) {
}
}
Data::Birthday UserData::birthday() const {
return _birthday;
}

View file

@ -630,7 +630,7 @@ void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
});
}, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck);
if (_inner->peer()->canManageGifts() && _inner->peer()->isChannel()) {
if (_inner->peer()->canManageGifts()) {
addAction({ .isSeparator = true });
addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] {

View file

@ -535,6 +535,7 @@ inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey;
inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey;
inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey;
inputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey;
inputPrivacyKeyNoPaidMessages#bdc597b4 = InputPrivacyKey;
privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey;
privacyKeyChatInvite#500e6dfa = PrivacyKey;

View file

@ -356,10 +356,16 @@ void AddMessagesPrivacyButton(
not_null<Ui::VerticalLayout*> container) {
const auto session = &controller->session();
const auto privacy = &session->api().globalPrivacy();
auto label = rpl::conditional(
auto label = rpl::combine(
privacy->newRequirePremium(),
tr::lng_edit_privacy_contacts_and_premium(),
tr::lng_edit_privacy_everyone());
privacy->newChargeStars()
) | rpl::map([=](bool requirePremium, int chargeStars) {
return chargeStars
? tr::lng_edit_privacy_paid()
: requirePremium
? tr::lng_edit_privacy_contacts_and_premium()
: tr::lng_edit_privacy_everyone();
}) | rpl::flatten_latest();
const auto &st = st::settingsButtonNoIcon;
const auto button = AddButtonWithLabel(
container,