diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0ffd19f99..2f42270ee 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -273,6 +273,8 @@ PRIVATE boxes/local_storage_box.h boxes/max_invite_box.cpp boxes/max_invite_box.h + boxes/moderate_messages_box.cpp + boxes/moderate_messages_box.h boxes/peer_list_box.cpp boxes/peer_list_box.h boxes/peer_list_controllers.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 62cf4464e..1174605db 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2854,7 +2854,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_audio_count#other" = "{count} audio"; "lng_ban_user" = "Ban User"; +"lng_ban_users" = "Ban users"; +"lng_restrict_users" = "Restrict users"; "lng_delete_all_from_user" = "Delete all from {user}"; +"lng_delete_all_from_users" = "Delete all from users"; +"lng_restrict_user_part" = "Partially restrict this user"; +"lng_restrict_users_part" = "Partially restrict users"; +"lng_restrict_user_full" = "Fully ban this user"; +"lng_restrict_users_full" = "Fully ban users"; +"lng_restrict_users_part_single_header" = "What can this user do?"; +"lng_restrict_users_part_header#one" = "What can {count} selected user do?"; +"lng_restrict_users_part_header#other" = "What can {count} selected users do?"; "lng_report_spam" = "Report Spam"; "lng_report_spam_and_leave" = "Report spam and leave"; "lng_report_spam_done" = "Thank you for your report."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 890bb87e7..ef166d580 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1074,3 +1074,16 @@ collectibleBox: Box(defaultBox) { buttonHeight: 36px; button: collectibleCopy; } + +moderateBoxUserpic: UserpicButton(defaultUserpicButton) { + size: size(34px, 42px); + photoSize: 34px; + photoPosition: point(0px, 4px); +} +moderateBoxExpand: icon {{ "chat/reply_type_group", boxTextFg }}; +moderateBoxExpandHeight: 20px; +moderateBoxExpandRight: 10px; +moderateBoxExpandInnerSkip: 2px; +moderateBoxExpandFont: font(11px); +moderateBoxExpandToggleSize: 4px; +moderateBoxExpandToggleFourStrokes: 3px; diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp new file mode 100644 index 000000000..49d436700 --- /dev/null +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -0,0 +1,494 @@ +/* +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/moderate_messages_box.h" + +#include "boxes/delete_messages_box.h" +#include "boxes/peers/edit_peer_permissions_box.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_peer.h" +#include "data/data_user.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/toggle_arrow.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/rect_part.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +enum class ModerateOption { + Ban = (1 << 0), + DeleteAll = (1 << 1), +}; +inline constexpr bool is_flag_type(ModerateOption) { return true; } +using ModerateOptions = base::flags; + +ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) { + Expects(!items.empty()); + + const auto peer = items.front()->history()->peer; + auto allCanBan = true; + auto allCanDelete = true; + for (const auto &item : items) { + if (!allCanBan && !allCanDelete) { + return ModerateOptions(0); + } + if (peer != item->history()->peer) { + return ModerateOptions(0); + } + if (!item->suggestBanReport()) { + allCanBan = false; + } + if (!item->suggestDeleteAllReport()) { + allCanDelete = false; + } + } + return ModerateOptions(0) + | (allCanBan ? ModerateOption::Ban : ModerateOptions(0)) + | (allCanDelete ? ModerateOption::DeleteAll : ModerateOptions(0)); +} + +class Button final : public Ui::RippleButton { +public: + Button(not_null parent, int count); + + void setChecked(bool checked); + [[nodiscard]] bool checked() const; + + [[nodiscard]] static QSize ComputeSize(int); + +private: + void paintEvent(QPaintEvent *event) override; + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + + const int _count; + const QString _text; + bool _checked = false; + + Ui::Animations::Simple _animation; + +}; + +Button::Button(not_null parent, int count) +: RippleButton(parent, st::defaultRippleAnimation) +, _count(count) +, _text(QString::number(std::abs(_count))) { +} + +QSize Button::ComputeSize(int count) { + return QSize( + st::moderateBoxExpandHeight + + st::moderateBoxExpand.width() + + st::moderateBoxExpandInnerSkip * 4 + + st::moderateBoxExpandFont->width( + QString::number(std::abs(count))) + + st::moderateBoxExpandToggleSize, + st::moderateBoxExpandHeight); +} + +void Button::setChecked(bool checked) { + if (_checked == checked) { + return; + } + _checked = checked; + _animation.stop(); + _animation.start( + [=] { update(); }, + checked ? 0 : 1, + checked ? 1 : 0, + st::slideWrapDuration); +} + +bool Button::checked() const { + return _checked; +} + +void Button::paintEvent(QPaintEvent *event) { + auto p = Painter(this); + auto hq = PainterHighQualityEnabler(p); + Ui::RippleButton::paintRipple(p, QPoint()); + const auto radius = height() / 2; + p.setPen(Qt::NoPen); + st::moderateBoxExpand.paint( + p, + radius, + (height() - st::moderateBoxExpand.height()) / 2, + width()); + + const auto innerSkip = st::moderateBoxExpandInnerSkip; + + p.setBrush(Qt::NoBrush); + p.setPen(st::boxTextFg); + p.setFont(st::moderateBoxExpandFont); + p.drawText( + QRect( + innerSkip + radius + st::moderateBoxExpand.width(), + 0, + width(), + height()), + _text, + style::al_left); + + const auto path = Ui::ToggleUpDownArrowPath( + width() - st::moderateBoxExpandToggleSize - radius, + height() / 2, + st::moderateBoxExpandToggleSize, + st::moderateBoxExpandToggleFourStrokes, + _animation.value(_checked ? 1. : 0.)); + p.fillPath(path, st::boxTextFg); +} + +QImage Button::prepareRippleMask() const { + return Ui::RippleAnimation::RoundRectMask(size(), size().height() / 2); +} + +QPoint Button::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()); +} + +} // namespace + +void CreateModerateMessagesBox( + not_null box, + const HistoryItemsList &items) { + const auto options = CalculateModerateOptions(items); + const auto inner = box->verticalLayout(); + + const auto users = [&] { + auto result = std::vector>(); + for (const auto &item : items) { + if (const auto user = item->from()->asUser()) { + if (!ranges::contains(result, not_null{ user })) { + result.push_back(user); + } + } + } + return result; + }(); + Assert(!users.empty()); + + const auto isSingle = users.size() == 1; + const auto buttonPadding = isSingle + ? QMargins() + : QMargins(0, 0, Button::ComputeSize(users.size()).width(), 0); + + struct Controller final { + rpl::event_stream toggleRequestsFromTop; + rpl::event_stream toggleRequestsFromInner; + rpl::event_stream checkAllRequests; + }; + const auto createUsersList = [&](not_null controller) { + const auto wrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + wrap->toggle(false, anim::type::instant); + + controller->toggleRequestsFromTop.events( + ) | rpl::start_with_next([=](bool toggled) { + wrap->toggle(toggled, anim::type::normal); + }, wrap->lifetime()); + + const auto container = wrap->entity(); + Ui::AddSkip(container); + + auto &lifetime = wrap->lifetime(); + const auto clicks = lifetime.make_state>(); + const auto checkboxes = ranges::views::all( + users + ) | ranges::views::transform([&](not_null user) { + const auto line = container->add( + object_ptr(container)); + const auto &st = st::moderateBoxUserpic; + line->resize(line->width(), st.size.height()); + + const auto userpic = Ui::CreateChild( + line, + user, + st); + const auto checkbox = Ui::CreateChild( + line, + user->name(), + false, + st::defaultBoxCheckbox); + line->widthValue( + ) | rpl::start_with_next([=](int width) { + userpic->moveToLeft( + st::boxRowPadding.left() + + checkbox->checkRect().width() + + st::defaultBoxCheckbox.textPosition.x(), + 0); + const auto skip = st::defaultBoxCheckbox.textPosition.x(); + checkbox->resizeToWidth(width + - rect::right(userpic) + - skip + - st::boxRowPadding.right()); + checkbox->moveToLeft( + rect::right(userpic) + skip, + ((userpic->height() - checkbox->height()) / 2) + + st::defaultBoxCheckbox.margin.top()); + }, checkbox->lifetime()); + + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + checkbox->setAttribute(Qt::WA_TransparentForMouseEvents); + + line->setClickedCallback([=] { + checkbox->setChecked(!checkbox->checked()); + clicks->fire({}); + }); + + return checkbox; + }) | ranges::to_vector; + + clicks->events( + ) | rpl::start_with_next([=] { + controller->toggleRequestsFromInner.fire_copy( + ranges::any_of(checkboxes, &Ui::Checkbox::checked)); + }, container->lifetime()); + + controller->checkAllRequests.events( + ) | rpl::start_with_next([=](bool checked) { + for (const auto &c : checkboxes) { + c->setChecked(checked); + } + }, container->lifetime()); + }; + + const auto appendList = [&](not_null checkbox) { + const auto button = Ui::CreateChild