/* 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/choose_peer_box.h" #include "apiwrap.h" // ApiWrap::botCommonGroups / requestBotCommonGroups. #include "boxes/add_contact_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/premium_limits_box.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_peer.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item_reply_markup.h" #include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "main/main_session.h" // Session::api(). #include "ui/boxes/confirm_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" namespace { class ChoosePeerBoxController final : public ChatsListBoxController , public base::has_weak_ptr { public: ChoosePeerBoxController( not_null navigation, not_null bot, RequestPeerQuery query, Fn>)> callback); Main::Session &session() const override; void rowClicked(not_null row) override; [[nodiscard]] rpl::producer selectedCountValue() const; void submit(); QString savedMessagesChatStatus() const override { return tr::lng_saved_forward_here(tr::now); } private: void prepareViewHook() override; std::unique_ptr createRow(not_null history) override; QString emptyBoxText() const override; void prepareRestrictions(); const not_null _navigation; not_null _bot; RequestPeerQuery _query; base::flat_set> _commonGroups; base::flat_set> _selected; rpl::variable _selectedCount; Fn>)> _callback; }; using RightsMap = std::vector>>; [[nodiscard]] RightsMap GroupRights() { using Flag = ChatAdminRight; return { { Flag::ChangeInfo, tr::lng_request_group_change_info }, { Flag::DeleteMessages, tr::lng_request_group_delete_messages }, { Flag::BanUsers, tr::lng_request_group_ban_users }, { Flag::InviteByLinkOrAdd, tr::lng_request_group_invite }, { Flag::PinMessages, tr::lng_request_group_pin_messages }, { Flag::ManageTopics, tr::lng_request_group_manage_topics }, { Flag::ManageCall, tr::lng_request_group_manage_video_chats }, { Flag::Anonymous, tr::lng_request_group_anonymous }, { Flag::AddAdmins, tr::lng_request_group_add_admins }, }; } [[nodiscard]] RightsMap BroadcastRights() { using Flag = ChatAdminRight; return { { Flag::ChangeInfo, tr::lng_request_channel_change_info }, { Flag::PostMessages, tr::lng_request_channel_post_messages }, { Flag::EditMessages, tr::lng_request_channel_edit_messages }, { Flag::DeleteMessages, tr::lng_request_channel_delete_messages }, { Flag::InviteByLinkOrAdd, tr::lng_request_channel_add_subscribers }, { Flag::ManageCall, tr::lng_request_channel_manage_livestreams }, { Flag::AddAdmins, tr::lng_request_channel_add_admins }, }; } [[nodiscard]] QString RightsText( ChatAdminRights rights, const RightsMap &phrases) { auto list = QStringList(); for (const auto &[flag, phrase] : phrases) { if (rights & flag) { list.push_back(phrase(tr::now)); } } const auto count = list.size(); if (!count) { return QString(); } const auto last = list.back(); return (count > 1) ? tr::lng_request_peer_rights_and( tr::now, lt_rights, list.mid(0, count - 1).join(", "), lt_last, last) : last; } [[nodiscard]] QString GroupRightsText(ChatAdminRights rights) { return RightsText(rights, GroupRights()); } [[nodiscard]] QString BroadcastRightsText(ChatAdminRights rights) { return RightsText(rights, BroadcastRights()); } [[nodiscard]] QStringList RestrictionsList(RequestPeerQuery query) { using Type = RequestPeerQuery::Type; using Restriction = RequestPeerQuery::Restriction; auto result = QStringList(); const auto addRestriction = [&]( Restriction value, tr::phrase<> yes, tr::phrase<> no) { if (value == Restriction::Yes) { result.push_back(yes(tr::now)); } else if (value == Restriction::No) { result.push_back(no(tr::now)); } }; const auto addRights = [&](const QString &rights) { if (!rights.isEmpty()) { result.push_back( tr::lng_request_peer_rights(tr::now, lt_rights, rights)); } }; switch (query.type) { case Type::User: if (query.userIsBot != Restriction::Yes) { addRestriction( query.userIsPremium, tr::lng_request_user_premium_yes, tr::lng_request_user_premium_no); } break; case Type::Group: addRestriction( query.hasUsername, tr::lng_request_group_public_yes, tr::lng_request_group_public_no); addRestriction( query.groupIsForum, tr::lng_request_group_topics_yes, tr::lng_request_group_topics_no); if (query.amCreator) { result.push_back(tr::lng_request_group_am_owner(tr::now)); } else { addRights(GroupRightsText(query.myRights)); } break; case Type::Broadcast: addRestriction( query.hasUsername, tr::lng_request_channel_public_yes, tr::lng_request_channel_public_no); if (query.amCreator) { result.push_back(tr::lng_request_channel_am_owner(tr::now)); } else { addRights(BroadcastRightsText(query.myRights)); } break; } return result; } object_ptr MakeConfirmBox( not_null bot, not_null peer, RequestPeerQuery query, Fn confirmed) { const auto name = peer->name(); const auto botName = bot->name(); auto text = tr::lng_request_peer_confirm( tr::now, lt_chat, Ui::Text::Bold(name), lt_bot, Ui::Text::Bold(botName), Ui::Text::WithEntities); if (!peer->isUser()) { const auto rights = peer->isBroadcast() ? BroadcastRightsText(query.botRights) : GroupRightsText(query.botRights); if (!rights.isEmpty()) { text.append('\n').append('\n').append( tr::lng_request_peer_confirm_rights( tr::now, lt_bot, Ui::Text::Bold(botName), lt_chat, Ui::Text::Bold(name), lt_rights, TextWithEntities{ rights }, Ui::Text::WithEntities)); } else if (!peer->isBroadcast() && query.isBotParticipant) { const auto common = bot->session().api().botCommonGroups(bot); if (!common || !ranges::contains(*common, peer)) { text.append('\n').append('\n').append( tr::lng_request_peer_confirm_add( tr::now, lt_bot, Ui::Text::Bold(botName), lt_chat, Ui::Text::Bold(name), Ui::Text::WithEntities)); } } } return Ui::MakeConfirmBox({ .text = std::move(text), .confirmed = [=](Fn close) { confirmed(); close(); }, .confirmText = tr::lng_request_peer_confirm_send(tr::now), }); } object_ptr CreatePeerByQueryBox( not_null navigation, not_null bot, RequestPeerQuery query, Fn>)> done) { const auto weak = std::make_shared>(); auto callback = [=](not_null peer) { done({ peer }); if (const auto strong = weak->data()) { strong->closeBox(); } }; auto result = Box( navigation, bot, query, std::move(callback)); *weak = result.data(); return result; } [[nodiscard]] bool FilterPeerByQuery( not_null peer, RequestPeerQuery query, const base::flat_set> &commonGroups) { using Type = RequestPeerQuery::Type; using Restriction = RequestPeerQuery::Restriction; const auto checkRestriction = [](Restriction restriction, bool value) { return (restriction == Restriction::Any) || ((restriction == Restriction::Yes) == value); }; const auto checkRights = []( ChatAdminRights wanted, bool creator, ChatAdminRights rights) { return creator || ((rights & wanted) == wanted); }; switch (query.type) { case Type::User: { const auto user = peer->asUser(); return user && checkRestriction(query.userIsBot, user->isBot()) && checkRestriction(query.userIsPremium, user->isPremium()); } case Type::Group: { const auto chat = peer->asChat(); const auto megagroup = peer->asMegagroup(); return (chat || megagroup) && (!query.amCreator || (chat ? chat->amCreator() : megagroup->amCreator())) && checkRestriction(query.groupIsForum, peer->isForum()) && checkRestriction( query.hasUsername, megagroup && megagroup->hasUsername()) && checkRights( query.myRights, chat ? chat->amCreator() : megagroup->amCreator(), chat ? chat->adminRights() : megagroup->adminRights()) && (!query.isBotParticipant || query.myRights || commonGroups.contains(peer) || (chat ? chat->canAddMembers() : megagroup->canAddMembers())); } case Type::Broadcast: { const auto broadcast = peer->asBroadcast(); return broadcast && (!query.amCreator || broadcast->amCreator()) && checkRestriction(query.hasUsername, broadcast->hasUsername()) && checkRights( query.myRights, broadcast->amCreator(), broadcast->adminRights()); } } Unexpected("Type in FilterPeerByQuery."); } ChoosePeerBoxController::ChoosePeerBoxController( not_null navigation, not_null bot, RequestPeerQuery query, Fn>)> callback) : ChatsListBoxController(&navigation->session()) , _navigation(navigation) , _bot(bot) , _query(query) , _callback(std::move(callback)) { if (const auto list = _bot->session().api().botCommonGroups(_bot)) { _commonGroups = { begin(*list), end(*list) }; } } Main::Session &ChoosePeerBoxController::session() const { return _navigation->session(); } void ChoosePeerBoxController::prepareRestrictions() { auto above = object_ptr((QWidget*)nullptr); const auto raw = above.data(); auto rows = RestrictionsList(_query); if (!rows.empty()) { Ui::AddSubsectionTitle( raw, tr::lng_request_peer_requirements(), { 0, st::membersMarginTop, 0, 0 }); const auto skip = st::defaultSubsectionTitlePadding.left(); auto separator = QString::fromUtf8("\n\xE2\x80\xA2 "); raw->add( object_ptr( raw, separator + rows.join(separator), st::requestPeerRestriction), { skip, 0, skip, st::membersMarginTop }); Ui::AddDivider(raw); } const auto make = [&](tr::phrase<> text, const style::icon &st) { auto button = raw->add( object_ptr( raw, text(), st::inviteViaLinkButton), { 0, st::membersMarginTop, 0, 0 }); const auto icon = Ui::CreateChild( button, st, QPoint()); button->heightValue( ) | rpl::start_with_next([=](int height) { icon->moveToLeft( st::choosePeerCreateIconLeft, (height - st::inviteViaLinkIcon.height()) / 2); }, icon->lifetime()); button->setClickedCallback([=] { _navigation->parentController()->show( CreatePeerByQueryBox(_navigation, _bot, _query, _callback)); }); button->events( ) | rpl::filter([=](not_null e) { return (e->type() == QEvent::Enter); }) | rpl::start_with_next([=] { delegate()->peerListMouseLeftGeometry(); }, button->lifetime()); return button; }; if (_query.type == RequestPeerQuery::Type::Group) { make(tr::lng_request_group_create, st::choosePeerGroupIcon); } else if (_query.type == RequestPeerQuery::Type::Broadcast) { make(tr::lng_request_channel_create, st::choosePeerChannelIcon); } if (raw->count() > 0) { delegate()->peerListSetAboveWidget(std::move(above)); } } void ChoosePeerBoxController::prepareViewHook() { delegate()->peerListSetTitle([&] { using Type = RequestPeerQuery::Type; using Restriction = RequestPeerQuery::Restriction; switch (_query.type) { case Type::User: return (_query.userIsBot == Restriction::Yes) ? tr::lng_request_bot_title() : (_query.maxQuantity > 1) ? tr::lng_request_users_title() : tr::lng_request_user_title(); case Type::Group: return tr::lng_request_group_title(); case Type::Broadcast: return tr::lng_request_channel_title(); } Unexpected("Type in RequestPeerQuery."); }()); prepareRestrictions(); } void ChoosePeerBoxController::rowClicked(not_null row) { const auto limit = _query.maxQuantity; const auto multiselect = (limit > 1); const auto peer = row->peer(); if (multiselect) { if (_selected.contains(peer) || _selected.size() < limit) { delegate()->peerListSetRowChecked(row, !row->checked()); if (row->checked()) { _selected.emplace(peer); } else { _selected.remove(peer); } _selectedCount = int(_selected.size()); } return; } const auto done = [callback = _callback, peer] { const auto onstack = callback; onstack({ peer }); }; if (const auto user = peer->asUser()) { done(); } else { delegate()->peerListUiShow()->showBox( MakeConfirmBox(_bot, peer, _query, done)); } } rpl::producer ChoosePeerBoxController::selectedCountValue() const { return _selectedCount.value(); } void ChoosePeerBoxController::submit() { const auto onstack = _callback; onstack(ranges::to_vector(_selected)); } auto ChoosePeerBoxController::createRow(not_null history) -> std::unique_ptr { return FilterPeerByQuery(history->peer, _query, _commonGroups) ? std::make_unique(history) : nullptr; } QString ChoosePeerBoxController::emptyBoxText() const { using Type = RequestPeerQuery::Type; using Restriction = RequestPeerQuery::Restriction; const auto result = [](tr::phrase<> title, tr::phrase<> text) { return title(tr::now) + "\n\n" + text(tr::now); }; switch (_query.type) { case Type::User: return (_query.userIsBot == Restriction::Yes) ? result(tr::lng_request_bot_no, tr::lng_request_bot_no_about) : result(tr::lng_request_user_no, tr::lng_request_user_no_about); case Type::Group: return result( tr::lng_request_group_no, tr::lng_request_group_no_about); case Type::Broadcast: return result( tr::lng_request_channel_no, tr::lng_request_channel_no_about); } Unexpected("Type in ChoosePeerBoxController::emptyBoxText."); } } // namespace void ShowChoosePeerBox( not_null navigation, not_null bot, RequestPeerQuery query, Fn>)> chosen) { const auto needCommonGroups = query.isBotParticipant && (query.type == RequestPeerQuery::Type::Group) && !query.myRights; if (needCommonGroups && !bot->session().api().botCommonGroups(bot)) { const auto weak = base::make_weak(navigation); bot->session().api().requestBotCommonGroups(bot, [=] { if (const auto strong = weak.get()) { ShowChoosePeerBox(strong, bot, query, chosen); } }); return; } const auto weak = std::make_shared>(); auto callback = [=, done = std::move(chosen)]( std::vector> peers) { done(std::move(peers)); if (const auto strong = weak->data()) { strong->closeBox(); } }; const auto limit = query.maxQuantity; auto controller = std::make_unique( navigation, bot, query, std::move(callback)); auto initBox = [=, ptr = controller.get()](not_null box) { ptr->selectedCountValue() | rpl::start_with_next([=](int count) { box->clearButtons(); if (limit > 1) { box->setAdditionalTitle(rpl::single(u"%1 / %2"_q.arg(count).arg(limit))); } if (count > 0) { box->addButton(tr::lng_intro_submit(), [=] { ptr->submit(); if (*weak) { (*weak)->closeBox(); } }); } box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); }, box->lifetime()); }; *weak = navigation->parentController()->show(Box( std::move(controller), std::move(initBox))); }