tdesktop/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp

682 lines
18 KiB
C++

/*
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 "data/data_channel.h"
#include "data/data_chat.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/toast/toast.h"
#include "info/profile/info_profile_button.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 "window/window_session_controller.h"
#include "mainwindow.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace {
constexpr auto kSlowmodeValues = 7;
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.");
}
template <typename CheckboxesMap, typename DependenciesMap>
void ApplyDependencies(
const CheckboxesMap &checkboxes,
const DependenciesMap &dependencies,
QPointer<Ui::Checkbox> changed) {
const auto checkAndApply = [&](
auto &&current,
auto dependency,
bool isChecked) {
for (auto &&checkbox : checkboxes) {
if ((checkbox.first & dependency)
&& (checkbox.second->checked() == isChecked)) {
current->setChecked(isChecked);
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;
}
};
}
std::vector<std::pair<ChatRestrictions, QString>> RestrictionLabels() {
const auto langKeys = {
tr::lng_rights_chat_send_text,
tr::lng_rights_chat_send_media,
tr::lng_rights_chat_send_stickers,
tr::lng_rights_chat_send_links,
tr::lng_rights_chat_send_polls,
tr::lng_rights_chat_add_members,
tr::lng_rights_group_pin,
tr::lng_rights_group_info,
};
std::vector<std::pair<ChatRestrictions, QString>> vector;
const auto restrictions = Data::ListOfRestrictions();
auto i = 0;
for (const auto &key : langKeys) {
vector.emplace_back(restrictions[i++], key(tr::now));
}
return vector;
}
std::vector<std::pair<ChatAdminRights, QString>> AdminRightLabels(
bool isGroup,
bool anyoneCanAddMembers) {
using Flag = ChatAdminRight;
if (isGroup) {
return {
{ Flag::f_change_info, tr::lng_rights_group_info(tr::now) },
{ Flag::f_delete_messages, tr::lng_rights_group_delete(tr::now) },
{ Flag::f_ban_users, tr::lng_rights_group_ban(tr::now) },
{ Flag::f_invite_users, anyoneCanAddMembers
? tr::lng_rights_group_invite_link(tr::now)
: tr::lng_rights_group_invite(tr::now) },
{ Flag::f_pin_messages, tr::lng_rights_group_pin(tr::now) },
{ Flag::f_add_admins, tr::lng_rights_add_admins(tr::now) },
};
} else {
return {
{ Flag::f_change_info, tr::lng_rights_channel_info(tr::now) },
{ Flag::f_post_messages, tr::lng_rights_channel_post(tr::now) },
{ Flag::f_edit_messages, tr::lng_rights_channel_edit(tr::now) },
{ Flag::f_delete_messages, tr::lng_rights_channel_delete(tr::now) },
{ Flag::f_invite_users, tr::lng_rights_group_invite(tr::now) },
{ Flag::f_add_admins, tr::lng_rights_add_admins(tr::now) }
};
}
}
auto Dependencies(ChatRestrictions)
-> std::vector<std::pair<ChatRestriction, ChatRestriction>> {
using Flag = ChatRestriction;
return {
// stickers <-> gifs
{ Flag::f_send_gifs, Flag::f_send_stickers },
{ Flag::f_send_stickers, Flag::f_send_gifs },
// stickers <-> games
{ Flag::f_send_games, Flag::f_send_stickers },
{ Flag::f_send_stickers, Flag::f_send_games },
// stickers <-> inline
{ Flag::f_send_inline, Flag::f_send_stickers },
{ Flag::f_send_stickers, Flag::f_send_inline },
// stickers -> send_media
{ Flag::f_send_stickers, Flag::f_send_messages },
// embed_links -> send_media
{ Flag::f_embed_links, Flag::f_send_messages },
// send_media -> send_messages
{ Flag::f_send_media, Flag::f_send_messages },
// send_polls -> send_messages
{ Flag::f_send_polls, Flag::f_send_messages },
// send_messages -> view_messages
{ Flag::f_send_messages, Flag::f_view_messages },
};
}
ChatRestrictions NegateRestrictions(ChatRestrictions value) {
using Flag = ChatRestriction;
return (~value) & (Flag(0)
// view_messages is always allowed, so it is never in restrictions.
//| Flag::f_view_messages
| Flag::f_change_info
| Flag::f_embed_links
| Flag::f_invite_users
| Flag::f_pin_messages
| Flag::f_send_games
| Flag::f_send_gifs
| Flag::f_send_inline
| Flag::f_send_media
| Flag::f_send_messages
| Flag::f_send_polls
| Flag::f_send_stickers);
}
auto Dependencies(ChatAdminRights)
-> std::vector<std::pair<ChatAdminRight, ChatAdminRight>> {
return {};
}
auto ToPositiveNumberString() {
return rpl::map([](int count) {
return count ? QString::number(count) : QString();
});
}
ChatRestrictions DisabledByAdminRights(not_null<PeerData*> 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::f_pin_messages)
? Flag(0)
: Flag::f_pin_messages)
| ((adminRights & Admin::f_invite_users)
? Flag(0)
: Flag::f_invite_users)
| ((adminRights & Admin::f_change_info)
? Flag(0)
: Flag::f_change_info);
}
} // namespace
ChatAdminRights DisabledByDefaultRestrictions(not_null<PeerData*> 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::f_pin_messages)
? Flag(0)
: Flag::f_pin_messages)
//
// 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::f_invite_users)
// ? Flag(0)
// : Flag::f_invite_users)
//
| ((restrictions & Restriction::f_change_info)
? Flag(0)
: Flag::f_change_info);
}
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::f_send_stickers) {
restrictions |= ChatRestriction::f_send_inline;
} else {
restrictions &= ~ChatRestriction::f_send_inline;
}
// 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 FullAdminRights(bool isGroup) {
auto result = ChatAdminRights();
for (const auto &[flag, label] : AdminRightLabels(isGroup, true)) {
result |= flag;
}
return result;
}
EditPeerPermissionsBox::EditPeerPermissionsBox(
QWidget*,
not_null<PeerData*> peer)
: _peer(peer->migrateToOrMe()) {
}
auto EditPeerPermissionsBox::saveEvents() const -> rpl::producer<Result> {
Expects(_save != nullptr);
return _save->clicks() | rpl::map(_value);
}
void EditPeerPermissionsBox::prepare() {
setTitle(tr::lng_manage_peer_permissions());
const auto inner = setInnerWidget(object_ptr<Ui::VerticalLayout>(this));
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::f_change_info | Flag::f_pin_messages)
: Flags(0))
| disabledByAdminRights;
}
Unexpected("User in EditPeerPermissionsBox.");
}());
const auto disabledMessages = [&] {
auto result = std::map<Flags, QString>();
result.emplace(
disabledByAdminRights,
tr::lng_rights_permission_cant_edit(tr::now));
if (const auto channel = _peer->asChannel()) {
if (channel->isPublic()) {
result.emplace(
Flag::f_change_info | Flag::f_pin_messages,
tr::lng_rights_permission_unavailable(tr::now));
}
}
return result;
}();
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
this,
tr::lng_rights_default_restrictions_header(),
restrictions,
disabledMessages);
inner->add(std::move(checkboxes));
const auto getSlowmodeSeconds = addSlowmodeSlider(inner);
addBannedButtons(inner);
_value = [=, rights = getRestrictions]() -> Result {
return { rights(), getSlowmodeSeconds() };
};
_save = addButton(tr::lng_settings_save());
addButton(tr::lng_cancel(), [=] { closeBox(); });
setDimensionsToContent(st::boxWidth, inner);
}
Fn<int()> EditPeerPermissionsBox::addSlowmodeSlider(
not_null<Ui::VerticalLayout*> container) {
using namespace rpl::mappers;
if (const auto chat = _peer->asChat()) {
if (!chat->amCreator()) {
return [] { return 0; };
}
}
const auto channel = _peer->asChannel();
auto &lifetime = container->lifetime();
const auto secondsCount = lifetime.make_state<rpl::variable<int>>(
channel ? channel->slowmodeSeconds() : 0);
container->add(
object_ptr<BoxContentDivider>(container),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_rights_slowmode_header(),
st::rightsHeaderLabel),
st::rightsHeaderMargin);
addSlowmodeLabels(container);
const auto slider = container->add(
object_ptr<Ui::MediaSlider>(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;
});
const auto about = container->add(
object_ptr<Ui::DividerLabel>(
container,
object_ptr<Ui::FlatLabel>(
container,
std::move(aboutText),
st::boxDividerLabel),
st::proxyAboutPadding),
style::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip));
return [=] { return secondsCount->current(); };
}
void EditPeerPermissionsBox::addSlowmodeLabels(
not_null<Ui::VerticalLayout*> container) {
const auto labels = container->add(
object_ptr<Ui::FixedHeightWidget>(container, st::normalFont->height),
st::slowmodeLabelsMargin);
for (auto i = 0; i != kSlowmodeValues; ++i) {
const auto seconds = SlowmodeDelayByIndex(i);
const auto label = Ui::CreateChild<Ui::LabelSimple>(
labels,
st::slowmodeLabel,
(!seconds
? tr::lng_rights_slowmode_off(tr::now)
: (seconds < 60)
? tr::lng_rights_slowmode_seconds(
tr::now,
lt_count,
seconds)
: (seconds < 3600)
? tr::lng_rights_slowmode_minutes(
tr::now,
lt_count,
seconds / 60)
: tr::lng_rights_slowmode_hours(
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());
}
}
void EditPeerPermissionsBox::addBannedButtons(
not_null<Ui::VerticalLayout*> container) {
if (const auto chat = _peer->asChat()) {
if (!chat->amCreator()) {
return;
}
}
const auto channel = _peer->asChannel();
const auto navigation = App::wnd()->sessionController();
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::peerPermissionsButton));
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::peerPermissionsButton));
}
}
template <
typename Flags,
typename DisabledMessagePairs,
typename FlagLabelPairs>
EditFlagsControl<Flags> CreateEditFlags(
QWidget *parent,
rpl::producer<QString> header,
Flags checked,
const DisabledMessagePairs &disabledMessagePairs,
const FlagLabelPairs &flagLabelPairs) {
auto widget = object_ptr<Ui::VerticalLayout>(parent);
const auto container = widget.data();
const auto checkboxes = container->lifetime(
).make_state<std::map<Flags, QPointer<Ui::Checkbox>>>();
const auto value = [=] {
auto result = Flags(0);
for (const auto &[flags, checkbox] : *checkboxes) {
if (checkbox->checked()) {
result |= flags;
} else {
result &= ~flags;
}
}
return result;
};
const auto changes = container->lifetime(
).make_state<rpl::event_stream<>>();
const auto applyDependencies = [=](Ui::Checkbox *control) {
static const auto dependencies = Dependencies(Flags());
ApplyDependencies(*checkboxes, dependencies, control);
};
container->add(
object_ptr<Ui::FlatLabel>(
container,
std::move(header),
st::rightsHeaderLabel),
st::rightsHeaderMargin);
auto addCheckbox = [&](Flags flags, const QString &text) {
const auto lockedIt = ranges::find_if(
disabledMessagePairs,
[&](const auto &pair) { return (pair.first & flags) != 0; });
const auto locked = (lockedIt != end(disabledMessagePairs))
? std::make_optional(lockedIt->second)
: std::nullopt;
const auto toggled = ((checked & flags) != 0);
auto toggle = std::make_unique<Ui::ToggleView>(
st::rightsToggle,
toggled);
toggle->setLocked(locked.has_value());
const auto control = container->add(
object_ptr<Ui::Checkbox>(
container,
text,
st::rightsCheckbox,
std::move(toggle)),
st::rightsToggleMargin);
control->checkedChanges(
) | rpl::start_with_next([=](bool checked) {
if (locked.has_value()) {
if (checked != toggled) {
Ui::Toast::Show(*locked);
control->setChecked(toggled);
}
} else {
InvokeQueued(control, [=] {
applyDependencies(control);
changes->fire({});
});
}
}, control->lifetime());
checkboxes->emplace(flags, control);
};
for (const auto &[flags, label] : flagLabelPairs) {
addCheckbox(flags, label);
}
applyDependencies(nullptr);
for (const auto &[flags, checkbox] : *checkboxes) {
checkbox->finishAnimating();
}
return {
std::move(widget),
value,
changes->events() | rpl::map(value)
};
}
EditFlagsControl<MTPDchatBannedRights::Flags> CreateEditRestrictions(
QWidget *parent,
rpl::producer<QString> header,
MTPDchatBannedRights::Flags restrictions,
std::map<MTPDchatBannedRights::Flags, QString> disabledMessages) {
auto result = CreateEditFlags(
parent,
header,
NegateRestrictions(restrictions),
disabledMessages,
RestrictionLabels());
result.value = [original = std::move(result.value)]{
return NegateRestrictions(original());
};
result.changes = std::move(
result.changes
) | rpl::map(NegateRestrictions);
return result;
}
EditFlagsControl<MTPDchatAdminRights::Flags> CreateEditAdminRights(
QWidget *parent,
rpl::producer<QString> header,
MTPDchatAdminRights::Flags rights,
std::map<MTPDchatAdminRights::Flags, QString> disabledMessages,
bool isGroup,
bool anyoneCanAddMembers) {
return CreateEditFlags(
parent,
header,
rights,
disabledMessages,
AdminRightLabels(isGroup, anyoneCanAddMembers));
}