diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b8fde1519..7e7679f6d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2181,6 +2181,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_channel_reached_more_group#other" = "This group reached **Level {count}** and can now {post}."; "lng_boost_channel_post_stories#one" = "post **{count} story** per day"; "lng_boost_channel_post_stories#other" = "post **{count} stories** per day"; +"lng_boost_channel_features" = "Your boosts will help {channel} to unlock new features."; +"lng_boost_group_lift_restrictions" = "Boost the group to remove messaging restrictions."; +"lng_boost_group_lift_restrictions_many#one" = "Boost the group **{count} times** to remove messaging restrictions."; +"lng_boost_group_lift_restrictions_many#other" = "Boost the group **{count} times** to remove messaging restrictions."; "lng_boost_error_gifted_title" = "Can't boost with gifted Premium!"; "lng_boost_error_gifted_text" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels."; "lng_boost_error_gifted_text_group" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost groups."; @@ -3566,6 +3570,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_slowmode_interval_seconds#other" = "every {count} seconds"; "lng_rights_slowmode_interval_minutes#one" = "every {count} minute"; "lng_rights_slowmode_interval_minutes#other" = "every {count} minutes"; +"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_slowmode_enabled"= "Slow mode is enabled. You 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."; @@ -3661,6 +3668,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_restricted_send_gifs" = "The admins of this group restricted you from posting GIFs here."; "lng_restricted_send_inline" = "The admins of this group restricted you from posting inline content here."; "lng_restricted_send_polls" = "The admins of this group restricted you from posting polls here."; +"lng_restricted_boost_group" = "Boost this group to send messages"; "lng_restricted_send_message_until" = "The admins of this group restricted you from writing here until {date}, {time}."; "lng_restricted_send_photos_until" = "The admins of this group restricted you from posting photos here until {date}, {time}."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index f43708f48..14a859e76 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -791,6 +791,9 @@ slowmodeLabelsMargin: margins(0px, 5px, 0px, 0px); slowmodeLabel: LabelSimple(defaultLabelSimple) { textFg: windowSubTextFg; } +boostsUnrestrictLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} customBadgeField: InputField(defaultInputField) { textMargins: margins(2px, 7px, 2px, 0px); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index fba0c7303..8ae5907e6 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -198,6 +198,37 @@ void SaveSlowmodeSeconds( api->registerModifyRequest(key, requestId); } +void SaveBoostsUnrestrict( + not_null channel, + int boostsUnrestrict, + Fn done) { + const auto api = &channel->session().api(); + const auto key = Api::RequestKey("boosts_unrestrict", channel->id); + const auto requestId = api->request( + MTPchannels_SetBoostsToUnblockRestrictions( + channel->inputChannel, + MTP_int(boostsUnrestrict)) + ).done([=](const MTPUpdates &result) { + api->clearModifyRequest(key); + api->applyUpdates(result); + channel->setBoostsUnrestrict( + channel->boostsApplied(), + boostsUnrestrict); + done(); + }).fail([=](const MTP::Error &error) { + api->clearModifyRequest(key); + if (error.type() != u"CHAT_NOT_MODIFIED"_q) { + return; + } + channel->setBoostsUnrestrict( + channel->boostsApplied(), + boostsUnrestrict); + done(); + }).send(); + + api->registerModifyRequest(key, requestId); +} + void ShowEditPermissions( not_null navigation, not_null peer) { @@ -215,6 +246,10 @@ void ShowEditPermissions( close); if (const auto channel = peer->asChannel()) { SaveSlowmodeSeconds(channel, result.slowmodeSeconds, close); + SaveBoostsUnrestrict( + channel, + result.boostsUnrestrict, + close); } }; auto done = [=](EditPeerPermissionsBoxResult result) { @@ -225,7 +260,8 @@ void ShowEditPermissions( const auto saveFor = peer->migrateToOrMe(); const auto chat = saveFor->asChat(); - if (!result.slowmodeSeconds || !chat) { + if (!chat + || (!result.slowmodeSeconds && !result.boostsUnrestrict)) { save(saveFor, result); return; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index f5024859c..36204506e 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -8,8 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_permissions_box.h" #include "lang/lang_keys.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_chat.h" +#include "data/data_session.h" #include "ui/effects/toggle_arrow.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -34,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" +#include "styles/style_chat.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" #include "styles/style_window.h" @@ -42,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kSlowmodeValues = 7; +constexpr auto kBoostsUnrestrictValues = 5; constexpr auto kSuggestGigagroupThreshold = 199000; constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000); @@ -163,6 +168,10 @@ int SlowmodeDelayByIndex(int index) { Unexpected("Index in SlowmodeDelayByIndex."); } +[[nodiscard]] int BoostsUnrestrictByIndex(int index) { + return index + 1; +} + template void ApplyDependencies( const CheckboxesMap &checkboxes, @@ -768,14 +777,14 @@ void AddSlowmodeLabels( } } -Fn AddSlowmodeSlider( +rpl::producer AddSlowmodeSlider( not_null container, not_null peer) { using namespace rpl::mappers; if (const auto chat = peer->asChat()) { if (!chat->amCreator()) { - return [] { return 0; }; + return rpl::single(0); } } const auto channel = peer->asChannel(); @@ -783,10 +792,6 @@ Fn AddSlowmodeSlider( const auto secondsCount = lifetime.make_state>( channel ? channel->slowmodeSeconds() : 0); - container->add( - object_ptr(container), - { 0, st::infoProfileSkip, 0, st::infoProfileSkip }); - container->add( object_ptr( container, @@ -856,7 +861,157 @@ Fn AddSlowmodeSlider( st::proxyAboutPadding), style::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip)); - return [=] { return secondsCount->current(); }; + return secondsCount->value(); +} + +void AddBoostsUnrestrictLabels( + not_null container, + not_null session) { + const auto labels = container->add( + object_ptr(container, st::normalFont->height), + st::slowmodeLabelsMargin); + const auto manager = &session->data().customEmojiManager(); + const auto one = Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + st::boostMessageIcon, + st::boostMessageIconPadding)); + const auto many = Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + st::boostsMessageIcon, + st::boostsMessageIconPadding)); + const auto context = Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + .customEmojiLoopLimit = 1, + }; + for (auto i = 0; i != kBoostsUnrestrictValues; ++i) { + const auto label = Ui::CreateChild( + labels, + st::boostsUnrestrictLabel); + label->setMarkedText( + TextWithEntities(i ? one : many).append(QString::number(i + 1)), + context); + rpl::combine( + labels->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outer, int inner) { + const auto skip = st::localStorageLimitMargin; + const auto size = st::localStorageLimitSlider.seekSize; + const auto available = outer + - skip.left() + - skip.right() + - size.width(); + const auto shift = (i == 0) + ? -(size.width() / 2) + : (i + 1 == kBoostsUnrestrictValues) + ? (size.width() - (size.width() / 2) - inner) + : (-inner / 2); + const auto left = skip.left() + + (size.width() / 2) + + (i * available) / (kBoostsUnrestrictValues - 1) + + shift; + label->moveToLeft(left, 0, outer); + }, label->lifetime()); + } +} + +rpl::producer AddBoostsUnrestrictSlider( + not_null container, + not_null peer) { + using namespace rpl::mappers; + + if (const auto chat = peer->asChat()) { + if (!chat->amCreator()) { + return rpl::single(0); + } + } + const auto channel = peer->asChannel(); + auto &lifetime = container->lifetime(); + const auto boostsUnrestrict = lifetime.make_state>( + channel ? channel->boostsUnrestrict() : 0); + + container->add( + object_ptr(container), + { 0, st::infoProfileSkip, 0, st::infoProfileSkip }); + + auto enabled = boostsUnrestrict->value( + ) | rpl::map(_1 > 0); + container->add(object_ptr( + container, + tr::lng_rights_boosts_no_restrict(), + st::defaultSettingsButton + ))->toggleOn(rpl::duplicate(enabled))->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + if (toggled && !boostsUnrestrict->current()) { + *boostsUnrestrict = 1; + } else if (!toggled && boostsUnrestrict->current()) { + *boostsUnrestrict = 0; + } + }, container->lifetime()); + + const auto outer = container->add( + object_ptr>( + container, + object_ptr(container))); + outer->toggleOn(rpl::duplicate(enabled), anim::type::normal); + outer->finishAnimating(); + + const auto inner = outer->entity(); + + AddBoostsUnrestrictLabels(inner, &peer->session()); + + const auto slider = inner->add( + object_ptr(inner, st::localStorageLimitSlider), + st::localStorageLimitMargin); + slider->resize(st::localStorageLimitSlider.seekSize); + slider->setPseudoDiscrete( + kBoostsUnrestrictValues, + BoostsUnrestrictByIndex, + boostsUnrestrict->current(), + [=](int boosts) { + (*boostsUnrestrict) = boosts; + }); + + inner->add( + object_ptr( + inner, + object_ptr( + inner, + rpl::conditional( + boostsUnrestrict->value() | rpl::map(_1 > 0), + tr::lng_rights_boosts_about_on(), + tr::lng_rights_boosts_about()), + st::boxDividerLabel), + st::proxyAboutPadding), + style::margins(0, st::infoProfileSkip, 0, 0)); + + return boostsUnrestrict->value(); +} + +rpl::producer AddBoostsUnrestrictWrapped( + not_null container, + not_null peer, + rpl::producer shown) { + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + wrap->toggleOn(rpl::duplicate(shown), anim::type::normal); + wrap->finishAnimating(); + + auto result = AddBoostsUnrestrictSlider(wrap->entity(), peer); + const auto divider = container->add( + object_ptr>( + container, + object_ptr(container), + QMargins{ 0, st::infoProfileSkip, 0, st::infoProfileSkip })); + divider->toggleOn(rpl::combine( + std::move(shown), + rpl::duplicate(result), + !rpl::mappers::_1 || !rpl::mappers::_2)); + divider->finishAnimating(); + + return result; } void AddSuggestGigagroup( @@ -983,7 +1138,40 @@ void ShowEditPeerPermissionsBox( inner->add(std::move(checkboxes)); - const auto getSlowmodeSeconds = AddSlowmodeSlider(inner, peer); + struct State { + rpl::variable slowmodeSeconds; + rpl::variable boostsUnrestrict; + rpl::variable hasSendRestrictions; + }; + static constexpr auto kSendRestrictions = Flag::EmbedLinks + | Flag::SendGames + | Flag::SendGifs + | Flag::SendInline + | Flag::SendPolls + | Flag::SendStickers + | Flag::SendPhotos + | Flag::SendVideos + | Flag::SendVideoMessages + | Flag::SendMusic + | Flag::SendVoiceMessages + | Flag::SendFiles + | Flag::SendOther; + const auto state = inner->lifetime().make_state(); + state->hasSendRestrictions = ((restrictions & kSendRestrictions) != 0) + || (peer->isChannel() && peer->asChannel()->slowmodeSeconds() > 0); + state->boostsUnrestrict = AddBoostsUnrestrictWrapped( + inner, + peer, + state->hasSendRestrictions.value()); + state->slowmodeSeconds = AddSlowmodeSlider(inner, peer); + state->hasSendRestrictions = rpl::combine( + rpl::single( + restrictions + ) | rpl::then(std::move(changes)), + state->slowmodeSeconds.value() + ) | rpl::map([](ChatRestrictions restrictions, int slowmodeSeconds) { + return ((restrictions & kSendRestrictions) != 0) || slowmodeSeconds; + }); if (const auto channel = peer->asChannel()) { if (channel->amCreator() @@ -999,7 +1187,18 @@ void ShowEditPeerPermissionsBox( AddBannedButtons(inner, navigation, peer); box->addButton(tr::lng_settings_save(), [=, rights = getRestrictions] { - done({ rights(), getSlowmodeSeconds() }); + const auto restrictions = rights(); + const auto slowmodeSeconds = state->slowmodeSeconds.current(); + const auto hasRestrictions = (slowmodeSeconds > 0) + || ((restrictions & kSendRestrictions) != 0); + const auto boostsUnrestrict = hasRestrictions + ? state->boostsUnrestrict.current() + : 0; + done({ + restrictions, + slowmodeSeconds, + boostsUnrestrict, + }); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h index c1ce0eee1..05e1962d4 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h @@ -37,6 +37,7 @@ class SessionNavigation; struct EditPeerPermissionsBoxResult final { ChatRestrictions rights; int slowmodeSeconds = 0; + int boostsUnrestrict = 0; }; void ShowEditPeerPermissionsBox( diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index e130dac18..39cd1a1ca 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -772,34 +772,74 @@ void ChannelData::setMigrateFromChat(ChatData *chat) { } int ChannelData::slowmodeSeconds() const { - return _slowmodeSeconds; + if (const auto info = mgInfo.get()) { + return info->slowmodeSeconds; + } + return 0; } void ChannelData::setSlowmodeSeconds(int seconds) { - if (_slowmodeSeconds == seconds) { + if (!mgInfo || slowmodeSeconds() == seconds) { return; } - _slowmodeSeconds = seconds; + mgInfo->slowmodeSeconds = seconds; session().changes().peerUpdated(this, UpdateFlag::Slowmode); } TimeId ChannelData::slowmodeLastMessage() const { - return (hasAdminRights() || amCreator()) ? 0 : _slowmodeLastMessage; + return (hasAdminRights() || amCreator() || !mgInfo) + ? 0 + : mgInfo->slowmodeLastMessage; } void ChannelData::growSlowmodeLastMessage(TimeId when) { + const auto info = mgInfo.get(); const auto now = base::unixtime::now(); accumulate_min(when, now); - if (_slowmodeLastMessage > now) { - _slowmodeLastMessage = when; - } else if (_slowmodeLastMessage >= when) { + if (!info) { + return; + } else if (info->slowmodeLastMessage > now) { + info->slowmodeLastMessage = when; + } else if (info->slowmodeLastMessage >= when) { return; } else { - _slowmodeLastMessage = when; + info->slowmodeLastMessage = when; } session().changes().peerUpdated(this, UpdateFlag::Slowmode); } +int ChannelData::boostsApplied() const { + if (const auto info = mgInfo.get()) { + return info->boostsApplied; + } + return 0; +} + +int ChannelData::boostsUnrestrict() const { + if (const auto info = mgInfo.get()) { + return info->boostsUnrestrict; + } + return 0; +} + +void ChannelData::setBoostsUnrestrict(int applied, int unrestrict) { + if (const auto info = mgInfo.get()) { + if (info->boostsApplied == applied + && info->boostsUnrestrict == unrestrict) { + return; + } + const auto wasUnrestricted = (info->boostsApplied > 0) + && (info->boostsApplied >= info->boostsUnrestrict); + const auto nowUnrestricted = (applied > 0) + && (applied >= unrestrict); + info->boostsApplied = applied; + info->boostsUnrestrict = unrestrict; + if (wasUnrestricted != nowUnrestricted) { + session().changes().peerUpdated(this, UpdateFlag::Rights); + } + } +} + void ChannelData::setInvitePeek(const QString &hash, TimeId expires) { if (!_invitePeek) { _invitePeek = std::make_unique(); @@ -1118,8 +1158,9 @@ void ApplyChannelUpdate( if (stickersChanged) { session->changes().peerUpdated(channel, UpdateFlag::StickersSet); } - - channel->mgInfo->boostsApplied = update.vboosts_applied().value_or_empty(); + channel->setBoostsUnrestrict( + update.vboosts_applied().value_or_empty(), + update.vboosts_unrestrict().value_or_empty()); } channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); channel->setTranslationDisabled(update.is_translations_disabled()); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 680627e0e..cd12c8a9e 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -133,6 +133,10 @@ public: mutable int lastParticipantsStatus = LastParticipantsUpToDate; int lastParticipantsCount = 0; int boostsApplied = 0; + int boostsUnrestrict = 0; + + int slowmodeSeconds = 0; + TimeId slowmodeLastMessage = 0; private: ChatData *_migratedFrom = nullptr; @@ -433,6 +437,11 @@ public: [[nodiscard]] TimeId slowmodeLastMessage() const; void growSlowmodeLastMessage(TimeId when); + [[nodiscard]] int boostsApplied() const; + [[nodiscard]] int boostsUnrestrict() const; + [[nodiscard]] bool unrestrictedByBoosts() const; + void setBoostsUnrestrict(int applied, int unrestrict); + void setInvitePeek(const QString &hash, TimeId expires); void clearInvitePeek(); [[nodiscard]] TimeId invitePeekExpires() const; @@ -520,9 +529,6 @@ private: std::unique_ptr _call; PeerId _callDefaultJoinAs = 0; - int _slowmodeSeconds = 0; - TimeId _slowmodeLastMessage = 0; - }; namespace Data {