/* 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 "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" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/widgets/labels.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/box_content_divider.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_values.h" #include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_info_box.h" #include "settings/settings_power_saving.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "main/main_session.h" #include "apiwrap.h" #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" #include "styles/style_settings.h" namespace { constexpr auto kSlowmodeValues = 7; constexpr auto kBoostsUnrestrictValues = 5; constexpr auto kSuggestGigagroupThreshold = 199000; constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000); [[nodiscard]] auto Dependencies(PowerSaving::Flags) -> std::vector> { return {}; } [[nodiscard]] auto NestedRestrictionLabelsList( Data::RestrictionsSetOptions options) -> std::vector> { using Flag = ChatRestriction; auto first = std::vector{ { Flag::SendOther, tr::lng_rights_chat_send_text(tr::now) }, }; auto media = std::vector{ { Flag::SendPhotos, tr::lng_rights_chat_photos(tr::now) }, { Flag::SendVideos, tr::lng_rights_chat_videos(tr::now) }, { Flag::SendVideoMessages, tr::lng_rights_chat_video_messages(tr::now) }, { Flag::SendMusic, tr::lng_rights_chat_music(tr::now) }, { Flag::SendVoiceMessages, tr::lng_rights_chat_voice_messages(tr::now) }, { Flag::SendFiles, tr::lng_rights_chat_files(tr::now) }, { Flag::SendStickers | Flag::SendGifs | Flag::SendGames | Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) }, { Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) }, { Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) }, }; auto second = std::vector{ { Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) }, { Flag::CreateTopics, tr::lng_rights_group_add_topics(tr::now) }, { Flag::PinMessages, tr::lng_rights_group_pin(tr::now) }, { Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) }, }; if (!options.isForum) { second.erase( ranges::remove( second, Flag::CreateTopics | Flag(), &RestrictionLabel::flags), end(second)); } return { { std::nullopt, std::move(first) }, { tr::lng_rights_chat_send_media(), std::move(media) }, { std::nullopt, std::move(second) }, }; } [[nodiscard]] auto NestedAdminRightLabels( Data::AdminRightsSetOptions options) -> std::vector> { using Flag = ChatAdminRight; if (options.isGroup) { auto first = std::vector{ { Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) }, { Flag::DeleteMessages, tr::lng_rights_group_delete(tr::now) }, { Flag::BanUsers, tr::lng_rights_group_ban(tr::now) }, { Flag::InviteByLinkOrAdd, options.anyoneCanAddMembers ? tr::lng_rights_group_invite_link(tr::now) : tr::lng_rights_group_invite(tr::now) }, { Flag::ManageTopics, tr::lng_rights_group_topics(tr::now) }, { Flag::PinMessages, tr::lng_rights_group_pin(tr::now) }, }; auto stories = std::vector{ { Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) }, { Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) }, { Flag::DeleteStories, tr::lng_rights_channel_delete_stories(tr::now) }, }; auto second = std::vector{ { Flag::ManageCall, tr::lng_rights_group_manage_calls(tr::now) }, { Flag::Anonymous, tr::lng_rights_group_anonymous(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, }; if (!options.isForum) { first.erase( ranges::remove( first, Flag::ManageTopics | Flag(), &AdminRightLabel::flags), end(first)); } return { { std::nullopt, std::move(first) }, { tr::lng_rights_channel_manage_stories(), std::move(stories) }, { std::nullopt, std::move(second) }, }; } auto first = std::vector{ { Flag::ChangeInfo, tr::lng_rights_channel_info(tr::now) }, }; auto messages = std::vector{ { Flag::PostMessages, tr::lng_rights_channel_post(tr::now) }, { Flag::EditMessages, tr::lng_rights_channel_edit(tr::now) }, { Flag::DeleteMessages, tr::lng_rights_channel_delete(tr::now) }, }; auto stories = std::vector{ { Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) }, { Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) }, { Flag::DeleteStories, tr::lng_rights_channel_delete_stories(tr::now) }, }; auto second = std::vector{ { Flag::InviteByLinkOrAdd, tr::lng_rights_group_invite(tr::now) }, { Flag::ManageCall, tr::lng_rights_channel_manage_calls(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, }; return { { std::nullopt, std::move(first) }, { tr::lng_rights_channel_manage(), std::move(messages) }, { tr::lng_rights_channel_manage_stories(), std::move(stories) }, { std::nullopt, std::move(second) }, }; } int SlowmodeDelayByIndex(int index) { Expects(index >= 0 && index < kSlowmodeValues); switch (index) { case 0: return 0; case 1: return 10; case 2: return 30; case 3: return 60; case 4: return 5 * 60; case 5: return 15 * 60; case 6: return 60 * 60; } Unexpected("Index in SlowmodeDelayByIndex."); } [[nodiscard]] int BoostsUnrestrictByIndex(int index) { return index + 1; } template void ApplyDependencies( const CheckboxesMap &checkboxes, const DependenciesMap &dependencies, Ui::AbstractCheckView *changed) { const auto checkAndApply = [&]( auto &¤t, auto dependency, bool isChecked) { for (auto &&checkbox : checkboxes) { if ((checkbox.first & dependency) && (checkbox.second->checked() == isChecked)) { current->setChecked(isChecked, anim::type::normal); return true; } } return false; }; const auto applySomeDependency = [&] { auto result = false; for (auto &&entry : checkboxes) { if (entry.second == changed) { continue; } auto isChecked = entry.second->checked(); for (auto &&dependency : dependencies) { const auto check = isChecked ? dependency.first : dependency.second; if (entry.first & check) { if (checkAndApply( entry.second, (isChecked ? dependency.second : dependency.first), !isChecked)) { result = true; break; } } } } return result; }; const auto maxFixesCount = int(checkboxes.size()); for (auto i = 0; i != maxFixesCount; ++i) { if (!applySomeDependency()) { break; } }; } auto Dependencies(ChatRestrictions) -> std::vector> { using Flag = ChatRestriction; return { // stickers <-> gifs { Flag::SendGifs, Flag::SendStickers }, { Flag::SendStickers, Flag::SendGifs }, // stickers <-> games { Flag::SendGames, Flag::SendStickers }, { Flag::SendStickers, Flag::SendGames }, // stickers <-> inline { Flag::SendInline, Flag::SendStickers }, { Flag::SendStickers, Flag::SendInline }, // embed_links -> send_plain { Flag::EmbedLinks, Flag::SendOther }, // send_* -> view_messages { Flag::SendStickers, Flag::ViewMessages }, { Flag::SendGifs, Flag::ViewMessages }, { Flag::SendGames, Flag::ViewMessages }, { Flag::SendInline, Flag::ViewMessages }, { Flag::SendPolls, Flag::ViewMessages }, { Flag::SendPhotos, Flag::ViewMessages }, { Flag::SendVideos, Flag::ViewMessages }, { Flag::SendVideoMessages, Flag::ViewMessages }, { Flag::SendMusic, Flag::ViewMessages }, { Flag::SendVoiceMessages, Flag::ViewMessages }, { Flag::SendFiles, Flag::ViewMessages }, { Flag::SendOther, Flag::ViewMessages }, }; } ChatRestrictions NegateRestrictions(ChatRestrictions value) { using Flag = ChatRestriction; return (~value) & (Flag(0) // view_messages is always allowed, so it is never in restrictions. //| Flag::ViewMessages | Flag::ChangeInfo | Flag::EmbedLinks | Flag::AddParticipants | Flag::CreateTopics | Flag::PinMessages | Flag::SendGames | Flag::SendGifs | Flag::SendInline | Flag::SendPolls | Flag::SendStickers | Flag::SendPhotos | Flag::SendVideos | Flag::SendVideoMessages | Flag::SendMusic | Flag::SendVoiceMessages | Flag::SendFiles | Flag::SendOther); } auto Dependencies(ChatAdminRights) -> std::vector> { return {}; } auto ToPositiveNumberString() { return rpl::map([](int count) { return count ? QString::number(count) : QString(); }); } ChatRestrictions DisabledByAdminRights(not_null peer) { using Flag = ChatRestriction; using Admin = ChatAdminRight; using Admins = ChatAdminRights; const auto adminRights = [&] { const auto full = ~Admins(0); if (const auto chat = peer->asChat()) { return chat->amCreator() ? full : chat->adminRights(); } else if (const auto channel = peer->asChannel()) { return channel->amCreator() ? full : channel->adminRights(); } Unexpected("User in DisabledByAdminRights."); }(); return Flag(0) | ((adminRights & Admin::ManageTopics) ? Flag(0) : Flag::CreateTopics) | ((adminRights & Admin::PinMessages) ? Flag(0) : Flag::PinMessages) | ((adminRights & Admin::InviteByLinkOrAdd) ? Flag(0) : Flag::AddParticipants) | ((adminRights & Admin::ChangeInfo) ? Flag(0) : Flag::ChangeInfo); } not_null AddInnerToggle( not_null container, const style::SettingsButton &st, std::vector> innerCheckViews, not_null*> wrap, rpl::producer buttonLabel, std::optional locked, Settings::IconDescriptor &&icon) { const auto button = container->add(object_ptr( container, nullptr, st)); if (icon) { Settings::AddButtonIcon(button, st, std::move(icon)); } const auto toggleButton = Ui::CreateChild( container.get(), nullptr, st); struct State final { State(const style::Toggle &st, Fn c) : checkView(st, false, c) { } Ui::ToggleView checkView; Ui::Animations::Simple animation; rpl::event_stream<> anyChanges; std::vector> innerChecks; }; const auto state = button->lifetime().make_state( st.toggle, [=] { toggleButton->update(); }); state->innerChecks = std::move(innerCheckViews); const auto countChecked = [=] { return ranges::count_if( state->innerChecks, [](const auto &v) { return v->checked(); }); }; for (const auto &innerCheck : state->innerChecks) { innerCheck->checkedChanges( ) | rpl::to_empty | rpl::start_to_stream( state->anyChanges, button->lifetime()); } const auto checkView = &state->checkView; { const auto separator = Ui::CreateChild(container.get()); separator->paintRequest( ) | rpl::start_with_next([=, bg = st.textBgOver] { auto p = QPainter(separator); p.fillRect(separator->rect(), bg); }, separator->lifetime()); const auto separatorHeight = 2 * st.toggle.border + st.toggle.diameter; button->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { const auto w = st::rightsButtonToggleWidth; toggleButton->setGeometry( r.x() + r.width() - w, r.y(), w, r.height()); separator->setGeometry( toggleButton->x() - st::lineWidth, r.y() + (r.height() - separatorHeight) / 2, st::lineWidth, separatorHeight); }, toggleButton->lifetime()); const auto checkWidget = Ui::CreateChild(toggleButton); checkWidget->resize(checkView->getSize()); checkWidget->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(checkWidget); checkView->paint(p, 0, 0, checkWidget->width()); }, checkWidget->lifetime()); toggleButton->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { checkWidget->moveToRight( st.toggleSkip, (s.height() - checkWidget->height()) / 2); }, toggleButton->lifetime()); } state->anyChanges.events_starting_with( rpl::empty_value() ) | rpl::map(countChecked) | rpl::start_with_next([=](int count) { checkView->setChecked(count > 0, anim::type::normal); }, toggleButton->lifetime()); checkView->setLocked(locked.has_value()); checkView->finishAnimating(); const auto totalInnerChecks = state->innerChecks.size(); const auto label = Ui::CreateChild( button, rpl::combine( std::move(buttonLabel), state->anyChanges.events_starting_with( rpl::empty_value() ) | rpl::map(countChecked) ) | rpl::map([=](const QString &t, int checked) { auto count = Ui::Text::Bold(" " + QString::number(checked) + '/' + QString::number(totalInnerChecks)); return TextWithEntities::Simple(t).append(std::move(count)); })); label->setAttribute(Qt::WA_TransparentForMouseEvents); const auto arrow = Ui::CreateChild(button); { const auto &icon = st::permissionsExpandIcon; arrow->resize(icon.size()); arrow->paintRequest( ) | rpl::start_with_next([=, &icon] { auto p = QPainter(arrow); const auto center = QPointF( icon.width() / 2., icon.height() / 2.); const auto progress = state->animation.value( wrap->toggled() ? 1. : 0.); auto hq = std::optional(); if (progress > 0.) { hq.emplace(p); p.translate(center); p.rotate(progress * 180.); p.translate(-center); } icon.paint(p, 0, 0, arrow->width()); }, arrow->lifetime()); } button->sizeValue( ) | rpl::start_with_next([=, &st](const QSize &s) { const auto labelLeft = st.padding.left(); const auto labelRight = s.width() - toggleButton->width(); label->resizeToWidth(labelRight - labelLeft - arrow->width()); label->moveToLeft( labelLeft, (s.height() - label->height()) / 2); arrow->moveToLeft( std::min( labelLeft + label->textMaxWidth(), labelRight - arrow->width()), (s.height() - arrow->height()) / 2); }, button->lifetime()); wrap->toggledValue( ) | rpl::skip(1) | rpl::start_with_next([=](bool toggled) { state->animation.start( [=] { arrow->update(); }, toggled ? 0. : 1., toggled ? 1. : 0., st::slideWrapDuration); }, button->lifetime()); const auto handleLocked = [=] { if (locked.has_value()) { Ui::Toast::Show(container, *locked); return true; } return false; }; button->clicks( ) | rpl::start_with_next([=] { if (!handleLocked()) { wrap->toggle(!wrap->toggled(), anim::type::normal); } }, button->lifetime()); toggleButton->clicks( ) | rpl::start_with_next([=] { if (!handleLocked()) { const auto checked = !checkView->checked(); for (const auto &innerCheck : state->innerChecks) { innerCheck->setChecked(checked, anim::type::normal); } } }, toggleButton->lifetime()); return button; } template [[nodiscard]] EditFlagsControl CreateEditFlags( not_null container, Flags checked, EditFlagsDescriptor &&descriptor) { struct State final { std::map> checkViews; rpl::event_stream<> anyChanges; rpl::variable forceDisabledMessage; rpl::variable forceDisabled; base::flat_map realCheckedValues; base::weak_ptr toast; }; const auto state = container->lifetime().make_state(); if (descriptor.forceDisabledMessage) { state->forceDisabledMessage = std::move( descriptor.forceDisabledMessage); state->forceDisabled = state->forceDisabledMessage.value( ) | rpl::map([=](const QString &message) { return !message.isEmpty(); }); state->forceDisabled.value( ) | rpl::start_with_next([=](bool disabled) { if (disabled) { for (const auto &[flags, checkView] : state->checkViews) { checkView->setChecked(false, anim::type::normal); } } else { for (const auto &[flags, checkView] : state->checkViews) { if (const auto i = state->realCheckedValues.find(flags) ; i != state->realCheckedValues.end()) { checkView->setChecked( i->second, anim::type::normal); } } } }, container->lifetime()); } const auto &st = descriptor.st ? *descriptor.st : st::rightsButton; const auto value = [=] { auto result = Flags(0); for (const auto &[flags, checkView] : state->checkViews) { if (checkView->checked()) { result |= flags; } else { result &= ~flags; } } return result; }; const auto applyDependencies = [=](Ui::AbstractCheckView *view) { static const auto dependencies = Dependencies(Flags()); ApplyDependencies(state->checkViews, dependencies, view); }; if (descriptor.header) { container->add( object_ptr( container, std::move(descriptor.header), st::rightsHeaderLabel), st::rightsHeaderMargin); } const auto addCheckbox = [&]( not_null verticalLayout, bool isInner, const EditFlagsLabel &entry) { const auto flags = entry.flags; const auto lockedIt = ranges::find_if( descriptor.disabledMessages, [&](const auto &pair) { return (pair.first & flags) != 0; }); const auto locked = (lockedIt != end(descriptor.disabledMessages)) ? std::make_optional(lockedIt->second) : std::nullopt; const auto realChecked = (checked & flags) != 0; state->realCheckedValues.emplace(flags, realChecked); const auto toggled = realChecked && !state->forceDisabled.current(); const auto checkView = [&]() -> not_null { if (isInner) { const auto checkbox = verticalLayout->add( object_ptr( verticalLayout, entry.label, toggled, st::settingsCheckbox), st.padding); const auto button = Ui::CreateChild( verticalLayout.get(), st::defaultRippleAnimation); button->stackUnder(checkbox); rpl::combine( verticalLayout->widthValue(), checkbox->geometryValue() ) | rpl::start_with_next([=](int w, const QRect &r) { button->setGeometry(0, r.y(), w, r.height()); }, button->lifetime()); checkbox->setAttribute(Qt::WA_TransparentForMouseEvents); const auto checkView = checkbox->checkView(); button->setClickedCallback([=] { checkView->setChecked( !checkView->checked(), anim::type::normal); }); return checkView; } else { const auto button = Settings::AddButtonWithIcon( verticalLayout, rpl::single(entry.label), st, { entry.icon }); const auto toggle = Ui::CreateChild( button.get()); // Looks like a bug in Clang, fails to compile with 'auto&' below. rpl::lifetime &lifetime = toggle->lifetime(); const auto checkView = lifetime.make_state( st.toggle, toggled, [=] { toggle->update(); }); toggle->resize(checkView->getSize()); toggle->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(toggle); checkView->paint(p, 0, 0, toggle->width()); }, toggle->lifetime()); button->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { toggle->moveToRight( st.toggleSkip, (s.height() - toggle->height()) / 2); }, toggle->lifetime()); button->setClickedCallback([=] { checkView->setChecked( !checkView->checked(), anim::type::normal); }); checkView->setLocked(locked.has_value()); return checkView; } }(); state->checkViews.emplace(flags, checkView); checkView->checkedChanges( ) | rpl::start_with_next([=](bool checked) { if (checked && state->forceDisabled.current()) { if (!state->toast) { state->toast = Ui::Toast::Show(container, { .text = { state->forceDisabledMessage.current() }, .duration = kForceDisableTooltipDuration, }); } checkView->setChecked(false, anim::type::instant); } else if (locked.has_value()) { if (checked != toggled) { if (!state->toast) { state->toast = Ui::Toast::Show(container, { .text = { *locked }, .duration = kForceDisableTooltipDuration, }); } checkView->setChecked(toggled, anim::type::instant); } } else { if (!state->forceDisabled.current()) { state->realCheckedValues[flags] = checked; } InvokeQueued(container, [=] { applyDependencies(checkView); state->anyChanges.fire({}); }); } }, verticalLayout->lifetime()); return checkView; }; for (const auto &nestedWithLabel : descriptor.labels) { Assert(!nestedWithLabel.nested.empty()); const auto isInner = nestedWithLabel.nestingLabel.has_value(); auto wrap = isInner ? object_ptr>( container, object_ptr(container)) : object_ptr>{ nullptr }; const auto verticalLayout = wrap ? wrap->entity() : container.get(); auto innerChecks = std::vector>(); for (const auto &entry : nestedWithLabel.nested) { const auto c = addCheckbox(verticalLayout, isInner, entry); if (isInner) { innerChecks.push_back(c); } } if (wrap) { const auto raw = wrap.data(); raw->hide(anim::type::instant); AddInnerToggle( container, st, innerChecks, raw, *nestedWithLabel.nestingLabel, std::nullopt, { nestedWithLabel.nested.front().icon }); container->add(std::move(wrap)); container->widthValue( ) | rpl::start_with_next([=](int w) { raw->resizeToWidth(w); }, raw->lifetime()); } } applyDependencies(nullptr); for (const auto &[flags, checkView] : state->checkViews) { checkView->finishAnimating(); } return { nullptr, value, state->anyChanges.events() | rpl::map(value) }; } void AddSlowmodeLabels( not_null container) { const auto labels = container->add( object_ptr(container, st::normalFont->height), st::slowmodeLabelsMargin); for (auto i = 0; i != kSlowmodeValues; ++i) { const auto seconds = SlowmodeDelayByIndex(i); const auto label = Ui::CreateChild( labels, st::slowmodeLabel, (!seconds ? tr::lng_rights_slowmode_off(tr::now) : (seconds < 60) ? tr::lng_seconds_tiny(tr::now, lt_count, seconds) : (seconds < 3600) ? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60) : tr::lng_hours_tiny(tr::now, lt_count,seconds / 3600))); 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 == kSlowmodeValues) ? (size.width() - (size.width() / 2) - inner) : (-inner / 2); const auto left = skip.left() + (size.width() / 2) + (i * available) / (kSlowmodeValues - 1) + shift; label->moveToLeft(left, 0, outer); }, label->lifetime()); } } rpl::producer AddSlowmodeSlider( 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 secondsCount = lifetime.make_state>( channel ? channel->slowmodeSeconds() : 0); container->add( object_ptr( container, tr::lng_rights_slowmode_header(), st::rightsHeaderLabel), st::rightsHeaderMargin); AddSlowmodeLabels(container); const auto slider = container->add( object_ptr(container, st::localStorageLimitSlider), st::localStorageLimitMargin); slider->resize(st::localStorageLimitSlider.seekSize); slider->setPseudoDiscrete( kSlowmodeValues, SlowmodeDelayByIndex, secondsCount->current(), [=](int seconds) { (*secondsCount) = seconds; }); auto hasSlowMode = secondsCount->value( ) | rpl::map( _1 != 0 ) | rpl::distinct_until_changed(); auto useSeconds = secondsCount->value( ) | rpl::map( _1 < 60 ) | rpl::distinct_until_changed(); auto interval = rpl::combine( std::move(useSeconds), tr::lng_rights_slowmode_interval_seconds( lt_count, secondsCount->value() | tr::to_count()), tr::lng_rights_slowmode_interval_minutes( lt_count, secondsCount->value() | rpl::map(_1 / 60.)) ) | rpl::map([]( bool use, const QString &seconds, const QString &minutes) { return use ? seconds : minutes; }); auto aboutText = rpl::combine( std::move(hasSlowMode), tr::lng_rights_slowmode_about(), tr::lng_rights_slowmode_about_interval( lt_interval, std::move(interval)) ) | rpl::map([]( bool has, const QString &about, const QString &aboutInterval) { return has ? aboutInterval : about; }); container->add( object_ptr( container, object_ptr( container, std::move(aboutText), st::boxDividerLabel), st::proxyAboutPadding), style::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip)); 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 ? many : one).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( not_null container, Fn callback) { container->add( object_ptr( container, tr::lng_rights_gigagroup_title(), st::rightsHeaderLabel), st::rightsHeaderMargin); container->add(EditPeerInfoBox::CreateButton( container, tr::lng_rights_gigagroup_convert(), rpl::single(QString()), std::move(callback), st::manageGroupTopicsButton, { &st::menuIconChatDiscuss })); container->add( object_ptr( container, object_ptr( container, tr::lng_rights_gigagroup_about(), st::boxDividerLabel), st::proxyAboutPadding), style::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip)); } void AddBannedButtons( not_null container, not_null navigation, not_null peer) { if (const auto chat = peer->asChat()) { if (!chat->amCreator()) { return; } } const auto channel = peer->asChannel(); container->add(EditPeerInfoBox::CreateButton( container, tr::lng_manage_peer_exceptions(), (channel ? Info::Profile::RestrictedCountValue(channel) : rpl::single(0)) | ToPositiveNumberString(), [=] { ParticipantsBoxController::Start( navigation, peer, ParticipantsBoxController::Role::Restricted); }, st::manageGroupTopicsButton, { &st::menuIconPermissions })); if (channel) { container->add(EditPeerInfoBox::CreateButton( container, tr::lng_manage_peer_removed_users(), Info::Profile::KickedCountValue(channel) | ToPositiveNumberString(), [=] { ParticipantsBoxController::Start( navigation, peer, ParticipantsBoxController::Role::Kicked); }, st::manageGroupTopicsButton, { &st::menuIconRemove })); } } } // namespace void ShowEditPeerPermissionsBox( not_null box, not_null navigation, not_null channelOrGroup, Fn done) { const auto peer = channelOrGroup->migrateToOrMe(); box->setTitle(tr::lng_manage_peer_permissions()); const auto inner = box->verticalLayout(); using Flag = ChatRestriction; using Flags = ChatRestrictions; const auto disabledByAdminRights = DisabledByAdminRights(peer); const auto restrictions = FixDependentRestrictions([&] { if (const auto chat = peer->asChat()) { return chat->defaultRestrictions() | disabledByAdminRights; } else if (const auto channel = peer->asChannel()) { return channel->defaultRestrictions() | (channel->isPublic() ? (Flag::ChangeInfo | Flag::PinMessages) : Flags(0)) | disabledByAdminRights; } Unexpected("User in EditPeerPermissionsBox."); }()); const auto disabledMessages = [&] { auto result = base::flat_map(); result.emplace( disabledByAdminRights, tr::lng_rights_permission_cant_edit(tr::now)); if (const auto channel = peer->asChannel()) { if (channel->isPublic() || (channel->isMegagroup() && channel->linkedChat())) { result.emplace( Flag::ChangeInfo | Flag::PinMessages, tr::lng_rights_permission_unavailable(tr::now)); } } return result; }(); auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( inner, tr::lng_rights_default_restrictions_header(), restrictions, disabledMessages, { .isForum = peer->isForum() }); inner->add(std::move(checkboxes)); 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() && channel->membersCount() >= kSuggestGigagroupThreshold) { AddSuggestGigagroup( inner, AboutGigagroupCallback( peer->asChannel(), navigation->parentController())); } } AddBannedButtons(inner, navigation, peer); box->addButton(tr::lng_settings_save(), [=, rights = getRestrictions] { 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(); }); box->setWidth(st::boxWideWidth); } Fn AboutGigagroupCallback( not_null channel, not_null controller) { const auto weak = base::make_weak(controller); const auto converting = std::make_shared(); const auto convertSure = [=] { if (*converting) { return; } *converting = true; channel->session().api().request(MTPchannels_ConvertToGigagroup( channel->inputChannel )).done([=](const MTPUpdates &result) { channel->session().api().applyUpdates(result); if (const auto strong = weak.get()) { strong->window().hideSettingsAndLayer(); strong->showToast(tr::lng_gigagroup_done(tr::now)); } }).fail([=] { *converting = false; }).send(); }; const auto convertWarn = [=] { const auto strong = weak.get(); if (*converting || !strong) { return; } strong->show(Box([=](not_null box) { box->setTitle(tr::lng_gigagroup_warning_title()); box->addRow( object_ptr( box, tr::lng_gigagroup_warning( ) | Ui::Text::ToRichLangValue(), st::infoAboutGigagroup)); box->addButton(tr::lng_gigagroup_convert_sure(), convertSure); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); })); }; return [=] { const auto strong = weak.get(); if (*converting || !strong) { return; } strong->show(Box([=](not_null box) { box->setTitle(tr::lng_gigagroup_convert_title()); const auto addFeature = [&](rpl::producer text) { using namespace rpl::mappers; const auto prefix = QString::fromUtf8("\xE2\x80\xA2 "); box->addRow( object_ptr( box, std::move(text) | rpl::map(prefix + _1), st::infoAboutGigagroup), style::margins( st::boxRowPadding.left(), st::boxLittleSkip, st::boxRowPadding.right(), st::boxLittleSkip)); }; addFeature(tr::lng_gigagroup_convert_feature1()); addFeature(tr::lng_gigagroup_convert_feature2()); addFeature(tr::lng_gigagroup_convert_feature3()); box->addButton(tr::lng_gigagroup_convert_sure(), convertWarn); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); })); }; } std::vector RestrictionLabels( Data::RestrictionsSetOptions options) { auto result = std::vector(); for (const auto &[_, r] : NestedRestrictionLabelsList(options)) { result.insert(result.end(), r.begin(), r.end()); } return result; } std::vector AdminRightLabels( Data::AdminRightsSetOptions options) { auto result = std::vector(); for (const auto &[_, r] : NestedAdminRightLabels(options)) { result.insert(result.end(), r.begin(), r.end()); } return result; } EditFlagsControl CreateEditRestrictions( QWidget *parent, rpl::producer header, ChatRestrictions restrictions, base::flat_map disabledMessages, Data::RestrictionsSetOptions options) { auto widget = object_ptr(parent); auto result = CreateEditFlags( widget.data(), NegateRestrictions(restrictions), { .header = std::move(header), .labels = NestedRestrictionLabelsList(options), .disabledMessages = std::move(disabledMessages), }); result.widget = std::move(widget); result.value = [original = std::move(result.value)]{ return NegateRestrictions(original()); }; result.changes = std::move( result.changes ) | rpl::map(NegateRestrictions); return result; } EditFlagsControl CreateEditAdminRights( QWidget *parent, rpl::producer header, ChatAdminRights rights, base::flat_map disabledMessages, Data::AdminRightsSetOptions options) { auto widget = object_ptr(parent); auto result = CreateEditFlags( widget.data(), rights, { .header = std::move(header), .labels = NestedAdminRightLabels(options), .disabledMessages = std::move(disabledMessages), }); result.widget = std::move(widget); return result; } ChatAdminRights DisabledByDefaultRestrictions(not_null peer) { using Flag = ChatAdminRight; using Restriction = ChatRestriction; const auto restrictions = FixDependentRestrictions([&] { if (const auto chat = peer->asChat()) { return chat->defaultRestrictions(); } else if (const auto channel = peer->asChannel()) { return channel->defaultRestrictions(); } Unexpected("User in DisabledByDefaultRestrictions."); }()); return Flag(0) | ((restrictions & Restriction::PinMessages) ? Flag(0) : Flag::PinMessages) // // We allow to edit 'invite_users' admin right no matter what // is chosen in default permissions for 'invite_users', because // if everyone can 'invite_users' it handles invite link for admins. // //| ((restrictions & Restriction::AddParticipants) // ? Flag(0) // : Flag::InviteByLinkOrAdd) // | ((restrictions & Restriction::ChangeInfo) ? Flag(0) : Flag::ChangeInfo); } ChatRestrictions FixDependentRestrictions(ChatRestrictions restrictions) { const auto &dependencies = Dependencies(restrictions); // Fix iOS bug of saving send_inline like embed_links. // We copy send_stickers to send_inline. if (restrictions & ChatRestriction::SendStickers) { restrictions |= ChatRestriction::SendInline; } else { restrictions &= ~ChatRestriction::SendInline; } // Apply the strictest. const auto fixOne = [&] { for (const auto &[first, second] : dependencies) { if ((restrictions & second) && !(restrictions & first)) { restrictions |= first; return true; } } return false; }; while (fixOne()) { } return restrictions; } ChatAdminRights AdminRightsForOwnershipTransfer( Data::AdminRightsSetOptions options) { auto result = ChatAdminRights(); for (const auto &entry : AdminRightLabels(options)) { if (!(entry.flags & ChatAdminRight::Anonymous)) { result |= entry.flags; } } return result; } EditFlagsControl CreateEditPowerSaving( QWidget *parent, PowerSaving::Flags flags, rpl::producer forceDisabledMessage) { auto widget = object_ptr(parent); auto descriptor = Settings::PowerSavingLabels(); descriptor.forceDisabledMessage = std::move(forceDisabledMessage); auto result = CreateEditFlags( widget.data(), flags, std::move(descriptor)); result.widget = std::move(widget); return result; }