/* 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 "api/api_chat_participants.h" #include "apiwrap.h" #include "base/timer.h" #include "boxes/delete_messages_box.h" #include "boxes/peers/edit_peer_permissions_box.h" #include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_chat_participant_status.h" #include "data/data_histories.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" #include "main/main_session.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, Fn confirmed) { using Users = std::vector>; struct Controller final { rpl::event_stream toggleRequestsFromTop; rpl::event_stream toggleRequestsFromInner; rpl::event_stream checkAllRequests; Fn collectRequests; }; constexpr auto kSmallDelayMs = 5; const auto options = CalculateModerateOptions(items); const auto inner = box->verticalLayout(); const auto users = [&] { auto result = Users(); 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 confirms = inner->lifetime().make_state>(); const auto isSingle = users.size() == 1; const auto buttonPadding = isSingle ? QMargins() : QMargins(0, 0, Button::ComputeSize(users.size()).width(), 0); using Request = Fn, not_null)>; const auto sequentiallyRequest = [=](Request request, Users users) { const auto session = &items.front()->history()->session(); const auto history = items.front()->history(); const auto peerId = history->peer->id; const auto userIds = ranges::views::all( users ) | ranges::views::transform([](not_null user) { return user->id; }) | ranges::to_vector; const auto lifetime = std::make_shared(); const auto counter = lifetime->make_state(0); const auto timer = lifetime->make_state(); timer->setCallback(crl::guard(session, [=] { if ((*counter) < userIds.size()) { const auto peer = session->data().peer(peerId); const auto channel = peer ? peer->asChannel() : nullptr; const auto from = session->data().peer(userIds[*counter]); if (const auto user = from->asUser(); channel && user) { request(user, channel); } (*counter)++; } else { lifetime->destroy(); } })); timer->callEach(kSmallDelayMs); }; const auto handleConfirmation = [=]( not_null checkbox, not_null controller, Request request) { confirms->events() | rpl::start_with_next([=] { if (checkbox->checked()) { if (isSingle) { const auto item = items.front(); const auto channel = item->history()->peer->asChannel(); request(users.front(), channel); } else if (const auto collect = controller->collectRequests) { sequentiallyRequest(request, collect()); } } }, checkbox->lifetime()); }; 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()); controller->collectRequests = [=] { auto result = Users(); for (auto i = 0; i < checkboxes.size(); i++) { if (checkboxes[i]->checked()) { result.push_back(users[i]); } } return result; }; }; const auto appendList = [&]( not_null checkbox, not_null controller) { const auto button = Ui::CreateChild