/* 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 "api/api_messages_search.h" #include "apiwrap.h" #include "base/event_filter.h" #include "base/timer.h" #include "boxes/delete_messages_box.h" #include "boxes/peers/edit_peer_permissions_box.h" #include "core/application.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/boxes/confirm_box.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" #include "styles/style_window.h" namespace { using Participants = std::vector>; struct ModerateOptions final { bool allCanBan = false; bool allCanDelete = false; Participants participants; }; ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) { Expects(!items.empty()); auto result = ModerateOptions{ .allCanBan = true, .allCanDelete = true, }; const auto peer = items.front()->history()->peer; for (const auto &item : items) { if (!result.allCanBan && !result.allCanDelete) { return {}; } if (peer != item->history()->peer) { return {}; } if (!item->suggestBanReport()) { result.allCanBan = false; } if (!item->suggestDeleteAllReport()) { result.allCanDelete = false; } if (const auto p = item->from()) { if (!ranges::contains(result.participants, not_null{ p })) { result.participants.push_back(p); } } } return result; } [[nodiscard]] rpl::producer MessagesCountValue( not_null history, not_null from) { return [=](auto consumer) { auto lifetime = rpl::lifetime(); auto search = lifetime.make_state(history); consumer.put_next(0); search->messagesFounds( ) | rpl::start_with_next([=](const Api::FoundMessages &found) { consumer.put_next_copy(found.total); }, lifetime); search->searchMessages({ .from = from }); return lifetime; }; } 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) { struct Controller final { rpl::event_stream toggleRequestsFromTop; rpl::event_stream toggleRequestsFromInner; rpl::event_stream checkAllRequests; Fn collectRequests; }; const auto [allCanBan, allCanDelete, participants] = CalculateModerateOptions(items); const auto inner = box->verticalLayout(); Assert(!participants.empty()); const auto confirms = inner->lifetime().make_state>(); const auto isSingle = participants.size() == 1; const auto buttonPadding = isSingle ? QMargins() : QMargins(0, 0, Button::ComputeSize(participants.size()).width(), 0); const auto session = &items.front()->history()->session(); const auto historyPeerId = items.front()->history()->peer->id; using Request = Fn, not_null)>; const auto sequentiallyRequest = [=]( Request request, Participants participants) { constexpr auto kSmallDelayMs = 5; const auto participantIds = ranges::views::all( participants ) | ranges::views::transform([](not_null peer) { return peer->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) < participantIds.size()) { const auto peer = session->data().peer(historyPeerId); const auto channel = peer ? peer->asChannel() : nullptr; const auto from = session->data().peer( participantIds[*counter]); if (channel && from) { request(from, 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() && controller->collectRequests) { sequentiallyRequest(request, controller->collectRequests()); } }, checkbox->lifetime()); }; const auto isEnter = [=](not_null event) { if (event->type() == QEvent::KeyPress) { if (const auto k = static_cast(event.get())) { return (k->key() == Qt::Key_Enter) || (k->key() == Qt::Key_Return); } } return false; }; base::install_event_filter(box, [=](not_null event) { if (isEnter(event)) { box->triggerButton(0); return base::EventFilterResult::Cancel; } return base::EventFilterResult::Continue; }); const auto handleSubmition = [=](not_null checkbox) { base::install_event_filter(box, [=](not_null event) { if (!isEnter(event) || !checkbox->checked()) { return base::EventFilterResult::Continue; } box->uiShow()->show(Ui::MakeConfirmBox({ .text = tr::lng_gigagroup_warning_title(), .confirmed = [=](Fn close) { box->triggerButton(0); close(); }, .confirmText = tr::lng_box_yes(), .cancelText = tr::lng_box_no(), })); return base::EventFilterResult::Cancel; }); }; const auto createParticipantsList = [&]( 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( participants ) | ranges::views::transform([&](not_null peer) { 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, peer, st); const auto checkbox = Ui::CreateChild( line, peer->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 = Participants(); for (auto i = 0; i < checkboxes.size(); i++) { if (checkboxes[i]->checked()) { result.push_back(participants[i]); } } return result; }; }; const auto appendList = [&]( not_null checkbox, not_null controller) { if (isSingle) { const auto p = participants.front(); controller->collectRequests = [=] { return Participants{ p }; }; return; } const auto count = int(participants.size()); const auto button = Ui::CreateChild