1735 linhas
51 KiB
C++
1735 linhas
51 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/moderate_messages_box.h"
|
|
|
|
#include "api/api_blocked_peers.h"
|
|
#include "api/api_chat_participants.h"
|
|
#include "api/api_messages_search.h"
|
|
#include "api/api_report.h"
|
|
#include "apiwrap.h"
|
|
#include "base/event_filter.h"
|
|
#include "base/options.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_filters.h"
|
|
#include "data/data_chat_participant_status.h"
|
|
#include "data/data_histories.h"
|
|
#include "data/data_peer.h"
|
|
#include "data/data_saved_sublist.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 "lottie/lottie_icon.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_lottie_custom_emoji.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "ui/vertical_list.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/checkbox.h"
|
|
#include "ui/widgets/expandable_peer_list.h"
|
|
#include "ui/widgets/participants_check_view.h"
|
|
#include "ui/wrap/slide_wrap.h"
|
|
#include "ui/ui_utility.h"
|
|
#include "styles/style_boxes.h"
|
|
#include "styles/style_layers.h"
|
|
#include "styles/style_window.h"
|
|
|
|
#include "window/window_session_controller.h"
|
|
#include "window/window_controller.h"
|
|
#include "boxes/choose_filter_box.h"
|
|
#include "boxes/peer_list_box.h"
|
|
#include "main/main_session_settings.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/rect.h"
|
|
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
|
#include "ui/widgets/menu/menu_add_action_callback.h"
|
|
#include "ui/widgets/menu/menu_multiline_action.h"
|
|
#include "ui/widgets/popup_menu.h"
|
|
#include "ui/widgets/menu/menu_action.h"
|
|
#include "base/qt/qt_key_modifiers.h"
|
|
#include "ui/effects/round_checkbox.h"
|
|
#include "styles/style_chat.h"
|
|
#include "styles/style_menu_icons.h"
|
|
#include "styles/style_info.h"
|
|
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
|
|
|
|
base::options::toggle ModerateCommonGroups({
|
|
.id = kModerateCommonGroups,
|
|
.name = "Ban users from several groups at once.",
|
|
});
|
|
|
|
const char kModerateCommonGroups[] = "moderate-common-groups";
|
|
|
|
namespace {
|
|
|
|
constexpr auto kModerateMessagesBoxAnimationDuration = crl::time(80);
|
|
|
|
struct ModerateOptions final {
|
|
bool reportSpam = false;
|
|
bool deleteAllMessages = false;
|
|
bool deleteAllReactions = false;
|
|
bool banOrRestrict = false;
|
|
Participants participants;
|
|
};
|
|
|
|
[[nodiscard]] bool PeerCanDeleteMessages(not_null<PeerData*> peer) {
|
|
if (const auto chat = peer->asChat()) {
|
|
return chat->canDeleteMessages();
|
|
}
|
|
const auto channel = peer->asChannel();
|
|
return channel && channel->canDeleteMessages();
|
|
}
|
|
|
|
[[nodiscard]] bool IsExcludedModerateParticipant(
|
|
not_null<PeerData*> peer,
|
|
not_null<PeerData*> participant) {
|
|
if ((participant == peer) || participant->isSelf()) {
|
|
return true;
|
|
} else if (const auto channel = participant->asChannel()) {
|
|
return (channel->discussionLink() == peer);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
|
|
Expects(!items.empty());
|
|
|
|
auto result = ModerateOptions{
|
|
.deleteAllMessages = true,
|
|
.banOrRestrict = true,
|
|
};
|
|
|
|
const auto peer = items.front()->history()->peer;
|
|
for (const auto &item : items) {
|
|
if (!result.deleteAllMessages && !result.banOrRestrict) {
|
|
return {};
|
|
}
|
|
if (peer != item->history()->peer) {
|
|
return {};
|
|
}
|
|
{
|
|
const auto author = item->author();
|
|
if (author == peer) {
|
|
return {};
|
|
} else if (const auto channel = author->asChannel()) {
|
|
if (channel->discussionLink() == peer) {
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
if (!item->suggestBanReport()) {
|
|
result.banOrRestrict = false;
|
|
}
|
|
if (!item->suggestDeleteAllReport()) {
|
|
result.deleteAllMessages = false;
|
|
}
|
|
if (const auto p = item->from()) {
|
|
if (!ranges::contains(result.participants, not_null{ p })) {
|
|
result.participants.push_back(p);
|
|
}
|
|
}
|
|
}
|
|
result.deleteAllReactions = result.deleteAllMessages;
|
|
result.reportSpam = result.deleteAllMessages || result.banOrRestrict;
|
|
return result;
|
|
}
|
|
|
|
ModerateOptions CalculateModerateOptions(const ModerateReactionEntry &reaction) {
|
|
auto result = ModerateOptions{
|
|
.participants = { reaction.participant },
|
|
};
|
|
if (IsExcludedModerateParticipant(reaction.peer, reaction.participant)) {
|
|
return result;
|
|
}
|
|
result.reportSpam = Api::GetReactionReportCapabilities(
|
|
reaction.peer,
|
|
reaction.participant
|
|
).canReport || (reaction.peer->asChannel() != nullptr);
|
|
result.deleteAllReactions = PeerCanDeleteMessages(reaction.peer);
|
|
if (const auto channel = reaction.peer->asChannel()) {
|
|
result.deleteAllMessages = channel->canDeleteMessages();
|
|
result.banOrRestrict = channel->canRestrictParticipant(
|
|
reaction.participant);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] bool HasModerateActions(const ModerateOptions &options) {
|
|
return options.reportSpam
|
|
|| options.deleteAllMessages
|
|
|| options.deleteAllReactions
|
|
|| options.banOrRestrict;
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities ParticipantsExpanderText(int count) {
|
|
return tr::marked()
|
|
.append(st::moderateBoxExpand)
|
|
.append(QString::number(count));
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities DeleteOptionsExpanderText(
|
|
int checkedCount,
|
|
int totalCount) {
|
|
return tr::marked(u"%1 / %2"_q.arg(checkedCount).arg(totalCount));
|
|
}
|
|
|
|
[[nodiscard]] rpl::producer<base::flat_map<PeerId, int>> MessagesCountValue(
|
|
not_null<History*> history,
|
|
std::vector<not_null<PeerData*>> from) {
|
|
return [=](auto consumer) {
|
|
auto lifetime = rpl::lifetime();
|
|
struct State final {
|
|
base::flat_map<PeerId, int> messagesCounts;
|
|
int index = 0;
|
|
rpl::lifetime apiLifetime;
|
|
};
|
|
const auto search = lifetime.make_state<Api::MessagesSearch>(history);
|
|
const auto state = lifetime.make_state<State>();
|
|
const auto send = [=](auto repeat) -> void {
|
|
if (state->index >= from.size()) {
|
|
consumer.put_next_copy(state->messagesCounts);
|
|
return;
|
|
}
|
|
const auto peer = from[state->index];
|
|
const auto peerId = peer->id;
|
|
state->apiLifetime = search->messagesFounds(
|
|
) | rpl::on_next([=](const Api::FoundMessages &found) {
|
|
state->messagesCounts[peerId] = found.total;
|
|
state->index++;
|
|
repeat(repeat);
|
|
});
|
|
search->searchMessages({ .from = peer });
|
|
};
|
|
consumer.put_next({});
|
|
send(send);
|
|
|
|
return lifetime;
|
|
};
|
|
}
|
|
|
|
using CommonGroups = std::vector<not_null<PeerData*>>;
|
|
using CollectCommon = std::shared_ptr<std::vector<PeerId>>;
|
|
|
|
void FillMenuModerateCommonGroups(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
CommonGroups common,
|
|
CollectCommon collectCommon,
|
|
not_null<UserData*> user,
|
|
Fn<void()> onDestroyedCallback) {
|
|
const auto resultList
|
|
= menu->lifetime().make_state<base::flat_set<PeerId>>();
|
|
const auto rememberCheckbox = Ui::CreateChild<Ui::Checkbox>(
|
|
menu,
|
|
QString());
|
|
auto multiline = base::make_unique_q<Ui::Menu::MultilineAction>(
|
|
menu->menu(),
|
|
menu->st().menu,
|
|
st::historyHasCustomEmoji,
|
|
st::historyHasCustomEmojiPosition,
|
|
tr::lng_restrict_users_kick_from_common_group(tr::now, tr::rich));
|
|
multiline->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
menu->addAction(std::move(multiline));
|
|
const auto session = &common.front()->session();
|
|
const auto settingsOnStart = session->settings().moderateCommonGroups();
|
|
const auto checkboxesUpdate = std::make_shared<rpl::event_stream<>>();
|
|
const auto save = [=] {
|
|
auto result = std::vector<PeerId>(
|
|
resultList->begin(),
|
|
resultList->end());
|
|
*collectCommon = std::move(result);
|
|
};
|
|
for (const auto &group : common) {
|
|
struct State {
|
|
std::optional<Ui::RoundImageCheckbox> checkbox;
|
|
Ui::RpWidget *checkboxWidget = nullptr;
|
|
};
|
|
auto item = base::make_unique_q<Ui::Menu::Action>(
|
|
menu->menu(),
|
|
menu->st().menu,
|
|
Ui::Menu::CreateAction(
|
|
menu->menu(),
|
|
group->name(),
|
|
[] {}),
|
|
nullptr,
|
|
nullptr);
|
|
const auto state = item->lifetime().make_state<State>();
|
|
const auto setChecked = [=, peerId = group->id](bool checked) {
|
|
state->checkbox->setChecked(checked);
|
|
if (state->checkbox->checked()) {
|
|
resultList->insert(peerId);
|
|
} else {
|
|
resultList->erase(peerId);
|
|
}
|
|
save();
|
|
};
|
|
item->setActionTriggered([=] {
|
|
setChecked(!state->checkbox->checked());
|
|
});
|
|
const auto raw = item.get();
|
|
checkboxesUpdate->events() | rpl::on_next([=, peerId = group->id] {
|
|
setChecked(ranges::contains(*collectCommon, peerId));
|
|
}, raw->lifetime());
|
|
state->checkboxWidget = Ui::CreateChild<Ui::RpWidget>(raw);
|
|
state->checkboxWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
state->checkboxWidget->resize(item->width() * 2, item->height());
|
|
state->checkboxWidget->show();
|
|
state->checkbox.emplace(
|
|
st::moderateCommonGroupsCheckbox,
|
|
[=] { state->checkboxWidget->update(); },
|
|
PaintUserpicCallback(group, true),
|
|
[=](int size) { return (group->isForum() || group->isMonoforum())
|
|
? int(size * Ui::ForumUserpicRadiusMultiplier())
|
|
: std::optional<int>(); });
|
|
state->checkbox->setChecked(
|
|
/*ranges::contains(
|
|
session->settings().moderateCommonGroups(),
|
|
group->id)
|
|
|| */(collectCommon
|
|
&& ranges::contains(*collectCommon, group->id)),
|
|
anim::type::instant);
|
|
state->checkboxWidget->paintOn([=](QPainter &p) {
|
|
auto pp = Painter(state->checkboxWidget);
|
|
state->checkbox->paint(
|
|
pp,
|
|
st::menuWithIcons.itemIconPosition.x(),
|
|
st::menuWithIcons.itemIconPosition.y(),
|
|
raw->width());
|
|
});
|
|
menu->addAction(std::move(item));
|
|
}
|
|
menu->addSeparator();
|
|
if (const auto window = Core::App().findWindow(menu->parentWidget())) {
|
|
auto hasActions = false;
|
|
Ui::Menu::CreateAddActionCallback(menu)(Ui::Menu::MenuCallback::Args{
|
|
.text = tr::lng_restrict_users_kick_from_common_group(tr::now),
|
|
.handler = nullptr,
|
|
.icon = &st::menuIconAddToFolder,
|
|
.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {
|
|
hasActions = FillChooseFilterWithAdminedGroupsMenu(
|
|
window->sessionController(),
|
|
menu,
|
|
user,
|
|
checkboxesUpdate,
|
|
common,
|
|
collectCommon);
|
|
},
|
|
.submenuSt = &st::foldersMenu,
|
|
});
|
|
if (!hasActions) {
|
|
menu->removeAction(menu->actions().size() - 1);
|
|
menu->removeAction(menu->actions().size() - 1); // Separator.
|
|
}
|
|
}
|
|
menu->addSeparator();
|
|
{
|
|
auto item = base::make_unique_q<Ui::Menu::Action>(
|
|
menu->menu(),
|
|
menu->st().menu,
|
|
Ui::Menu::CreateAction(
|
|
menu->menu(),
|
|
tr::lng_remember(tr::now),
|
|
[] {}),
|
|
nullptr,
|
|
nullptr);
|
|
item->setPreventClose(true);
|
|
item->setActionTriggered([=] {
|
|
rememberCheckbox->setChecked(!rememberCheckbox->checked());
|
|
});
|
|
rememberCheckbox->setParent(item.get());
|
|
rememberCheckbox->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
rememberCheckbox->move(st::lineWidth * 8, -st::lineWidth * 2);
|
|
rememberCheckbox->show();
|
|
menu->addAction(std::move(item));
|
|
}
|
|
menu->setDestroyedCallback([=] {
|
|
onDestroyedCallback();
|
|
if (!rememberCheckbox->checked()) {
|
|
session->settings().setModerateCommonGroups(settingsOnStart);
|
|
session->saveSettingsDelayed();
|
|
}
|
|
});
|
|
}
|
|
|
|
void ProccessCommonGroups(
|
|
const HistoryItemsList &items,
|
|
Fn<void(CommonGroups, not_null<UserData*>)> processHas) {
|
|
const auto moderateOptions = CalculateModerateOptions(items);
|
|
if (moderateOptions.participants.size() != 1
|
|
|| !moderateOptions.banOrRestrict) {
|
|
return;
|
|
}
|
|
const auto participant = moderateOptions.participants.front();
|
|
const auto user = participant->asUser();
|
|
if (!user) {
|
|
return;
|
|
}
|
|
const auto currentGroupId = items.front()->history()->peer->id;
|
|
user->session().api().requestBotCommonGroups(user, [=] {
|
|
const auto commonGroups = user->session().api().botCommonGroups(user);
|
|
if (!commonGroups || commonGroups->empty()) {
|
|
return;
|
|
}
|
|
|
|
auto filtered = CommonGroups();
|
|
for (const auto &group : *commonGroups) {
|
|
if (group->id == currentGroupId) {
|
|
continue;
|
|
}
|
|
const auto channel = group->asChannel();
|
|
if (channel && channel->canRestrictParticipant(user)) {
|
|
if (channel->isGroupAdmin(user) && !channel->amCreator()) {
|
|
continue;
|
|
}
|
|
filtered.push_back(group);
|
|
}
|
|
}
|
|
|
|
if (!filtered.empty()) {
|
|
processHas(filtered, user);
|
|
}
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void CreateModerateMessagesBox(
|
|
not_null<Ui::GenericBox*> box,
|
|
ModerateMessagesBoxEntry entry,
|
|
Fn<void()> confirmed,
|
|
ModerateMessagesBoxOptions options) {
|
|
const auto &items = entry.items;
|
|
const auto reaction = entry.reaction;
|
|
Expects(!items.empty() || reaction.has_value());
|
|
box->setLayerAnimationDuration(kModerateMessagesBoxAnimationDuration);
|
|
|
|
const auto hasItems = !items.empty();
|
|
const auto hasReaction = reaction.has_value();
|
|
const auto itemsCount = hasItems ? int(items.size()) : 0;
|
|
|
|
using Controller = Ui::ExpandablePeerListController;
|
|
|
|
const auto moderateOptions = hasItems
|
|
? CalculateModerateOptions(items)
|
|
: CalculateModerateOptions(*reaction);
|
|
const auto reportSpam = moderateOptions.reportSpam;
|
|
const auto deleteAllMessages = moderateOptions.deleteAllMessages;
|
|
const auto deleteAllReactions = moderateOptions.deleteAllReactions;
|
|
const auto banOrRestrict = moderateOptions.banOrRestrict;
|
|
const auto &participants = moderateOptions.participants;
|
|
const auto inner = box->verticalLayout();
|
|
|
|
Assert(!participants.empty());
|
|
|
|
const auto confirms = inner->lifetime().make_state<rpl::event_stream<>>();
|
|
const auto collectCommon = std::make_shared<std::vector<PeerId>>();
|
|
|
|
const auto isSingle = participants.size() == 1;
|
|
const auto buttonPadding = isSingle
|
|
? QMargins()
|
|
: QMargins(
|
|
0,
|
|
0,
|
|
Ui::ExpanderButton::ComputeSize(
|
|
ParticipantsExpanderText(int(participants.size()))).width(),
|
|
0);
|
|
|
|
const auto firstItem = hasItems ? items.front().get() : nullptr;
|
|
const auto session = hasItems
|
|
? &firstItem->history()->session()
|
|
: &reaction->peer->session();
|
|
const auto peer = hasItems
|
|
? firstItem->history()->peer
|
|
: reaction->peer;
|
|
const auto history = hasItems
|
|
? firstItem->history().get()
|
|
: session->data().historyLoaded(peer);
|
|
const auto historyPeerId = peer->id;
|
|
const auto ids = hasItems
|
|
? session->data().itemsToIds(items)
|
|
: MessageIdsList{ FullMsgId(reaction->peer->id, reaction->msgId) };
|
|
const auto selectedMessagesByParticipant = [&] {
|
|
auto result = base::flat_map<PeerId, int>();
|
|
if (!hasItems && !hasReaction) {
|
|
return result;
|
|
}
|
|
if (hasItems) {
|
|
for (const auto &item : items) {
|
|
const auto from = item->from();
|
|
if (!from) {
|
|
continue;
|
|
}
|
|
const auto i = result.find(from->id);
|
|
if (i == result.end()) {
|
|
result.emplace(from->id, 1);
|
|
} else {
|
|
++i->second;
|
|
}
|
|
}
|
|
} else {
|
|
result.emplace(reaction->participant->id, 1);
|
|
}
|
|
return result;
|
|
}();
|
|
const auto participantIds = ranges::views::all(
|
|
participants
|
|
) | ranges::views::transform([](not_null<PeerData*> peer) {
|
|
return peer->id;
|
|
}) | ranges::to_vector;
|
|
|
|
if (hasItems) {
|
|
const auto remainingIds
|
|
= box->lifetime().make_state<base::flat_set<FullMsgId>>(
|
|
ids.begin(),
|
|
ids.end());
|
|
session->data().itemRemoved(
|
|
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
|
|
remainingIds->erase(item->fullId());
|
|
if (remainingIds->empty()) {
|
|
box->closeBox();
|
|
}
|
|
}, box->lifetime());
|
|
}
|
|
|
|
if (hasItems
|
|
&& (ModerateCommonGroups.value() || session->supportMode())) {
|
|
ProccessCommonGroups(
|
|
items,
|
|
crl::guard(box, [=](CommonGroups groups, not_null<UserData*> user) {
|
|
using namespace Ui;
|
|
const auto top = box->addTopButton(st::infoTopBarMenu);
|
|
auto &lifetime = top->lifetime();
|
|
const auto menu
|
|
= lifetime.make_state<base::unique_qptr<Ui::PopupMenu>>();
|
|
|
|
{
|
|
const auto was = collectCommon->size();
|
|
*menu = base::make_unique_q<Ui::PopupMenu>(
|
|
top,
|
|
st::popupMenuExpandedSeparator);
|
|
FillMenuModerateCommonGroups(
|
|
*menu,
|
|
groups,
|
|
collectCommon,
|
|
user,
|
|
[]{});
|
|
*menu = nullptr;
|
|
if (was != collectCommon->size()) {
|
|
top->setIconOverride(
|
|
&st::infoTopBarMenuActive,
|
|
&st::infoTopBarMenuActive);
|
|
const auto minicheck = Ui::CreateChild<Ui::RpWidget>(top);
|
|
minicheck->paintRequest() | rpl::on_next([=] {
|
|
auto p = Painter(minicheck);
|
|
const auto rect = minicheck->rect();
|
|
const auto iconSize = QSize(
|
|
st::mediaPlayerMenuCheck.width(),
|
|
st::mediaPlayerMenuCheck.height());
|
|
const auto scale = std::min(
|
|
rect.width() / float64(iconSize.width()),
|
|
rect.height() / float64(iconSize.height()));
|
|
if (scale < 1.0) {
|
|
p.save();
|
|
p.translate(rect.center());
|
|
p.scale(scale, scale);
|
|
p.translate(-rect.center());
|
|
}
|
|
st::mediaPlayerMenuCheck.paintInCenter(
|
|
p,
|
|
rect,
|
|
st::windowActiveTextFg->c);
|
|
if (scale < 1.0) {
|
|
p.restore();
|
|
}
|
|
}, minicheck->lifetime());
|
|
minicheck->resize(
|
|
st::mediaPlayerMenuCheck.width() / 1.5,
|
|
st::mediaPlayerMenuCheck.width() / 1.5);
|
|
minicheck->show();
|
|
minicheck->moveToLeft(
|
|
top->width() - st::lineWidth * 26,
|
|
top->height() - st::lineWidth * 29);
|
|
}
|
|
}
|
|
|
|
top->setClickedCallback([=] {
|
|
top->setForceRippled(true);
|
|
*menu = base::make_unique_q<Ui::PopupMenu>(
|
|
top,
|
|
st::popupMenuExpandedSeparator);
|
|
const auto onDestroyedCallback = [=, weak = top] {
|
|
if (const auto strong = weak.data()) {
|
|
strong->setForceRippled(false);
|
|
}
|
|
};
|
|
FillMenuModerateCommonGroups(
|
|
*menu,
|
|
groups,
|
|
collectCommon,
|
|
user,
|
|
onDestroyedCallback);
|
|
(*menu)->setForcedOrigin(PanelAnimation::Origin::TopRight);
|
|
const auto point = QPoint(top->width(), top->height());
|
|
(*menu)->popup(top->mapToGlobal(point));
|
|
});
|
|
}));
|
|
}
|
|
|
|
using Request = Fn<void(not_null<PeerData*>, not_null<ChannelData*>)>;
|
|
const auto sequentiallyRequest = [=](
|
|
Request request,
|
|
Participants participants,
|
|
std::optional<std::vector<PeerId>> channelIds = {}) {
|
|
constexpr auto kSmallDelayMs = 5;
|
|
const auto participantIds = ranges::views::all(
|
|
participants
|
|
) | ranges::views::transform([](not_null<PeerData*> peer) {
|
|
return peer->id;
|
|
}) | ranges::to_vector;
|
|
const auto channelIdList = channelIds.value_or(
|
|
std::vector<PeerId>{ historyPeerId });
|
|
const auto lifetime = std::make_shared<rpl::lifetime>();
|
|
const auto participantIndex = lifetime->make_state<int>(0);
|
|
const auto channelIndex = lifetime->make_state<int>(0);
|
|
const auto timer = lifetime->make_state<base::Timer>();
|
|
timer->setCallback(crl::guard(session, [=] {
|
|
if ((*participantIndex) < participantIds.size()) {
|
|
if ((*channelIndex) < channelIdList.size()) {
|
|
const auto from = session->data().peer(
|
|
participantIds[*participantIndex]);
|
|
const auto channel = session->data().peer(
|
|
channelIdList[*channelIndex])->asChannel();
|
|
if (from && channel) {
|
|
request(from, channel);
|
|
}
|
|
(*channelIndex)++;
|
|
} else {
|
|
(*participantIndex)++;
|
|
*channelIndex = 0;
|
|
}
|
|
} else {
|
|
lifetime->destroy();
|
|
}
|
|
}));
|
|
timer->callEach(kSmallDelayMs);
|
|
};
|
|
|
|
const auto handleConfirmation = [=](
|
|
not_null<Ui::Checkbox*> checkbox,
|
|
not_null<Controller*> controller,
|
|
Request request) {
|
|
confirms->events() | rpl::on_next([=] {
|
|
if (checkbox->checked() && controller->collectRequests) {
|
|
sequentiallyRequest(request, controller->collectRequests());
|
|
}
|
|
}, checkbox->lifetime());
|
|
};
|
|
|
|
const auto isEnter = [=](not_null<QEvent*> event) {
|
|
if (event->type() == QEvent::KeyPress) {
|
|
if (const auto k = static_cast<QKeyEvent*>(event.get())) {
|
|
return (k->key() == Qt::Key_Enter)
|
|
|| (k->key() == Qt::Key_Return);
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
base::install_event_filter(box, [=](not_null<QEvent*> event) {
|
|
if (isEnter(event)) {
|
|
box->triggerButton(0);
|
|
return base::EventFilterResult::Cancel;
|
|
}
|
|
return base::EventFilterResult::Continue;
|
|
});
|
|
|
|
const auto handleSubmitionIf = [=](Fn<bool()> enabled) {
|
|
base::install_event_filter(box, [=, enabled = std::move(enabled)](
|
|
not_null<QEvent*> event) {
|
|
if (!isEnter(event) || !enabled()) {
|
|
return base::EventFilterResult::Continue;
|
|
}
|
|
box->uiShow()->show(Ui::MakeConfirmBox({
|
|
.text = tr::lng_gigagroup_warning_title(),
|
|
.confirmed = [=](Fn<void()> close) {
|
|
box->triggerButton(0);
|
|
close();
|
|
},
|
|
.confirmText = tr::lng_box_yes(),
|
|
.cancelText = tr::lng_box_no(),
|
|
}));
|
|
return base::EventFilterResult::Cancel;
|
|
});
|
|
};
|
|
const auto handleSubmition = [=](not_null<Ui::Checkbox*> checkbox) {
|
|
handleSubmitionIf([=] {
|
|
return checkbox->checked();
|
|
});
|
|
};
|
|
Ui::Checkbox *deleteOptions = nullptr;
|
|
Ui::Checkbox *deleteMessages = nullptr;
|
|
Controller *deleteMessagesController = nullptr;
|
|
rpl::variable<base::flat_map<PeerId, int>> *deleteMessagesCounts = nullptr;
|
|
Ui::Checkbox *deleteReactions = nullptr;
|
|
Controller *deleteReactionsController = nullptr;
|
|
const auto effectiveCheckedParticipants = [](
|
|
Ui::Checkbox *checkbox,
|
|
Controller *controller) {
|
|
if (!checkbox || !controller || !controller->collectRequests) {
|
|
return Participants();
|
|
} else if (!checkbox->checked()
|
|
&& (controller->data.participants.size() == 1)) {
|
|
return Participants();
|
|
}
|
|
return controller->collectRequests();
|
|
};
|
|
const auto checkedParticipantsValue = [=](
|
|
not_null<Ui::Checkbox*> checkbox,
|
|
not_null<Controller*> controller)
|
|
-> rpl::producer<Participants> {
|
|
if (controller->data.participants.size() == 1) {
|
|
return checkbox->checkedValue() | rpl::map([=](bool) {
|
|
return effectiveCheckedParticipants(checkbox, controller);
|
|
});
|
|
}
|
|
return rpl::merge(
|
|
rpl::single(false),
|
|
controller->checkAllRequests.events(),
|
|
controller->toggleRequestsFromInner.events()
|
|
) | rpl::map([=](bool) {
|
|
return effectiveCheckedParticipants(checkbox, controller);
|
|
});
|
|
};
|
|
|
|
const auto subtitle = box->addRow(
|
|
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
|
box,
|
|
object_ptr<Ui::FlatLabel>(
|
|
box,
|
|
QString(),
|
|
st::boxLabel)));
|
|
subtitle->entity()->setTextColorOverride(st::windowSubTextFg->c);
|
|
subtitle->hide(anim::type::instant);
|
|
Ui::AddSkip(inner);
|
|
if (reportSpam) {
|
|
const auto report = box->addRow(
|
|
object_ptr<Ui::Checkbox>(
|
|
box,
|
|
tr::lng_report_spam(tr::now),
|
|
options.reportSpam,
|
|
st::defaultBoxCheckbox),
|
|
st::boxRowPadding + buttonPadding);
|
|
const auto controller = box->lifetime().make_state<Controller>(
|
|
Controller::Data{ .participants = participants });
|
|
Ui::AddExpandablePeerList(report, controller, inner);
|
|
handleSubmition(report);
|
|
|
|
handleConfirmation(report, controller, [=](
|
|
not_null<PeerData*> p,
|
|
not_null<ChannelData*> c) {
|
|
if (reaction.has_value()
|
|
&& Api::GetReactionReportCapabilities(
|
|
reaction->peer,
|
|
p
|
|
).canReport) {
|
|
Api::ReportReaction(
|
|
box->uiShow(),
|
|
reaction->peer,
|
|
reaction->msgId,
|
|
p);
|
|
} else {
|
|
Api::ReportSpam(p, ids);
|
|
}
|
|
});
|
|
}
|
|
|
|
const auto showMessagesCheckbox = deleteAllMessages;
|
|
const auto showReactionsCheckbox = deleteAllReactions;
|
|
const auto useSingleDeleteOptions = isSingle
|
|
&& showMessagesCheckbox
|
|
&& showReactionsCheckbox;
|
|
if (showMessagesCheckbox || showReactionsCheckbox) {
|
|
Ui::AddSkip(inner);
|
|
Ui::AddSkip(inner);
|
|
const auto checkedParticipants = options.deleteAll
|
|
? participantIds
|
|
: std::vector<PeerId>();
|
|
|
|
if (useSingleDeleteOptions) {
|
|
const auto participant = participants.front();
|
|
Assert(history != nullptr);
|
|
deleteMessagesCounts = box->lifetime().make_state<
|
|
rpl::variable<base::flat_map<PeerId, int>>>(
|
|
base::flat_map<PeerId, int>());
|
|
MessagesCountValue(
|
|
history,
|
|
participants
|
|
) | rpl::on_next([=](base::flat_map<PeerId, int> counts) {
|
|
deleteMessagesCounts->force_assign(std::move(counts));
|
|
}, box->lifetime());
|
|
deleteMessagesController = box->lifetime().make_state<Controller>(
|
|
Controller::Data{
|
|
.messagesCounts = deleteMessagesCounts->value(),
|
|
.participants = Participants{ participant },
|
|
.checked = checkedParticipants,
|
|
});
|
|
deleteReactionsController = box->lifetime().make_state<Controller>(
|
|
Controller::Data{
|
|
.participants = Participants{ participant },
|
|
.checked = checkedParticipants,
|
|
});
|
|
|
|
const auto deleteOptionsSize = Ui::ExpanderButton::ComputeSize(
|
|
DeleteOptionsExpanderText(2, 2));
|
|
const auto deleteOptionsPadding = QMargins(
|
|
0,
|
|
0,
|
|
deleteOptionsSize.width(),
|
|
0);
|
|
deleteOptions = inner->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
inner,
|
|
tr::lng_delete_all_from_user(
|
|
lt_user,
|
|
rpl::single(participant->shortName())),
|
|
options.deleteAll,
|
|
st::defaultBoxCheckbox),
|
|
st::boxRowPadding + deleteOptionsPadding);
|
|
const auto button = Ui::CreateChild<Ui::ExpanderButton>(
|
|
inner,
|
|
DeleteOptionsExpanderText(2, 2));
|
|
button->resize(deleteOptionsSize);
|
|
deleteOptions->geometryValue(
|
|
) | rpl::on_next([=](const QRect &rect) {
|
|
button->moveToRight(
|
|
st::moderateBoxExpandRight,
|
|
rect.top() + (rect.height() - button->height()) / 2,
|
|
inner->width());
|
|
button->raise();
|
|
}, button->lifetime());
|
|
|
|
const auto wrap = inner->add(
|
|
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
|
inner,
|
|
object_ptr<Ui::VerticalLayout>(inner)));
|
|
wrap->toggle(false, anim::type::instant);
|
|
button->setClickedCallback([=] {
|
|
button->checkView()->setChecked(
|
|
!button->checkView()->checked(),
|
|
anim::type::normal);
|
|
wrap->toggle(
|
|
button->checkView()->checked(),
|
|
anim::type::normal);
|
|
});
|
|
|
|
const auto container = wrap->entity();
|
|
const auto optionCheckRect = deleteOptions->checkRect();
|
|
const auto childOptionPadding = st::boxRowPadding
|
|
+ QMargins(
|
|
optionCheckRect.width()
|
|
+ st::defaultBoxCheckbox.textPosition.x()
|
|
- optionCheckRect.x(),
|
|
0,
|
|
0,
|
|
0);
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
deleteMessages = container->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
container,
|
|
tr::lng_delete_sub_messages(tr::now),
|
|
options.deleteAll,
|
|
st::defaultBoxCheckbox),
|
|
childOptionPadding);
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
deleteReactions = container->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
container,
|
|
tr::lng_delete_sub_reactions(tr::now),
|
|
options.deleteAll,
|
|
st::defaultBoxCheckbox),
|
|
childOptionPadding);
|
|
deleteMessagesController->collectRequests = [=] {
|
|
return deleteMessages->checked()
|
|
? Participants{ participant }
|
|
: Participants();
|
|
};
|
|
deleteReactionsController->collectRequests = [=] {
|
|
return deleteReactions->checked()
|
|
? Participants{ participant }
|
|
: Participants();
|
|
};
|
|
const auto updateDeleteOptions = [=] {
|
|
const auto count = (deleteMessages->checked() ? 1 : 0)
|
|
+ (deleteReactions->checked() ? 1 : 0);
|
|
deleteOptions->setChecked(
|
|
count == 2,
|
|
Ui::Checkbox::NotifyAboutChange::DontNotify);
|
|
button->setText(DeleteOptionsExpanderText(count, 2));
|
|
};
|
|
deleteOptions->checkedChanges(
|
|
) | rpl::on_next([=](bool checked) {
|
|
deleteMessages->setChecked(checked);
|
|
deleteReactions->setChecked(checked);
|
|
updateDeleteOptions();
|
|
}, deleteOptions->lifetime());
|
|
deleteMessages->checkedChanges(
|
|
) | rpl::on_next(updateDeleteOptions, deleteMessages->lifetime());
|
|
deleteReactions->checkedChanges(
|
|
) | rpl::on_next(updateDeleteOptions, deleteReactions->lifetime());
|
|
updateDeleteOptions();
|
|
handleSubmitionIf([=] {
|
|
return deleteMessages->checked()
|
|
|| deleteReactions->checked();
|
|
});
|
|
handleConfirmation(
|
|
not_null{ deleteMessages },
|
|
not_null{ deleteMessagesController },
|
|
[=](
|
|
not_null<PeerData*> p,
|
|
not_null<ChannelData*> c) {
|
|
p->session().api().deleteAllFromParticipant(c, p);
|
|
});
|
|
confirms->events() | rpl::on_next([=] {
|
|
if (deleteReactions->checked()
|
|
&& deleteReactionsController->collectRequests
|
|
&& !effectiveCheckedParticipants(
|
|
deleteReactions,
|
|
deleteReactionsController).empty()) {
|
|
for (const auto &participant
|
|
: deleteReactionsController->collectRequests()) {
|
|
const auto useOriginReaction = reaction
|
|
&& (participant == reaction->participant);
|
|
const auto originMsgId = useOriginReaction
|
|
? reaction->msgId
|
|
: MsgId();
|
|
const auto originReaction = useOriginReaction
|
|
? reaction->reaction
|
|
: Data::ReactionId();
|
|
peer->session().api().deleteAllReactionsFromParticipant(
|
|
peer,
|
|
participant,
|
|
originMsgId,
|
|
originReaction);
|
|
}
|
|
}
|
|
}, deleteReactions->lifetime());
|
|
} else {
|
|
if (showMessagesCheckbox) {
|
|
Assert(history != nullptr);
|
|
deleteMessagesCounts = box->lifetime().make_state<
|
|
rpl::variable<base::flat_map<PeerId, int>>>(
|
|
base::flat_map<PeerId, int>());
|
|
MessagesCountValue(
|
|
history,
|
|
participants
|
|
) | rpl::on_next([=](base::flat_map<PeerId, int> counts) {
|
|
deleteMessagesCounts->force_assign(std::move(counts));
|
|
}, box->lifetime());
|
|
deleteMessagesController = box->lifetime().make_state<Controller>(
|
|
Controller::Data{
|
|
.messagesCounts = deleteMessagesCounts->value(),
|
|
.participants = participants,
|
|
.checked = checkedParticipants,
|
|
});
|
|
deleteMessages = inner->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
inner,
|
|
tr::lng_delete_sub_messages(tr::now),
|
|
options.deleteAll,
|
|
st::defaultBoxCheckbox),
|
|
st::boxRowPadding + buttonPadding);
|
|
Ui::AddExpandablePeerList(
|
|
not_null{ deleteMessages },
|
|
not_null{ deleteMessagesController },
|
|
inner);
|
|
handleSubmition(not_null{ deleteMessages });
|
|
handleConfirmation(
|
|
not_null{ deleteMessages },
|
|
not_null{ deleteMessagesController },
|
|
[=](
|
|
not_null<PeerData*> p,
|
|
not_null<ChannelData*> c) {
|
|
p->session().api().deleteAllFromParticipant(c, p);
|
|
});
|
|
}
|
|
|
|
if (deleteMessages && showReactionsCheckbox) {
|
|
Ui::AddSkip(inner);
|
|
Ui::AddSkip(inner);
|
|
}
|
|
|
|
if (showReactionsCheckbox) {
|
|
deleteReactionsController = box->lifetime().make_state<Controller>(
|
|
Controller::Data{
|
|
.participants = participants,
|
|
.checked = checkedParticipants,
|
|
});
|
|
deleteReactions = inner->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
inner,
|
|
tr::lng_delete_sub_reactions(tr::now),
|
|
options.deleteAll,
|
|
st::defaultBoxCheckbox),
|
|
st::boxRowPadding + buttonPadding);
|
|
Ui::AddExpandablePeerList(
|
|
not_null{ deleteReactions },
|
|
not_null{ deleteReactionsController },
|
|
inner);
|
|
handleSubmition(not_null{ deleteReactions });
|
|
confirms->events() | rpl::on_next([=] {
|
|
if (deleteReactions->checked()
|
|
&& deleteReactionsController->collectRequests
|
|
&& !effectiveCheckedParticipants(
|
|
deleteReactions,
|
|
deleteReactionsController).empty()) {
|
|
for (const auto &participant
|
|
: deleteReactionsController->collectRequests()) {
|
|
const auto useOriginReaction = reaction
|
|
&& (participant == reaction->participant);
|
|
peer->session().api()
|
|
.deleteAllReactionsFromParticipant(
|
|
peer,
|
|
participant,
|
|
useOriginReaction ? reaction->msgId : MsgId(),
|
|
useOriginReaction
|
|
? reaction->reaction
|
|
: Data::ReactionId());
|
|
}
|
|
}
|
|
}, deleteReactions->lifetime());
|
|
}
|
|
}
|
|
}
|
|
const auto makeTitleLoadingDescriptor = [] {
|
|
return Lottie::IconDescriptor{
|
|
.name = u"transcribe_loading"_q,
|
|
.color = &st::attentionButtonFg,
|
|
.sizeOverride = Size(st::historyTranscribeLoadingSize),
|
|
.colorizeUsingAlpha = true,
|
|
};
|
|
};
|
|
const auto titleLoadingEmojiData = Ui::Text::LottieEmojiData(
|
|
makeTitleLoadingDescriptor());
|
|
struct MessageTitleData final {
|
|
int count = 0;
|
|
bool resolved = false;
|
|
};
|
|
const auto baseMessagesCount = int(ids.size());
|
|
const auto langUpdated = rpl::single(
|
|
0
|
|
) | rpl::then(Lang::Updated() | rpl::map([] {
|
|
return 0;
|
|
}));
|
|
const auto makeMessageTitleData = [=](
|
|
const base::flat_map<PeerId, int> &messagesCounts,
|
|
const Participants &checked) {
|
|
auto result = MessageTitleData{
|
|
.count = baseMessagesCount,
|
|
.resolved = true,
|
|
};
|
|
for (const auto &peer : checked) {
|
|
const auto i = messagesCounts.find(peer->id);
|
|
if (i == end(messagesCounts)) {
|
|
result.resolved = false;
|
|
} else {
|
|
result.count += i->second;
|
|
}
|
|
if (const auto j = selectedMessagesByParticipant.find(peer->id);
|
|
j != end(selectedMessagesByParticipant)) {
|
|
result.count -= j->second;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
auto title = [&]() -> rpl::producer<TextWithEntities> {
|
|
if (showMessagesCheckbox && !(hasReaction && !hasItems)) {
|
|
auto messageTitleData = rpl::combine(
|
|
deleteMessagesCounts->value(),
|
|
checkedParticipantsValue(
|
|
not_null{ deleteMessages },
|
|
not_null{ deleteMessagesController })
|
|
) | rpl::map(makeMessageTitleData);
|
|
return rpl::combine(
|
|
std::move(messageTitleData),
|
|
rpl::duplicate(langUpdated)
|
|
) | rpl::map([=](const MessageTitleData &data, int) {
|
|
const auto count = data.count;
|
|
const auto resolved = data.resolved;
|
|
const auto text = (count == 1)
|
|
? tr::lng_delete_title_message_one(tr::now)
|
|
: tr::lng_delete_title_message_many(
|
|
tr::now,
|
|
lt_count,
|
|
count);
|
|
if (resolved || count != 0) {
|
|
return TextWithEntities{ text };
|
|
}
|
|
const auto zeroIndex = text.indexOf('0');
|
|
return (zeroIndex == -1)
|
|
? TextWithEntities{ text }
|
|
: TextWithEntities()
|
|
.append(text.mid(0, zeroIndex))
|
|
.append(Ui::Text::LottieEmoji(
|
|
makeTitleLoadingDescriptor()))
|
|
.append(text.mid(zeroIndex + 1));
|
|
});
|
|
} else if (hasReaction && showMessagesCheckbox) {
|
|
auto messageTitleData = rpl::combine(
|
|
deleteMessagesCounts->value(),
|
|
checkedParticipantsValue(
|
|
not_null{ deleteMessages },
|
|
not_null{ deleteMessagesController })
|
|
) | rpl::map(makeMessageTitleData);
|
|
auto deleteReactionsChecked = deleteReactions
|
|
? deleteReactions->checkedValue()
|
|
: rpl::single(false);
|
|
return rpl::combine(
|
|
deleteMessages->checkedValue(),
|
|
std::move(messageTitleData),
|
|
std::move(deleteReactionsChecked),
|
|
rpl::duplicate(langUpdated)
|
|
) | rpl::map([=](
|
|
bool deleteMessagesChecked,
|
|
const MessageTitleData &data,
|
|
bool deleteReactionsChecked,
|
|
int) {
|
|
if (!deleteMessagesChecked) {
|
|
return TextWithEntities{ deleteReactionsChecked
|
|
? tr::lng_delete_title_reaction_all(tr::now)
|
|
: tr::lng_delete_title_reaction_this(tr::now) };
|
|
}
|
|
const auto count = data.count;
|
|
const auto resolved = data.resolved;
|
|
const auto text = (count == 1)
|
|
? tr::lng_delete_title_message_one(tr::now)
|
|
: tr::lng_delete_title_message_many(
|
|
tr::now,
|
|
lt_count,
|
|
count);
|
|
if (resolved || count != 0) {
|
|
return TextWithEntities{ text };
|
|
}
|
|
const auto zeroIndex = text.indexOf('0');
|
|
return (zeroIndex == -1)
|
|
? TextWithEntities{ text }
|
|
: TextWithEntities()
|
|
.append(text.mid(0, zeroIndex))
|
|
.append(Ui::Text::LottieEmoji(
|
|
makeTitleLoadingDescriptor()))
|
|
.append(text.mid(zeroIndex + 1));
|
|
});
|
|
} else if (hasItems) {
|
|
return rpl::duplicate(langUpdated) | rpl::map([=](int) {
|
|
return (itemsCount == 1)
|
|
? TextWithEntities{
|
|
tr::lng_delete_title_message_one(tr::now)
|
|
}
|
|
: TextWithEntities{
|
|
tr::lng_delete_title_message_many(
|
|
tr::now,
|
|
lt_count,
|
|
itemsCount)
|
|
};
|
|
});
|
|
} else if (deleteReactions) {
|
|
return rpl::combine(
|
|
deleteReactions->checkedValue(),
|
|
rpl::duplicate(langUpdated)
|
|
) | rpl::map([=](bool checked, int) {
|
|
return TextWithEntities{ checked
|
|
? tr::lng_delete_title_reaction_all(tr::now)
|
|
: tr::lng_delete_title_reaction_this(tr::now) };
|
|
});
|
|
}
|
|
return rpl::duplicate(langUpdated) | rpl::map([](int) {
|
|
return TextWithEntities{
|
|
tr::lng_delete_title_reaction_this(tr::now)
|
|
};
|
|
});
|
|
}();
|
|
auto titleContext = Core::TextContext({ .session = session });
|
|
auto titleFactory = std::move(titleContext.customEmojiFactory);
|
|
titleContext.customEmojiFactory = [
|
|
titleFactory = std::move(titleFactory),
|
|
titleLoadingEmojiData,
|
|
makeTitleLoadingDescriptor
|
|
](QStringView data, const Ui::Text::MarkedContext &context)
|
|
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
|
if (data == titleLoadingEmojiData) {
|
|
return std::make_unique<Ui::Text::LottieCustomEmoji>(
|
|
makeTitleLoadingDescriptor(),
|
|
context.repaint);
|
|
}
|
|
return titleFactory(data, context);
|
|
};
|
|
box->getDelegate()->setTitle(std::move(title), std::move(titleContext));
|
|
enum class SubtitleKind {
|
|
None,
|
|
ThisReaction,
|
|
SomeReactions,
|
|
AllReactions,
|
|
};
|
|
if (hasItems || (hasReaction && showMessagesCheckbox)) {
|
|
const auto subtitleKind = box->lifetime().make_state<
|
|
rpl::variable<SubtitleKind>>(SubtitleKind::None);
|
|
auto reactionsCheckedValue = showReactionsCheckbox
|
|
? checkedParticipantsValue(
|
|
not_null{ deleteReactions },
|
|
not_null{ deleteReactionsController })
|
|
: rpl::single(Participants());
|
|
auto messageTitleShownValue = [&] {
|
|
return hasItems
|
|
? rpl::single(true)
|
|
: (hasReaction && showMessagesCheckbox)
|
|
? deleteMessages->checkedValue()
|
|
: rpl::single(false);
|
|
}();
|
|
rpl::combine(
|
|
subtitleKind->value(),
|
|
rpl::duplicate(langUpdated)
|
|
) | rpl::map([](SubtitleKind kind, int) {
|
|
switch (kind) {
|
|
case SubtitleKind::ThisReaction:
|
|
return tr::lng_delete_label_also_this_reaction(tr::now);
|
|
case SubtitleKind::SomeReactions:
|
|
return tr::lng_delete_label_also_some_reactions(tr::now);
|
|
case SubtitleKind::AllReactions:
|
|
return tr::lng_delete_label_also_all_reactions(tr::now);
|
|
case SubtitleKind::None:
|
|
return QString();
|
|
}
|
|
Unexpected("Bad subtitle kind.");
|
|
}) | rpl::on_next([=](const QString &text) {
|
|
subtitle->entity()->setText(text);
|
|
}, subtitle->lifetime());
|
|
rpl::combine(
|
|
std::move(reactionsCheckedValue),
|
|
std::move(messageTitleShownValue)
|
|
) | rpl::on_next([=](
|
|
const Participants &checked,
|
|
bool messageTitleShown) {
|
|
auto kind = SubtitleKind::None;
|
|
if (messageTitleShown) {
|
|
if (!checked.empty()) {
|
|
kind = (checked.size() == participants.size())
|
|
? SubtitleKind::AllReactions
|
|
: SubtitleKind::SomeReactions;
|
|
} else if (hasReaction) {
|
|
kind = SubtitleKind::ThisReaction;
|
|
}
|
|
}
|
|
subtitleKind->force_assign(kind);
|
|
subtitle->toggle(kind != SubtitleKind::None, anim::type::normal);
|
|
}, subtitle->lifetime());
|
|
}
|
|
if (banOrRestrict) {
|
|
auto ownedWrap = peer->isMonoforum()
|
|
? nullptr
|
|
: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
|
inner,
|
|
object_ptr<Ui::VerticalLayout>(inner));
|
|
auto computeRestrictions = Fn<ChatRestrictions()>();
|
|
const auto wrap = ownedWrap.data();
|
|
|
|
Ui::AddSkip(inner);
|
|
Ui::AddSkip(inner);
|
|
const auto ban = inner->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
box,
|
|
rpl::conditional(
|
|
(ownedWrap
|
|
? ownedWrap->toggledValue()
|
|
: rpl::single(false) | rpl::type_erased),
|
|
tr::lng_restrict_user(
|
|
lt_count,
|
|
rpl::single(participants.size()) | tr::to_count()),
|
|
rpl::conditional(
|
|
rpl::single(isSingle),
|
|
tr::lng_ban_specific_user(
|
|
lt_user,
|
|
rpl::single(participants.front()->shortName())),
|
|
tr::lng_ban_users())),
|
|
options.banUser,
|
|
st::defaultBoxCheckbox),
|
|
st::boxRowPadding + buttonPadding);
|
|
const auto controller = box->lifetime().make_state<Controller>(
|
|
Controller::Data{ .participants = participants });
|
|
Ui::AddExpandablePeerList(ban, controller, inner);
|
|
handleSubmition(ban);
|
|
|
|
Ui::AddSkip(inner);
|
|
Ui::AddSkip(inner);
|
|
|
|
if (ownedWrap) {
|
|
inner->add(std::move(ownedWrap));
|
|
|
|
const auto container = wrap->entity();
|
|
wrap->toggle(false, anim::type::instant);
|
|
|
|
const auto emojiUp = Ui::Text::IconEmoji(
|
|
&st::moderateBoxExpandIcon);
|
|
const auto emojiDown = Ui::Text::IconEmoji(
|
|
&st::moderateBoxExpandIconDown);
|
|
|
|
auto label = object_ptr<Ui::FlatLabel>(
|
|
inner,
|
|
QString(),
|
|
st::moderateBoxDividerLabel);
|
|
const auto raw = label.data();
|
|
|
|
auto &lifetime = wrap->lifetime();
|
|
const auto scrollLifetime = lifetime.make_state<rpl::lifetime>();
|
|
label->setClickHandlerFilter([=](
|
|
const ClickHandlerPtr &handler,
|
|
Qt::MouseButton button) {
|
|
if (button != Qt::LeftButton) {
|
|
return false;
|
|
}
|
|
wrap->toggle(!wrap->toggled(), anim::type::normal);
|
|
{
|
|
inner->heightValue() | rpl::on_next([=] {
|
|
if (!wrap->animating()) {
|
|
scrollLifetime->destroy();
|
|
Ui::PostponeCall(crl::guard(box, [=] {
|
|
box->scrollToY(std::numeric_limits<int>::max());
|
|
}));
|
|
} else {
|
|
box->scrollToY(std::numeric_limits<int>::max());
|
|
}
|
|
}, *scrollLifetime);
|
|
}
|
|
return true;
|
|
});
|
|
wrap->toggledValue(
|
|
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
|
|
return ((toggled && isSingle)
|
|
? tr::lng_restrict_user_full
|
|
: (toggled && !isSingle)
|
|
? tr::lng_restrict_users_full
|
|
: isSingle
|
|
? tr::lng_restrict_user_part
|
|
: tr::lng_restrict_users_part)(
|
|
lt_emoji,
|
|
rpl::single(toggled ? emojiUp : emojiDown),
|
|
tr::marked);
|
|
}) | rpl::flatten_latest(
|
|
) | rpl::on_next([=](const TextWithEntities &text) {
|
|
raw->setMarkedText(tr::link(text, u"internal:"_q));
|
|
}, label->lifetime());
|
|
|
|
Ui::AddSkip(inner);
|
|
inner->add(object_ptr<Ui::DividerLabel>(
|
|
inner,
|
|
std::move(label),
|
|
st::defaultBoxDividerLabelPadding));
|
|
|
|
using Flag = ChatRestriction;
|
|
using Flags = ChatRestrictions;
|
|
const auto chat = peer->asChat();
|
|
const auto channel = peer->asChannel();
|
|
const auto defaultRestrictions = chat
|
|
? chat->defaultRestrictions()
|
|
: channel->defaultRestrictions();
|
|
const auto prepareFlags = FixDependentRestrictions(
|
|
defaultRestrictions
|
|
| ((channel && channel->isPublic())
|
|
? (Flag::ChangeInfo | Flag::PinMessages)
|
|
: Flags(0)));
|
|
const auto disabledMessages = [&] {
|
|
auto result = base::flat_map<Flags, QString>();
|
|
{
|
|
const auto disabled = FixDependentRestrictions(
|
|
defaultRestrictions
|
|
| ((channel && channel->isPublic())
|
|
? (Flag::ChangeInfo | Flag::PinMessages)
|
|
: Flags(0)));
|
|
result.emplace(
|
|
disabled,
|
|
tr::lng_rights_restriction_for_all(tr::now));
|
|
}
|
|
return result;
|
|
}();
|
|
|
|
auto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions(
|
|
box,
|
|
prepareFlags,
|
|
disabledMessages,
|
|
{ .isForum = peer->isForum(), .isUserSpecific = true });
|
|
computeRestrictions = getRestrictions;
|
|
std::move(changes) | rpl::on_next([=] {
|
|
ban->setChecked(true);
|
|
}, ban->lifetime());
|
|
Ui::AddSkip(container);
|
|
Ui::AddDivider(container);
|
|
Ui::AddSkip(container);
|
|
Ui::AddSubsectionTitle(
|
|
container,
|
|
rpl::conditional(
|
|
rpl::single(isSingle),
|
|
tr::lng_restrict_users_part_single_header(),
|
|
tr::lng_restrict_users_part_header(
|
|
lt_count,
|
|
rpl::single(participants.size()) | tr::to_count())));
|
|
container->add(std::move(checkboxes));
|
|
}
|
|
|
|
// Handle confirmation manually.
|
|
confirms->events() | rpl::on_next([=] {
|
|
if (ban->checked() && controller->collectRequests) {
|
|
const auto kick = !wrap || !wrap->toggled();
|
|
const auto restrictions = computeRestrictions
|
|
? computeRestrictions()
|
|
: ChatRestrictions();
|
|
const auto request = [=](
|
|
not_null<PeerData*> peer,
|
|
not_null<ChannelData*> channel) {
|
|
if (base::IsAltPressed() || base::IsCtrlPressed()) {
|
|
return;
|
|
}
|
|
if (!kick) {
|
|
Api::ChatParticipants::Restrict(
|
|
channel,
|
|
peer,
|
|
ChatRestrictionsInfo(), // Unused.
|
|
ChatRestrictionsInfo(restrictions, 0),
|
|
nullptr,
|
|
nullptr);
|
|
} else {
|
|
const auto block = channel->isMonoforum()
|
|
? channel->monoforumBroadcast()
|
|
: channel.get();
|
|
if (block) {
|
|
block->session().api().chatParticipants().kick(
|
|
block,
|
|
peer,
|
|
{ block->restrictions(), 0 });
|
|
}
|
|
}
|
|
};
|
|
if (collectCommon && !collectCommon->empty()) {
|
|
sequentiallyRequest(
|
|
request,
|
|
controller->collectRequests(),
|
|
*collectCommon);
|
|
} else {
|
|
sequentiallyRequest(
|
|
request,
|
|
controller->collectRequests());
|
|
}
|
|
}
|
|
}, ban->lifetime());
|
|
}
|
|
|
|
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
|
box->addButton(tr::lng_box_delete(), [=] {
|
|
confirms->fire({});
|
|
if (confirmed) {
|
|
confirmed();
|
|
}
|
|
if (hasItems) {
|
|
session->data().histories().deleteMessages(ids, true);
|
|
session->data().sendHistoryChangeNotifications();
|
|
}
|
|
const auto deleteThisReaction = reaction
|
|
&& !ranges::contains(
|
|
effectiveCheckedParticipants(
|
|
deleteReactions,
|
|
deleteReactionsController),
|
|
reaction->participant);
|
|
if (deleteThisReaction) {
|
|
session->api().deleteParticipantReaction(
|
|
reaction->peer,
|
|
reaction->msgId,
|
|
reaction->participant,
|
|
reaction->reaction);
|
|
}
|
|
close();
|
|
});
|
|
box->addButton(tr::lng_cancel(), close);
|
|
}
|
|
|
|
bool CanCreateModerateMessagesBox(const HistoryItemsList &items) {
|
|
const auto options = CalculateModerateOptions(items);
|
|
return HasModerateActions(options)
|
|
&& !options.participants.empty();
|
|
}
|
|
|
|
void SafeSubmitOnEnter(not_null<Ui::GenericBox*> box) {
|
|
base::install_event_filter(box, [=](not_null<QEvent*> event) {
|
|
if (event->type() == QEvent::KeyPress) {
|
|
if (const auto k = static_cast<QKeyEvent*>(event.get())) {
|
|
if ((k->key() == Qt::Key_Enter)
|
|
|| (k->key() == Qt::Key_Return)) {
|
|
box->uiShow()->show(Ui::MakeConfirmBox({
|
|
.text = tr::lng_gigagroup_warning_title(),
|
|
.confirmed = [=](Fn<void()> close) {
|
|
box->triggerButton(0);
|
|
close();
|
|
},
|
|
.confirmText = tr::lng_box_yes(),
|
|
.cancelText = tr::lng_box_no(),
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
return base::EventFilterResult::Continue;
|
|
});
|
|
}
|
|
|
|
void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) {
|
|
const auto container = box->verticalLayout();
|
|
|
|
const auto userpicPeer = peer->userpicPaintingPeer();
|
|
const auto maybeUser = peer->asUser();
|
|
const auto isBot = maybeUser && maybeUser->isBot();
|
|
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
|
|
SafeSubmitOnEnter(box);
|
|
|
|
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
|
|
container,
|
|
userpicPeer,
|
|
st::mainMenuUserpic,
|
|
peer->userpicShape());
|
|
userpic->showSavedMessagesOnSelf(true);
|
|
Ui::IconWithTitle(
|
|
container,
|
|
userpic,
|
|
Ui::CreateChild<Ui::FlatLabel>(
|
|
container,
|
|
peer->isSelf()
|
|
? tr::lng_saved_messages(tr::bold)
|
|
: maybeUser
|
|
? tr::lng_profile_delete_conversation(tr::bold)
|
|
: rpl::single(
|
|
tr::bold(userpicPeer->name())
|
|
) | rpl::type_erased,
|
|
box->getDelegate()->style().title));
|
|
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
|
|
box->addRow(
|
|
object_ptr<Ui::FlatLabel>(
|
|
container,
|
|
peer->isSelf()
|
|
? tr::lng_sure_delete_saved_messages()
|
|
: maybeUser
|
|
? tr::lng_sure_delete_history(
|
|
lt_contact,
|
|
rpl::single(peer->name()))
|
|
: (peer->isChannel() && !peer->isMegagroup())
|
|
? tr::lng_sure_leave_channel()
|
|
: tr::lng_sure_leave_group(),
|
|
st::boxLabel));
|
|
|
|
const auto maybeCheckbox = [&]() -> Ui::Checkbox* {
|
|
if (!peer->canRevokeFullHistory()) {
|
|
return nullptr;
|
|
}
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
return box->addRow(
|
|
object_ptr<Ui::Checkbox>(
|
|
container,
|
|
maybeUser
|
|
? tr::lng_delete_for_other_check(
|
|
tr::now,
|
|
lt_user,
|
|
TextWithEntities{ maybeUser->firstName },
|
|
tr::rich)
|
|
: tr::lng_delete_for_everyone_check(
|
|
tr::now,
|
|
tr::marked),
|
|
false,
|
|
st::defaultBoxCheckbox));
|
|
}();
|
|
|
|
const auto maybeBotCheckbox = [&]() -> Ui::Checkbox* {
|
|
if (!isBot) {
|
|
return nullptr;
|
|
}
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
return box->addRow(
|
|
object_ptr<Ui::Checkbox>(
|
|
container,
|
|
tr::lng_profile_block_bot(tr::now, tr::marked),
|
|
false,
|
|
st::defaultBoxCheckbox));
|
|
}();
|
|
|
|
const auto removeFromChatsFilters = [=](
|
|
not_null<History*> history) -> std::vector<FilterId> {
|
|
auto result = std::vector<FilterId>();
|
|
for (const auto &filter : peer->owner().chatsFilters().list()) {
|
|
if (filter.withoutAlways(history) != filter) {
|
|
result.push_back(filter.id());
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const auto maybeChatsFiltersCheckbox = [&]() -> Ui::Checkbox* {
|
|
const auto history = (isBot || !maybeUser)
|
|
? peer->owner().history(peer).get()
|
|
: nullptr;
|
|
if (!history || removeFromChatsFilters(history).empty()) {
|
|
return nullptr;
|
|
}
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
return box->addRow(
|
|
object_ptr<Ui::Checkbox>(
|
|
container,
|
|
(maybeBotCheckbox
|
|
? tr::lng_filters_checkbox_remove_bot
|
|
: (peer->isChannel() && !peer->isMegagroup())
|
|
? tr::lng_filters_checkbox_remove_channel
|
|
: tr::lng_filters_checkbox_remove_group)(
|
|
tr::now,
|
|
tr::marked),
|
|
false,
|
|
st::defaultBoxCheckbox));
|
|
}();
|
|
|
|
Ui::AddSkip(container);
|
|
|
|
auto buttonText = maybeUser
|
|
? tr::lng_box_delete()
|
|
: !maybeCheckbox
|
|
? tr::lng_box_leave()
|
|
: maybeCheckbox->checkedValue() | rpl::map([](bool checked) {
|
|
return checked ? tr::lng_box_delete() : tr::lng_box_leave();
|
|
}) | rpl::flatten_latest();
|
|
|
|
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
|
box->addButton(std::move(buttonText), [=] {
|
|
const auto revoke = maybeCheckbox && maybeCheckbox->checked();
|
|
const auto stopBot = maybeBotCheckbox && maybeBotCheckbox->checked();
|
|
const auto removeFromChats = maybeChatsFiltersCheckbox
|
|
&& maybeChatsFiltersCheckbox->checked();
|
|
Core::App().closeChatFromWindows(peer);
|
|
if (stopBot) {
|
|
peer->session().api().blockedPeers().block(peer);
|
|
}
|
|
if (removeFromChats) {
|
|
const auto history = peer->owner().history(peer).get();
|
|
const auto removeFrom = removeFromChatsFilters(history);
|
|
for (const auto &filter : peer->owner().chatsFilters().list()) {
|
|
if (!ranges::contains(removeFrom, filter.id())) {
|
|
continue;
|
|
}
|
|
const auto result = filter.withoutAlways(history);
|
|
if (result == filter) {
|
|
continue;
|
|
}
|
|
const auto tl = result.tl();
|
|
peer->owner().chatsFilters().apply(MTP_updateDialogFilter(
|
|
MTP_flags(MTPDupdateDialogFilter::Flag::f_filter),
|
|
MTP_int(filter.id()),
|
|
tl));
|
|
peer->session().api().request(MTPmessages_UpdateDialogFilter(
|
|
MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),
|
|
MTP_int(filter.id()),
|
|
tl
|
|
)).send();
|
|
}
|
|
}
|
|
// Don't delete old history by default,
|
|
// because Android app doesn't.
|
|
//
|
|
//if (const auto from = peer->migrateFrom()) {
|
|
// peer->session().api().deleteConversation(from, false);
|
|
//}
|
|
peer->session().api().deleteConversation(peer, revoke);
|
|
close();
|
|
}, st::attentionBoxButton);
|
|
box->addButton(tr::lng_cancel(), close);
|
|
}
|
|
|
|
void DeleteSublistBox(
|
|
not_null<Ui::GenericBox*> box,
|
|
not_null<Data::SavedSublist*> sublist) {
|
|
const auto container = box->verticalLayout();
|
|
|
|
const auto weak = base::make_weak(sublist.get());
|
|
const auto peer = sublist->sublistPeer();
|
|
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
|
|
SafeSubmitOnEnter(box);
|
|
|
|
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
|
|
container,
|
|
peer,
|
|
st::mainMenuUserpic);
|
|
Ui::IconWithTitle(
|
|
container,
|
|
userpic,
|
|
Ui::CreateChild<Ui::FlatLabel>(
|
|
container,
|
|
tr::lng_profile_delete_conversation(tr::bold),
|
|
box->getDelegate()->style().title));
|
|
|
|
Ui::AddSkip(container);
|
|
Ui::AddSkip(container);
|
|
|
|
box->addRow(
|
|
object_ptr<Ui::FlatLabel>(
|
|
container,
|
|
tr::lng_sure_delete_history(
|
|
lt_contact,
|
|
rpl::single(peer->name())),
|
|
st::boxLabel));
|
|
|
|
Ui::AddSkip(container);
|
|
|
|
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
|
box->addButton(tr::lng_box_delete(), [=] {
|
|
const auto strong = weak.get();
|
|
const auto parentChat = strong ? strong->parentChat() : nullptr;
|
|
if (!parentChat) {
|
|
return;
|
|
}
|
|
peer->session().api().deleteSublistHistory(parentChat, peer);
|
|
close();
|
|
}, st::attentionBoxButton);
|
|
box->addButton(tr::lng_cancel(), close);
|
|
}
|
|
|
|
ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions() {
|
|
return base::IsCtrlPressed()
|
|
? ModerateMessagesBoxOptions{
|
|
.reportSpam = true,
|
|
.deleteAll = true,
|
|
.banUser = true,
|
|
}
|
|
: ModerateMessagesBoxOptions{};
|
|
}
|