diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 633722197f..2c6a0a1969 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -178,6 +178,8 @@ PRIVATE boxes/peers/add_bot_to_chat_box.h boxes/peers/add_participants_box.cpp boxes/peers/add_participants_box.h + boxes/peers/choose_peer_box.cpp + boxes/peers/choose_peer_box.h boxes/peers/edit_contact_box.cpp boxes/peers/edit_contact_box.h boxes/peers/edit_forum_topic_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d4cd29b936..08e541bec9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3684,6 +3684,50 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_forum_messages#other" = "{count} messages"; "lng_forum_show_topics_list" = "Show Topics List"; +"lng_request_peer_requirements" = "Requirements"; +"lng_request_peer_rights" = "You should have the admin rights to {rights}."; +"lng_request_peer_rights_and" = "{rights} and {last}"; +"lng_request_user_title" = "Choose User"; +"lng_request_user_premium_yes" = "The user should have a Premium subscription."; +"lng_request_user_premium_no" = "The user shouldn't have a Premium subscription."; +"lng_request_user_no" = "No such users"; +"lng_request_user_no_about" = "You don't have users that meet the requirements for this bot."; +"lng_request_bot_title" = "Choose Bot"; +"lng_request_bot_no" = "No bots"; +"lng_request_bot_no_about" = "You don't have any bots."; +"lng_request_group_title" = "Choose Group"; +"lng_request_group_no" = "No such groups"; +"lng_request_group_no_about" = "You don't have groups that meet the requirements for this bot."; +"lng_request_group_public_yes" = "The group should be public."; +"lng_request_group_public_no" = "The group should be private."; +"lng_request_group_topics_yes" = "The group should have topics turned on."; +"lng_request_group_topics_no" = "The group should have topics turned off."; +"lng_request_group_am_owner" = "You should be the owner of the group."; +"lng_request_group_change_info" = "change group info"; +"lng_request_group_delete_messages" = "delete messages"; +"lng_request_group_ban_users" = "ban users"; +"lng_request_group_invite" = "invite users via link"; +"lng_request_group_pin_messages" = "pin messages"; +"lng_request_group_manage_topics" = "manage topics"; +"lng_request_group_manage_video_chats" = "manage video chats"; +"lng_request_group_anonymous" = "remain anonymous"; +"lng_request_group_add_admins" = "add new admins"; +"lng_request_group_create" = "Create a New Group for This"; +"lng_request_channel_title" = "Choose Channel"; +"lng_request_channel_no" = "No such channels"; +"lng_request_channel_no_about" = "You don't have channels that meet the requirements for this bot."; +"lng_request_channel_public_yes" = "The channel should be public."; +"lng_request_channel_public_no" = "The channel should be private."; +"lng_request_channel_am_owner" = "You should be the owner of the channel."; +"lng_request_channel_change_info" = "change channel info"; +"lng_request_channel_post_messages" = "post messages"; +"lng_request_channel_edit_messages" = "edit messages of others"; +"lng_request_channel_delete_messages" = "delete messages"; +"lng_request_channel_add_subscribers" = "add subscribers"; +"lng_request_channel_manage_livestreams" = "manage live streams"; +"lng_request_channel_add_admins" = "add new admins"; +"lng_request_channel_create" = "Create a New Channel for This"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index b5782b10ed..9e6e182eb8 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "boxes/passcode_box.h" #include "boxes/url_auth_box.h" +#include "boxes/peers/choose_peer_box.h" #include "lang/lang_keys.h" #include "core/core_cloud_password.h" #include "core/click_handler_types.h" @@ -404,8 +405,26 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { disabled); } break; - case ButtonType::RequestPeer: { // #TODO request_peer + case ButtonType::RequestPeer: { HideSingleUseKeyboard(controller, item); + + auto query = RequestPeerQuery(); + Assert(button->data.size() == sizeof(query)); + memcpy(&query, button->data.data(), sizeof(query)); + const auto peer = item->history()->peer; + const auto itemId = item->id; + const auto id = int32(button->buttonId); + const auto chosen = [=](not_null result) { + peer->session().api().request(MTPmessages_SendBotRequestedPeer( + peer->input, + MTP_int(itemId), + MTP_int(id), + result->input + )).done([=](const MTPUpdates &result) { + peer->session().api().applyUpdates(result); + }).send(); + }; + ShowChoosePeerBox(controller, chosen, query); } break; case ButtonType::SwitchInlineSame: diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp new file mode 100644 index 0000000000..b49d54f167 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -0,0 +1,375 @@ +/* +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 "boxes/peer_list_controllers.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 "settings/settings_common.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_settings.h" + +namespace { + +class ChoosePeerBoxController final + : public ChatsListBoxController + , public base::has_weak_ptr { +public: + ChoosePeerBoxController( + not_null session, + Fn)> callback, + RequestPeerQuery query); + + Main::Session &session() const override; + void rowClicked(not_null row) override; + + bool respectSavedMessagesChat() const override { + return true; + } + +private: + void prepareViewHook() override; + std::unique_ptr createRow(not_null history) override; + QString emptyBoxText() const override; + + void prepareRestrictions(); + + const not_null _session; + Fn)> _callback; + RequestPeerQuery _query; + +}; + +[[nodiscard]] QStringList RestrictionsList(RequestPeerQuery query) { + using Flag = ChatAdminRight; + 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 = [&]( + ChatAdminRights rights, + std::vector>> 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; + } + const auto last = list.back(); + const auto full = (count > 1) + ? tr::lng_request_peer_rights_and( + tr::now, + lt_rights, + list.mid(0, count - 1).join(", "), + lt_last, + last) + : last; + result.push_back( + tr::lng_request_peer_rights(tr::now, lt_rights, full)); + }; + 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(query.myRights, { + { 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 }, + }); + } + 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(query.myRights, { + { 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 }, + }); + } + break; + } + return result; +} + +object_ptr CreatePeerByQueryBox( + not_null session, + RequestPeerQuery query) { + return object_ptr(nullptr); +} + +[[nodiscard]] bool FilterPeerByQuery( + not_null peer, + RequestPeerQuery query) { + 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()); + } + 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 session, + Fn)> callback, + RequestPeerQuery query) +: ChatsListBoxController(session) +, _session(session) +, _callback(std::move(callback)) +, _query(query) { +} + +Main::Session &ChoosePeerBoxController::session() const { + return *_session; +} + +void ChoosePeerBoxController::prepareRestrictions() { + auto above = object_ptr((QWidget*)nullptr); + const auto raw = above.data(); + auto rows = RestrictionsList(_query); + if (!rows.empty()) { + Settings::AddSubsectionTitle( + raw, + tr::lng_request_peer_requirements(), + { 0, st::membersMarginTop, 0, 0 }); + const auto skip = st::settingsSubsectionTitlePadding.left(); + auto separator = QString::fromUtf8("\n\xE2\x80\xA2 "); + raw->add( + object_ptr( + raw, + separator + rows.join(separator), + st::requestPeerRestriction), + { skip, 0, skip, 0 }); + } + const auto make = [&](tr::phrase<> text) { + auto button = raw->add( + object_ptr( + raw, + text(), + st::inviteViaLinkButton), + { 0, st::membersMarginTop, 0, 0 }); + const auto icon = Ui::CreateChild( + button, + st::inviteViaLinkIcon, + QPoint()); + button->heightValue( + ) | rpl::start_with_next([=](int height) { + icon->moveToLeft( + st::inviteViaLinkIconPosition.x(), + (height - st::inviteViaLinkIcon.height()) / 2); + }, icon->lifetime()); + + button->setClickedCallback([=] { + delegate()->peerListShowBox( + CreatePeerByQueryBox(&session(), _query)); + }); + + 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); + } else if (_query.type == RequestPeerQuery::Type::Broadcast) { + make(tr::lng_request_channel_create); + } + + 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() + : 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 onstack = _callback; + onstack(row->peer()); +} + +auto ChoosePeerBoxController::createRow(not_null history) +-> std::unique_ptr { + return FilterPeerByQuery(history->peer, _query) + ? 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 + +QPointer ShowChoosePeerBox( + not_null navigation, + Fn)> &&chosen, + RequestPeerQuery query) { + const auto weak = std::make_shared>(); + auto initBox = [=](not_null box) { + box->addButton(tr::lng_cancel(), [box] { + box->closeBox(); + }); + }; + auto callback = [=, done = std::move(chosen)](not_null peer) { + done(peer); + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + *weak = navigation->parentController()->show(Box( + std::make_unique( + &navigation->session(), + std::move(callback), + query), + std::move(initBox)), Ui::LayerOption::KeepOther); + return weak->data(); +} diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h new file mode 100644 index 0000000000..52bfc17a8f --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h @@ -0,0 +1,23 @@ +/* +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 +*/ +#pragma once + +struct RequestPeerQuery; + +namespace Ui { +class BoxContent; +} // namespace Ui + +namespace Window { +class SessionNavigation; +} // namespace Window + +QPointer ShowChoosePeerBox( + not_null navigation, + Fn)> &&chosen, + RequestPeerQuery query); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 9f5468aeef..eb061f5943 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3947,9 +3947,18 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { } }; }; - auto prepareRequestedPeer = []( + auto prepareRequestedPeer = [&]( const MTPDmessageActionRequestedPeer &action) { - return PreparedServiceText{ { } }; + const auto peerId = peerFromMTP(action.vpeer()); + const auto peer = history()->owner().peer(peerId); + auto result = PreparedServiceText{}; + result.text = TextWithEntities{ + u"You chose "_q + }.append( + Ui::Text::Link(peer->name(), 1) + ).append(u" for the bot."_q); + result.links.push_back(peer->createOpenLink()); + return result; }; setServiceText(action.match([&]( diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index cd48240288..fecb3598e9 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -11,6 +11,47 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_components.h" +namespace { + +[[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( + const MTPRequestPeerType &query) { + using Type = RequestPeerQuery::Type; + using Restriction = RequestPeerQuery::Restriction; + auto result = RequestPeerQuery(); + const auto restriction = [](const MTPBool *value) { + return !value + ? Restriction::Any + : mtpIsTrue(*value) + ? Restriction::Yes + : Restriction::No; + }; + const auto rights = [](const MTPChatAdminRights *value) { + return value ? ChatAdminRightsInfo(*value).flags : ChatAdminRights(); + }; + query.match([&](const MTPDrequestPeerTypeUser &data) { + result.type = Type::User; + result.userIsBot = restriction(data.vbot()); + result.userIsPremium = restriction(data.vpremium()); + }, [&](const MTPDrequestPeerTypeChat &data) { + result.type = Type::Group; + result.amCreator = data.is_creator(); + result.isBotParticipant = data.is_bot_participant(); + result.groupIsForum = restriction(data.vforum()); + result.hasUsername = restriction(data.vhas_username()); + result.myRights = rights(data.vuser_admin_rights()); + result.botRights = rights(data.vbot_admin_rights()); + }, [&](const MTPDrequestPeerTypeBroadcast &data) { + result.type = Type::Broadcast; + result.amCreator = data.is_creator(); + result.hasUsername = restriction(data.vhas_username()); + result.myRights = rights(data.vuser_admin_rights()); + result.botRights = rights(data.vbot_admin_rights()); + }); + return result; +} + +} // namespace + HistoryMessageMarkupButton::HistoryMessageMarkupButton( Type type, const QString &text, @@ -70,10 +111,13 @@ void HistoryMessageMarkupData::fillRows( }, [&](const MTPDkeyboardButtonRequestPhone &data) { row.emplace_back(Type::RequestPhone, qs(data.vtext())); }, [&](const MTPDkeyboardButtonRequestPeer &data) { + const auto query = RequestPeerQueryFromTL(data.vpeer_type()); row.emplace_back( Type::RequestPeer, qs(data.vtext()), - QByteArray(), // #TODO request_peer + QByteArray( + reinterpret_cast(&query), + sizeof(query)), QString(), int64(data.vbutton_id().v)); }, [&](const MTPDkeyboardButtonUrl &data) { diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 0cab4c0c4b..719711d53c 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/flags.h" +#include "data/data_chat_participant_status.h" namespace Data { class Session; @@ -28,6 +29,29 @@ enum class ReplyMarkupFlag : uint32 { inline constexpr bool is_flag_type(ReplyMarkupFlag) { return true; } using ReplyMarkupFlags = base::flags; +struct RequestPeerQuery { + enum class Type : uchar { + User, + Group, + Broadcast, + }; + enum class Restriction : uchar { + Any, + Yes, + No, + }; + Type type = Type::User; + Restriction userIsBot = Restriction::Any; + Restriction userIsPremium = Restriction::Any; + Restriction groupIsForum = Restriction::Any; + Restriction hasUsername = Restriction::Any; + bool amCreator = false; + bool isBotParticipant = false; + ChatAdminRights myRights = {}; + ChatAdminRights botRights = {}; +}; +static_assert(std::is_trivially_copy_assignable_v); + struct HistoryMessageMarkupButton { enum class Type { Default, diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 0a64e38518..1b322bd9d8 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -527,3 +527,11 @@ settingsPremiumUserAbout: FlatLabel(boxDividerLabel) { settingsPremiumLock: icon{{ "emoji/premium_lock", windowActiveTextFg, point(0px, 1px) }}; settingsPremiumLockSkip: 3px; + +requestPeerRestriction: FlatLabel(defaultFlatLabel) { + minWidth: 240px; + textFg: membersAboutLimitFg; + style: TextStyle(boxTextStyle) { + lineHeight: 22px; + } +}