From 5c5bacaf419ecf16c57d20517bd2231d17f8f8af Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 15 May 2026 14:34:56 +0400 Subject: [PATCH] Update API scheme to layer 226. Guard-bots. --- .../Resources/icons/chat/mini_shield_lock.svg | 6 + Telegram/Resources/langs/lang.strings | 46 +++- Telegram/SourceFiles/api/api_chat_invite.cpp | 106 +++++--- Telegram/SourceFiles/api/api_chat_invite.h | 13 + Telegram/SourceFiles/api/api_updates.cpp | 9 + Telegram/SourceFiles/apiwrap.cpp | 19 +- Telegram/SourceFiles/boxes/boxes.style | 2 + .../boxes/peers/edit_participant_box.cpp | 232 ++++++++++++++--- .../boxes/peers/edit_participant_box.h | 1 + .../boxes/peers/edit_participants_box.cpp | 101 +++++++- .../boxes/peers/edit_peer_info_box.cpp | 26 +- .../boxes/peers/edit_peer_invite_link.cpp | 23 +- .../boxes/peers/edit_peer_invite_links.cpp | 3 + .../boxes/peers/edit_peer_permissions_box.cpp | 16 +- .../boxes/peers/edit_peer_type_box.cpp | 236 +++++++++++++++--- .../boxes/peers/edit_peer_type_box.h | 3 + .../boxes/peers/replace_boost_box.cpp | 11 +- .../boxes/peers/replace_boost_box.h | 1 + Telegram/SourceFiles/data/data_channel.cpp | 11 + Telegram/SourceFiles/data/data_channel.h | 6 + .../data/data_chat_participant_status.h | 2 + Telegram/SourceFiles/data/data_session.cpp | 10 + Telegram/SourceFiles/data/data_session.h | 9 + Telegram/SourceFiles/data/data_user.h | 1 + .../inline_bots/bot_attach_web_view.cpp | 96 ++++++- .../inline_bots/bot_attach_web_view.h | 24 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 39 ++- .../SourceFiles/ui/boxes/edit_invite_link.cpp | 209 +++++++++++----- .../SourceFiles/ui/boxes/edit_invite_link.h | 6 + Telegram/SourceFiles/ui/effects/premium.style | 1 + 30 files changed, 1069 insertions(+), 199 deletions(-) create mode 100644 Telegram/Resources/icons/chat/mini_shield_lock.svg diff --git a/Telegram/Resources/icons/chat/mini_shield_lock.svg b/Telegram/Resources/icons/chat/mini_shield_lock.svg new file mode 100644 index 0000000000..cd3713bc3e --- /dev/null +++ b/Telegram/Resources/icons/chat/mini_shield_lock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 282aa4e5fc..58f0513c5f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1948,7 +1948,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_peer_send_only_members" = "Only members"; "lng_manage_peer_send_only_members_about" = "Turn this on if you expect users to join your group before being able to send messages."; "lng_manage_peer_send_approve_members" = "Approve new members"; -"lng_manage_peer_send_approve_members_about" = "Turn this on if you want users to join the group only after they are approved by an admin."; +"lng_manage_peer_send_approve_subscribers" = "Approve new subscribers"; +"lng_manage_peer_send_approve_members_about" = "Admins must approve anyone who wants to send a message in the group."; +"lng_manage_peer_send_approve_members_about_channel" = "Admins must approve anyone who wants to subscribe to this channel."; +"lng_manage_peer_send_approve_members_about_managed" = "Admins must approve anyone who wants to send a message in the group. Managed by {bot}."; +"lng_manage_peer_send_approve_members_about_managed_channel" = "Admins must approve anyone who wants to subscribe to this channel. Managed by {bot}."; +"lng_manage_peer_request_apply_title" = "Apply to existing invite links"; +"lng_manage_peer_request_apply_enable_group#one" = "Also enable **Approve New Members** for **{count} existing invite link** in this group?"; +"lng_manage_peer_request_apply_enable_group#other" = "Also enable **Approve New Members** for **{count} existing invite links** in this group?"; +"lng_manage_peer_request_apply_enable_channel#one" = "Also enable **Approve New Subscribers** for **{count} existing invite link** in this channel?"; +"lng_manage_peer_request_apply_enable_channel#other" = "Also enable **Approve New Subscribers** for **{count} existing invite links** in this channel?"; +"lng_manage_peer_request_apply_disable_group#one" = "Also disable **Approve New Members** for **{count} existing invite link** in this group?"; +"lng_manage_peer_request_apply_disable_group#other" = "Also disable **Approve New Members** for **{count} existing invite links** in this group?"; +"lng_manage_peer_request_apply_disable_channel#one" = "Also disable **Approve New Subscribers** for **{count} existing invite link** in this channel?"; +"lng_manage_peer_request_apply_disable_channel#other" = "Also disable **Approve New Subscribers** for **{count} existing invite links** in this channel?"; +"lng_manage_peer_request_apply_skip" = "Don't Apply"; +"lng_manage_peer_request_apply_confirm" = "Apply"; "lng_manage_peer_no_forwards_title" = "Content protection"; "lng_manage_peer_no_forwards" = "Restrict saving content"; "lng_manage_peer_no_forwards_about" = "Members won't be able to copy, save or forward content from this group."; @@ -2788,11 +2803,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_qr_title" = "Invite by QR Code"; "lng_group_invite_qr_about" = "Everyone on Telegram can scan this code to join your group."; "lng_group_invite_qr_copied" = "QR Code copied to clipboard."; -"lng_group_invite_request_approve" = "Request admin approval"; -"lng_group_invite_about_approve" = "Turn this on if you want users to join only after they are approved by an admin."; -"lng_group_invite_about_no_approve" = "Turn this on if you want users to join only after they are approved by an admin."; -"lng_group_invite_about_approve_channel" = "Turn this on if you want users to join only after they are approved by an admin."; -"lng_group_invite_about_no_approve_channel" = "Turn this on if you want users to join only after they are approved by an admin."; +"lng_group_invite_request_approve" = "Approve New Members"; +"lng_group_invite_request_approve_channel" = "Approve New Subscribers"; +"lng_group_invite_about_approve" = "Require admin approval for people joining through this link."; +"lng_group_invite_about_approve_managed" = "Require admin approval for people joining through this link. Managed by {bot}."; +"lng_group_invite_about_no_approve" = "Require admin approval for people joining through this link."; +"lng_group_invite_about_approve_channel" = "Require admin approval for people joining through this link."; +"lng_group_invite_about_approve_managed_channel" = "Require admin approval for people joining through this link. Managed by {bot}."; +"lng_group_invite_about_no_approve_channel" = "Require admin approval for people joining through this link."; +"lng_group_invite_about_approve_disabled" = "This option is unavailable because anyone can join and send messages through the public group link."; +"lng_group_invite_about_approve_disabled_channel" = "This option is unavailable because anyone can join the channel through the public channel link."; +"lng_group_invite_approval_required" = "approval required"; "lng_group_invite_subscription" = "Require Monthly Fee"; "lng_group_invite_subscription_ph" = "Stars Amount per month"; "lng_group_invite_subscription_price" = "~{cost} / month"; @@ -2814,6 +2835,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_request_about_channel" = "This channel accepts new subscribers only after they are approved by its admins."; "lng_group_request_sent" = "You will be added to the group once an admin approves your request."; "lng_group_request_sent_channel" = "You will be added to the channel once its admins approve your request."; +"lng_group_request_declined" = "Your request to join the group was declined."; +"lng_group_request_declined_channel" = "Your request to join the channel was declined."; "lng_group_requests_pending#one" = "{count} join request"; "lng_group_requests_pending#other" = "{count} join requests"; "lng_group_requests_pending_user" = "{user} requested to join"; @@ -6378,8 +6401,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_dismiss_admin" = "Dismiss Admin"; "lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights."; "lng_rights_about_add_admins_no" = "This admin will not be able to add new admins."; +"lng_rights_about_process_join_requests" = "Allow this bot to approve new members using its own interfaces, such as captchas."; "lng_rights_about_by" = "This admin promoted by {user} on {date}."; +"lng_guard_bot_approve_title" = "Approve New Members"; +"lng_guard_bot_approve_text" = "New members will now need approval from {bot} before they can send messages in this group."; +"lng_guard_bot_approve_text_channel" = "New members will now need approval from {bot} before they can join this channel."; +"lng_guard_bot_approve_confirm" = "Enable"; +"lng_guard_bot_replace_title" = "Replace Bot"; +"lng_guard_bot_replace_text" = "{from} is currently processing join requests. Switch to {to} instead?"; +"lng_guard_bot_replace_confirm" = "Use {bot}"; +"lng_guard_bot_replace_cancel" = "Keep {bot}"; + "lng_rights_about_admin_cant_edit" = "You can't edit the rights of this admin."; "lng_rights_about_restriction_cant_edit" = "You cannot change the restrictions for this user."; "lng_rights_restriction_for_all" = "This option is disabled for all members in Group Permissions."; @@ -6475,6 +6508,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_group_anonymous" = "Remain anonymous"; "lng_rights_group_manage_ranks" = "Edit member tags"; "lng_rights_add_admins" = "Add new admins"; +"lng_rights_group_process_join_requests" = "Process join requests"; "lng_rights_chat_send_text" = "Send messages"; "lng_rights_chat_send_media" = "Send media"; "lng_rights_chat_send_stickers" = "Send stickers & GIFs"; diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index 059cbfc850..f2bd7ff180 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" #include "info/profile/info_profile_badge.h" +#include "inline_bots/bot_attach_web_view.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_credits_graphics.h" @@ -119,40 +120,52 @@ void SubmitChatInvite( bool isGroup) { session->api().request(MTPmessages_ImportChatInvite( MTP_string(hash) - )).done([=](const MTPUpdates &result) { - session->api().applyUpdates(result); + )).done([=](const MTPmessages_ChatInviteJoinResult &result) { const auto strongController = weak.get(); - if (!strongController) { - return; + if (strongController) { + strongController->hideLayer(); } - strongController->hideLayer(); - const auto handleChats = [&](const MTPVector &chats) { - if (chats.v.isEmpty()) { - return; - } - const auto peerId = chats.v[0].match([](const MTPDchat &data) { - return peerFromChat(data.vid().v); - }, [](const MTPDchannel &data) { - return peerFromChannel(data.vid().v); - }, [](auto&&) { - return PeerId(0); - }); - if (const auto peer = session->data().peerLoaded(peerId)) { - // Shows in the primary window anyway. - strongController->showPeerHistory( - peer, - Window::SectionShow::Way::Forward); - } - }; - result.match([&](const MTPDupdates &data) { - handleChats(data.vchats()); - }, [&](const MTPDupdatesCombined &data) { - handleChats(data.vchats()); - }, [&](auto &&) { - LOG(("API Error: unexpected update cons %1 " - "(ApiWrap::importChatInvite)").arg(result.type())); - }); + ProcessChatInviteJoinResult( + session, + strongController ? strongController->uiShow() : nullptr, + result, + [=](const MTPUpdates &updates) { + session->api().applyUpdates(updates); + if (!strongController) { + return; + } + const auto handleChats = [&]( + const MTPVector &chats) { + if (chats.v.isEmpty()) { + return; + } + const auto peerId = chats.v[0].match( + [](const MTPDchat &data) { + return peerFromChat(data.vid().v); + }, + [](const MTPDchannel &data) { + return peerFromChannel(data.vid().v); + }, + [](auto&&) { + return PeerId(0); + }); + if (const auto peer = session->data().peerLoaded(peerId)) { + strongController->showPeerHistory( + peer, + Window::SectionShow::Way::Forward); + } + }; + updates.match([&](const MTPDupdates &data) { + handleChats(data.vchats()); + }, [&](const MTPDupdatesCombined &data) { + handleChats(data.vchats()); + }, [&](auto &&) { + LOG(("API Error: unexpected update cons %1 " + "(ApiWrap::importChatInvite)").arg(updates.type())); + }); + }, + weak); }).fail([=](const MTP::Error &error) { const auto &type = error.type(); @@ -623,6 +636,37 @@ void ConfirmInviteBox( } // namespace +void ProcessChatInviteJoinResult( + not_null session, + std::shared_ptr show, + const MTPmessages_ChatInviteJoinResult &result, + Fn done, + base::weak_ptr controller) { + result.match([&](const MTPDmessages_chatInviteJoinResultOk &data) { + done(data.vupdates()); + }, [&](const MTPDmessages_chatInviteJoinResultWebView &data) { + session->data().processUsers(data.vusers()); + const auto bot = session->data().userLoaded(UserId(data.vbot_id().v)); + if (!bot || !show) { + LOG(("API Error: guard bot %1 not loaded " + "(Api::ProcessChatInviteJoinResult)").arg(data.vbot_id().v)); + return; + } + session->attachWebView().open({ + .bot = not_null{ bot }, + .parentShow = std::move(show), + .context = { + .controller = controller, + .maySkipConfirmation = false, + }, + .source = InlineBots::WebViewSourceJoinChat{ + .url = qs(data.vurl()), + .queryId = uint64(data.vquery_id().v), + }, + }); + }); +} + void CheckChatInvite( not_null controller, const QString &hash, diff --git a/Telegram/SourceFiles/api/api_chat_invite.h b/Telegram/SourceFiles/api/api_chat_invite.h index dbd5a43321..ae3a580f20 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.h +++ b/Telegram/SourceFiles/api/api_chat_invite.h @@ -7,12 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/weak_ptr.h" + class ChannelData; namespace Main { class Session; } // namespace Main +namespace Ui { +class Show; +} // namespace Ui + namespace Window { class SessionController; } // namespace Window @@ -25,4 +31,11 @@ void CheckChatInvite( ChannelData *invitePeekChannel = nullptr, Fn loaded = nullptr); +void ProcessChatInviteJoinResult( + not_null session, + std::shared_ptr show, + const MTPmessages_ChatInviteJoinResult &result, + Fn done, + base::weak_ptr controller = {}); + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 95837ee07d..77d68a9b64 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2232,6 +2232,15 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().webViewResultSent({ .queryId = d.vquery_id().v }); } break; + case mtpc_updateJoinChatWebViewDecision: { + const auto &d = update.c_updateJoinChatWebViewDecision(); + session().data().joinChatWebViewDecision({ + .peerId = peerFromMTP(d.vpeer()), + .queryId = uint64(d.vquery_id().v), + .result = d.vresult(), + }); + } break; + case mtpc_updateBotMenuButton: { const auto &d = update.c_updateBotMenuButton(); if (const auto bot = session().data().userLoaded(d.vbot_id())) { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 56ba19b84b..317ce13dab 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_authorizations.h" #include "api/api_attached_stickers.h" #include "api/api_blocked_peers.h" +#include "api/api_chat_invite.h" #include "api/api_chat_links.h" #include "api/api_chat_participants.h" #include "api/api_cloud_password.h" @@ -1822,14 +1823,20 @@ void ApiWrap::joinChannel(not_null channel) { } else if (!_channelAmInRequests.contains(channel)) { const auto requestId = request(MTPchannels_JoinChannel( channel->inputChannel() - )).done([=](const MTPUpdates &result) { + )).done([=](const MTPmessages_ChatInviteJoinResult &result) { _channelAmInRequests.remove(channel); - applyUpdates(result); - session().data().addRecentJoinChat({ - .fromPeerId = channel->id, - .joinedPeerId = channel->id, - }); + Api::ProcessChatInviteJoinResult( + _session, + ShowForPeer(channel), + result, + [=](const MTPUpdates &updates) { + applyUpdates(updates); + session().data().addRecentJoinChat({ + .fromPeerId = channel->id, + .joinedPeerId = channel->id, + }); + }); }).fail([=](const MTP::Error &error) { const auto &type = error.type(); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 1f21bbf8fb..4273d8c9c2 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1251,6 +1251,8 @@ futureOwnerBox: Box(defaultBox) { buttonHeight: 0px; } futureOwnerBoxSelect: collectibleBox; +guardBotReplaceBox: Box(futureOwnerBox) { +} disableSharingIconPadding: margins(12px, 12px, 12px, 8px); disableSharingButtonLock: IconEmoji { diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 153bcf203b..a337f9b3c4 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_bot_to_chat_box.h" #include "boxes/peers/edit_peer_permissions_box.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/peers/replace_boost_box.h" #include "settings/settings_common.h" #include "data/data_peer_values.h" #include "data/data_channel.h" @@ -46,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_info.h" +#include "styles/style_premium.h" #include "styles/style_settings.h" namespace { @@ -54,6 +56,108 @@ constexpr auto kMaxRestrictDelayDays = 366; constexpr auto kSecondsInDay = 24 * 60 * 60; constexpr auto kSecondsInWeek = 7 * kSecondsInDay; +[[nodiscard]] bool CanProcessJoinRequests( + not_null peer, + not_null user) { + const auto info = user->botInfo.get(); + return peer->isChannel() + && user->isBot() + && info + && info->supportsGuard; +} + +[[nodiscard]] object_ptr GuardBotReplaceBox( + not_null current, + not_null replacement, + Fn)> confirmed) { + return Box([=](not_null box) { + box->setStyle(st::guardBotReplaceBox); + const auto content = box->verticalLayout(); + const auto weak = base::make_weak(box); + content->add( + CreateUserpicsTransfer( + content, + rpl::single(std::vector{ not_null(current) }), + replacement, + UserpicsTransferType::GuardBotReplace), + st::boxRowPadding + st::boostReplaceUserpicsPadding); + content->add( + object_ptr( + content, + tr::lng_guard_bot_replace_title(), + st::boostCenteredTitle), + st::boxRowPadding + st::starrefJoinTitlePadding, + style::al_top); + content->add( + object_ptr( + content, + rpl::single(tr::lng_guard_bot_replace_text( + tr::now, + lt_from, + tr::bold(current->name()), + lt_to, + tr::bold(replacement->name()), + tr::rich)), + st::boxLabel), + QMargins( + st::boxPadding.left(), + 0, + st::boxPadding.right(), + st::boxPadding.bottom())); + Ui::AddSkip(content); + const auto use = content->add( + object_ptr( + content, + rpl::single(tr::lng_guard_bot_replace_confirm( + tr::now, + lt_bot, + replacement->shortName())), + st::defaultLightButton), + st::boxRowPadding, + style::al_justify); + Ui::AddSkip(content); + const auto keep = content->add( + object_ptr( + content, + rpl::single(tr::lng_guard_bot_replace_cancel( + tr::now, + lt_bot, + current->shortName())), + st::defaultLightButton), + st::boxRowPadding, + style::al_justify); + use->setClickedCallback([=] { + confirmed(crl::guard(weak, [=] { weak->closeBox(); })); + }); + keep->setClickedCallback([=] { + box->closeBox(); + }); + }); +} + +[[nodiscard]] object_ptr GuardBotApproveBox( + bool isGroup, + not_null bot, + Fn)> confirmed) { + const auto text = isGroup + ? tr::lng_guard_bot_approve_text( + tr::now, + lt_bot, + tr::bold(bot->name()), + tr::rich) + : tr::lng_guard_bot_approve_text_channel( + tr::now, + lt_bot, + tr::bold(bot->name()), + tr::rich); + return Ui::MakeConfirmBox({ + .text = text, + .confirmed = confirmed, + .confirmText = tr::lng_guard_bot_approve_confirm(), + .title = tr::lng_guard_bot_approve_title(), + }); +} + class Cover final : public Ui::FixedHeightWidget { public: Cover(QWidget *parent, not_null user, bool hasAdminRights); @@ -324,7 +428,10 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::ManageTopics | Flag::PinMessages | Flag::ManageCall - | Flag::ManageRanks) } + | Flag::ManageRanks + | (CanProcessJoinRequests(peer(), user()) + ? Flag::ProcessJoinRequests + : Flag())) } : ChatAdminRightsInfo{ (Flag::ChangeInfo | Flag::PostMessages | Flag::EditMessages @@ -410,6 +517,15 @@ void EditAdminBox::prepare() { const auto chat = peer()->asChat(); const auto channel = peer()->asChannel(); + const auto supportsProcessJoinRequests = canSave() + && CanProcessJoinRequests(peer(), user()); + const auto canProcessJoinRequests = supportsProcessJoinRequests + && peer()->isMegagroup(); + const auto guardBotRight = (canProcessJoinRequests + && channel + && (channel->guardBotId() == peerToUser(user()->id))) + ? Flag::ProcessJoinRequests + : Flag(); const auto prepareRights = _addingBot ? ChatAdminRightsInfo(_oldRights.flags | _addingBot->existing) : _oldRights.flags @@ -422,9 +538,16 @@ void EditAdminBox::prepare() { && !_oldRights.flags && channel && !channel->amCreator(); + const auto myEditableRights = channel + ? (channel->adminRights() + | (canProcessJoinRequests + ? Flag::ProcessJoinRequests + : Flag())) + : Flags(); const auto prepareFlags = disabledByDefaults + | guardBotRight | (prepareRights.flags - & (filterByMyRights ? channel->adminRights() : ~Flag(0))); + & (filterByMyRights ? myEditableRights : ~Flag(0))); const auto disabledMessages = [&] { auto result = base::flat_map(); @@ -443,7 +566,7 @@ void EditAdminBox::prepare() { } else if (const auto channel = peer()->asChannel()) { if (!channel->amCreator()) { result.emplace( - ~channel->adminRights(), + ~myEditableRights, tr::lng_rights_permission_cant_edit(tr::now)); } } @@ -459,6 +582,7 @@ void EditAdminBox::prepare() { .isGroup = isGroup, .isForum = peer()->isForum(), .anyoneCanAddMembers = anyoneCanAddMembers, + .canProcessJoinRequests = canProcessJoinRequests, }; Ui::AddSubsectionTitle(inner, tr::lng_rights_edit_admin_header()); auto [checkboxes, getChecked, changes, highlightWidget] = CreateEditAdminRights( @@ -492,26 +616,33 @@ void EditAdminBox::prepare() { Ui::AddDivider(emptyAboutAddAdminsInner->entity()); } Ui::AddSkip(aboutAddAdminsInner->entity()); - Ui::AddDividerText( - aboutAddAdminsInner->entity(), - rpl::duplicate( - selectedFlags - ) | rpl::map( - (_1 & Flag::AddAdmins) != 0 - ) | rpl::distinct_until_changed( - ) | rpl::map([=](bool canAddAdmins) -> rpl::producer { - const auto empty = (amCreator() && user()->isSelf()); - aboutAddAdminsInner->toggle(!empty, anim::type::instant); - emptyAboutAddAdminsInner->toggle(empty, anim::type::instant); - if (empty) { - return rpl::single(QString()); - } else if (!canSave()) { - return tr::lng_rights_about_admin_cant_edit(); - } else if (canAddAdmins) { - return tr::lng_rights_about_add_admins_yes(); - } - return tr::lng_rights_about_add_admins_no(); - }) | rpl::flatten_latest()); + if (canProcessJoinRequests) { + aboutAddAdminsInner->toggle(true, anim::type::instant); + Ui::AddDividerText( + aboutAddAdminsInner->entity(), + tr::lng_rights_about_process_join_requests()); + } else { + Ui::AddDividerText( + aboutAddAdminsInner->entity(), + rpl::duplicate( + selectedFlags + ) | rpl::map( + (_1 & Flag::AddAdmins) != 0 + ) | rpl::distinct_until_changed( + ) | rpl::map([=](bool canAddAdmins) -> rpl::producer { + const auto empty = (amCreator() && user()->isSelf()); + aboutAddAdminsInner->toggle(!empty, anim::type::instant); + emptyAboutAddAdminsInner->toggle(empty, anim::type::instant); + if (empty) { + return rpl::single(QString()); + } else if (!canSave()) { + return tr::lng_rights_about_admin_cant_edit(); + } else if (canAddAdmins) { + return tr::lng_rights_about_add_admins_yes(); + } + return tr::lng_rights_about_add_admins_no(); + }) | rpl::flatten_latest()); + } } if (canTransferOwnership()) { @@ -590,13 +721,15 @@ void EditAdminBox::prepare() { const auto newFlags = (value() | ChatAdminRight::Other) & ((!channel || channel->amCreator()) ? ~Flags(0) - : channel->adminRights()); - _saveCallback( - _oldRights, - ChatAdminRightsInfo(newFlags), - _tagControl - ? std::optional(_tagControl->currentRank()) - : std::nullopt); + : myEditableRights); + confirmGuardBotSave(ChatAdminRightsInfo(newFlags), [=] { + _saveCallback( + _oldRights, + ChatAdminRightsInfo(newFlags), + _tagControl + ? std::optional(_tagControl->currentRank()) + : std::nullopt); + }); }; _save = [=] { const auto show = uiShow(); @@ -631,9 +764,44 @@ void EditAdminBox::prepare() { } void EditAdminBox::finishAddAdmin() { + const auto confirmBox = _confirmBox; + _confirmBox = nullptr; + if (confirmBox) { + confirmBox->closeBox(); + } _finishSave(); - if (_confirmBox) { - _confirmBox->closeBox(); +} + +void EditAdminBox::confirmGuardBotSave( + ChatAdminRightsInfo rights, + Fn done) { + using Flag = ChatAdminRight; + + const auto channel = peer()->asChannel(); + if (!channel + || !(rights.flags & Flag::ProcessJoinRequests) + || !CanProcessJoinRequests(peer(), user())) { + done(); + return; + } + + const auto confirmed = crl::guard(this, [=](Fn close) { + close(); + done(); + }); + const auto current = channel->guardBot(); + if (current && current != user().get()) { + _confirmBox = getDelegate()->show(GuardBotReplaceBox( + current, + user(), + confirmed)); + } else if (!channel->requestToJoin()) { + _confirmBox = getDelegate()->show(GuardBotApproveBox( + channel->isMegagroup(), + user(), + confirmed)); + } else { + done(); } } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h index b3c1df3cfb..d9931cf02b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h @@ -101,6 +101,7 @@ private: return _saveCallback != nullptr; } void finishAddAdmin(); + void confirmGuardBotSave(ChatAdminRightsInfo rights, Fn done); void refreshButtons(); [[nodiscard]] bool canTransferOwnership() const; not_null*> setupTransferButton( diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 8f59876791..8b0fe0e0ff 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -200,6 +200,72 @@ void SaveChannelAdmin( }).send(); } +[[nodiscard]] ChatAdminRightsInfo StripLocalAdminRights( + ChatAdminRightsInfo rights) { + rights.flags &= ~ChatAdminRight::ProcessJoinRequests; + return rights; +} + +void SaveGuardBot( + std::shared_ptr show, + not_null channel, + not_null user, + bool processJoinRequests, + Fn onDone, + Fn onFail) { + const auto info = user->botInfo.get(); + const auto userId = peerToUser(user->id); + const auto guardBotId = channel->guardBotId(); + if (!info || !info->supportsGuard) { + if (onDone) { + onDone(); + } + return; + } else if (processJoinRequests + && channel->requestToJoin() + && guardBotId == userId) { + if (onDone) { + onDone(); + } + return; + } else if (!processJoinRequests && guardBotId != userId) { + if (onDone) { + onDone(); + } + return; + } + + using Flag = MTPchannels_ToggleJoinRequest::Flag; + const auto keepRequests = processJoinRequests || channel->requestToJoin(); + channel->session().api().request(MTPchannels_ToggleJoinRequest( + MTP_flags(Flag::f_guard_bot), + channel->inputChannel(), + MTP_bool(keepRequests), + processJoinRequests ? user->inputUser() : MTP_inputUserEmpty() + )).done([=](const MTPUpdates &result) { + channel->session().api().applyUpdates(result); + if (processJoinRequests) { + channel->addFlags(ChannelDataFlag::RequestToJoin); + channel->setGuardBotId(userId); + } else { + if (!keepRequests) { + channel->removeFlags(ChannelDataFlag::RequestToJoin); + } + channel->setGuardBotId(UserId()); + } + if (onDone) { + onDone(); + } + }).fail([=](const MTP::Error &error) { + if (show) { + show->showToast(error.type()); + } + if (onFail) { + onFail(); + } + }).send(); +} + void SaveChatParticipantKick( std::shared_ptr show, not_null chat, @@ -292,16 +358,38 @@ Fn &rank) { - const auto done = [=] { if (onDone) onDone(newRights, rank); }; + const auto processJoinRequests = ((newRights.flags + & ChatAdminRight::ProcessJoinRequests) != 0); + const auto manageGuardBot = peer->isMegagroup() + || processJoinRequests; + const auto strippedOldRights = StripLocalAdminRights(oldRights); + const auto strippedNewRights = StripLocalAdminRights(newRights); + const auto done = [=] { + if (onDone) { + onDone(strippedNewRights, rank); + } + }; const auto saveForChannel = [=](not_null channel) { SaveChannelAdmin( show, channel, user, - oldRights, - newRights, + strippedOldRights, + strippedNewRights, rank, - done, + [=] { + if (manageGuardBot) { + SaveGuardBot( + show, + channel, + user, + processJoinRequests, + done, + onFail); + } else { + done(); + } + }, onFail); }; if (const auto chat = peer->asChatNotMigrated()) { @@ -311,9 +399,10 @@ FndefaultAdminRights(user).flags) { + if (strippedNewRights.flags + == chat->defaultAdminRights(user).flags) { saveChatAdmin(true); - } else if (!newRights.flags) { + } else if (!strippedNewRights.flags) { saveChatAdmin(false); } else { peer->session().api().migrateChat(chat, saveForChannel); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index f14dd6a664..0427daf8a1 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -407,6 +407,7 @@ private: std::optional noForwards; std::optional joinToWrite; std::optional requestToJoin; + std::optional requestToJoinApplyToInvites; std::optional discussionLink; std::optional starsPerDirectMessage; }; @@ -989,6 +990,10 @@ void Controller::fillPrivacyTypeButton() { // Create Privacy Button. const auto hasLocation = _peer->isChannel() && _peer->asChannel()->hasLocation(); + const auto guardBot = _peer->isChannel() + ? _peer->asChannel()->guardBot() + : nullptr; + const auto guardBotUsername = guardBot ? guardBot->username() : QString(); _typeDataSavedValue = EditPeerTypeData{ .privacy = ((_peer->isChannel() && _peer->asChannel()->hasUsername()) @@ -1003,8 +1008,12 @@ void Controller::fillPrivacyTypeButton() { .noForwards = !_peer->allowsForwarding(), .joinToWrite = (_peer->isMegagroup() && _peer->asChannel()->joinToWrite()), - .requestToJoin = (_peer->isMegagroup() + .requestToJoin = (_peer->isChannel() && _peer->asChannel()->requestToJoin()), + .guardBotUsername = guardBotUsername, + .guardBotLink = guardBotUsername.isEmpty() + ? QString() + : _peer->session().createInternalLink(guardBotUsername), }; const auto isGroup = (_peer->isChat() || _peer->isMegagroup()); AddButtonWithText( @@ -2233,6 +2242,8 @@ bool Controller::validateRequestToJoin(Saving &to) const { return true; } to.requestToJoin = _typeDataSavedValue->requestToJoin; + to.requestToJoinApplyToInvites + = _typeDataSavedValue->requestToJoinApplyToInvites; return true; } @@ -2792,15 +2803,22 @@ void Controller::saveJoinToWrite() { } void Controller::saveRequestToJoin() { - const auto requestToJoin = _peer->isMegagroup() + const auto requestToJoin = _peer->isChannel() && _peer->asChannel()->requestToJoin(); if (!_savingData.requestToJoin || *_savingData.requestToJoin == requestToJoin) { return continueSave(); } + using Flag = MTPchannels_ToggleJoinRequest::Flag; + const auto channel = _peer->asChannel(); + const auto flags = _savingData.requestToJoinApplyToInvites.value_or(false) + ? Flag::f_apply_to_invites + : Flag(); _api.request(MTPchannels_ToggleJoinRequest( - _peer->asChannel()->inputChannel(), - MTP_bool(*_savingData.requestToJoin) + MTP_flags(flags), + channel->inputChannel(), + MTP_bool(*_savingData.requestToJoin), + MTP_inputUserEmpty() )).done([=](const MTPUpdates &result) { _peer->session().api().applyUpdates(result); continueSave(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index ac5b632317..d4f61cd0e9 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1619,6 +1619,16 @@ object_ptr EditLinkBox( }; const auto isGroup = !peer->isBroadcast(); const auto isPublic = peer->isChannel() && peer->asChannel()->isPublic(); + const auto globalRequestApproval = peer->isChannel() + && peer->asChannel()->requestToJoin(); + const auto guardBot = [&]() -> UserData* { + const auto channel = peer->asChannel(); + return channel ? channel->guardBot() : nullptr; + }(); + const auto guardBotUsername = guardBot ? guardBot->username() : QString(); + const auto guardBotLink = guardBotUsername.isEmpty() + ? QString() + : peer->session().createInternalLink(guardBotUsername); auto object = Box([=](not_null box) { const auto fill = isGroup ? Fn(nullptr) @@ -1626,7 +1636,15 @@ object_ptr EditLinkBox( return Ui::FillCreateInviteLinkSubscriptionToggle(box, peer); }; if (creating) { - Ui::CreateInviteLinkBox(box, fill, isGroup, isPublic, done); + Ui::CreateInviteLinkBox( + box, + fill, + isGroup, + isPublic, + globalRequestApproval, + guardBotUsername, + guardBotLink, + done); } else { Ui::EditInviteLinkBox( box, @@ -1640,6 +1658,9 @@ object_ptr EditLinkBox( .requestApproval = data.requestApproval, .isGroup = isGroup, .isPublic = isPublic, + .globalRequestApproval = globalRequestApproval, + .guardBotUsername = guardBotUsername, + .guardBotLink = guardBotLink, }, done); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index eacaa448ca..9809b8fae8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -209,6 +209,9 @@ private: add(QLocale().toString(time, QLocale::LongFormat)); } } + if (link.requestApproval) { + add(tr::lng_group_invite_approval_required(tr::now)); + } return result; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index b74ebd6841..fdb54d3a39 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -144,6 +144,12 @@ constexpr auto kDefaultChargeStars = 10; { Flag::Anonymous, tr::lng_rights_group_anonymous(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, }; + if (options.canProcessJoinRequests) { + second.push_back({ + Flag::ProcessJoinRequests, + tr::lng_rights_group_process_join_requests(tr::now), + }); + } if (!options.isForum) { first.erase( ranges::remove( @@ -181,6 +187,12 @@ constexpr auto kDefaultChargeStars = 10; { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, { Flag::BanUsers, tr::lng_rights_group_ban(tr::now) }, }; + if (options.canProcessJoinRequests) { + second.push_back({ + Flag::ProcessJoinRequests, + tr::lng_rights_group_process_join_requests(tr::now), + }); + } return { { std::nullopt, std::move(first) }, { tr::lng_rights_channel_manage(), std::move(messages) }, @@ -1467,7 +1479,9 @@ ChatAdminRights AdminRightsForOwnershipTransfer( Data::AdminRightsSetOptions options) { auto result = ChatAdminRights(); for (const auto &entry : AdminRightLabels(options)) { - if (!(entry.flags & ChatAdminRight::Anonymous)) { + if (!(entry.flags + & (ChatAdminRight::Anonymous + | ChatAdminRight::ProcessJoinRequests))) { result |= entry.flags; } } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index 0373cc6a16..d883cfa9cd 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/widgets/box_content_divider.h" +#include "ui/text/text_utilities.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -49,6 +50,68 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +[[nodiscard]] TextWithEntities RequestToJoinAbout( + bool joinToWrite, + bool requestToJoin, + bool isGroup, + const QString &guardBotUsername, + const QString &guardBotLink) { + if (!joinToWrite) { + return TextWithEntities{ + tr::lng_manage_peer_send_only_members_about(tr::now) + }; + } else if (requestToJoin && !guardBotUsername.isEmpty()) { + return (isGroup + ? tr::lng_manage_peer_send_approve_members_about_managed + : tr::lng_manage_peer_send_approve_members_about_managed_channel)( + tr::now, + lt_bot, + tr::link('@' + guardBotUsername, guardBotLink), + tr::marked); + } + return TextWithEntities{ + (isGroup + ? tr::lng_manage_peer_send_approve_members_about + : tr::lng_manage_peer_send_approve_members_about_channel)( + tr::now) + }; +} + +[[nodiscard]] TextWithEntities ApplyRequestToJoinToInvitesText( + bool enabled, + bool isGroup, + int count) { + if (enabled) { + return (isGroup + ? tr::lng_manage_peer_request_apply_enable_group + : tr::lng_manage_peer_request_apply_enable_channel)( + tr::now, + lt_count, + count, + tr::rich); + } + return (isGroup + ? tr::lng_manage_peer_request_apply_disable_group + : tr::lng_manage_peer_request_apply_disable_channel)( + tr::now, + lt_count, + count, + tr::rich); +} + +[[nodiscard]] int InviteLinksCount( + const MTPmessages_ChatAdminsWithInvites &result) { + auto count = 0; + result.match([&](const MTPDmessages_chatAdminsWithInvites &data) { + for (const auto &admin : data.vadmins().v) { + admin.match([&](const MTPDchatAdminWithInvites &data) { + count += data.vinvites_count().v; + }); + } + }); + return count; +} + class Controller : public base::has_weak_ptr { public: Controller( @@ -87,13 +150,17 @@ public: return _controls.joinToWrite && _controls.joinToWrite->toggled(); } [[nodiscard]] bool requestToJoin() const { - return _controls.requestToJoin && _controls.requestToJoin->toggled(); + return _controls.requestToJoin + && (_isGroup || getPrivacy() != Privacy::HasUsername) + && _controls.requestToJoin->toggled(); } [[nodiscard]] rpl::producer scrollToRequests() const { return _scrollToRequests.events(); } + void submit(Fn done); + void showError(rpl::producer text) { _controls.usernameInput->showError(); showUsernameError(std::move(text)); @@ -122,6 +189,7 @@ private: object_ptr createInviteLinkBlock(); void privacyChanged(Privacy value); + [[nodiscard]] bool requestsSectionShown(Privacy value) const; void checkUsernameAvailability(); void askUsernameRevoke(); @@ -130,6 +198,12 @@ private: void showUsernameGood(); void showUsernamePending(); void showUsernameEmpty(); + [[nodiscard]] EditPeerTypeData collectData() const; + void requestInviteLinksCount(Fn done); + void confirmApplyToInviteLinks( + EditPeerTypeData data, + int count, + Fn done); void fillPrivaciesButtons( not_null parent, @@ -152,6 +226,7 @@ private: bool _useLocationPhrases = false; bool _isGroup = false; bool _goodUsername = false; + bool _originalRequestToJoin = false; base::unique_qptr _wrap; base::Timer _checkUsernameTimer; @@ -185,11 +260,106 @@ Controller::Controller( , _goodUsername(_dataSavedValue ? !_dataSavedValue->username.isEmpty() : (_peer->isChannel() && !_peer->asChannel()->editableUsername().isEmpty())) +, _originalRequestToJoin(_dataSavedValue + ? _dataSavedValue->requestToJoin + : false) , _wrap(container) , _checkUsernameTimer([=] { checkUsernameAvailability(); }) { _peer->updateFull(); } +EditPeerTypeData Controller::collectData() const { + const auto privacy = getPrivacy(); + return EditPeerTypeData{ + .privacy = privacy, + .username = (privacy == Privacy::HasUsername + ? getUsernameInput() + : QString()), + .usernamesOrder = (privacy == Privacy::HasUsername + ? usernamesOrder() + : std::vector()), + .hasDiscussionLink = _dataSavedValue + ? _dataSavedValue->hasDiscussionLink + : false, + .noForwards = noForwards(), + .joinToWrite = joinToWrite(), + .requestToJoin = requestToJoin(), + .requestToJoinApplyToInvites = _dataSavedValue + ? _dataSavedValue->requestToJoinApplyToInvites + : std::optional(), + .guardBotUsername = _dataSavedValue + ? _dataSavedValue->guardBotUsername + : QString(), + .guardBotLink = _dataSavedValue + ? _dataSavedValue->guardBotLink + : QString(), + }; +} + +void Controller::submit(Fn done) { + const auto privacy = getPrivacy(); + if ((privacy == Privacy::HasUsername) && !goodUsername()) { + if (!getUsernameInput().isEmpty() || usernamesOrder().empty()) { + setFocusUsername(); + return; + } + } + + const auto data = collectData(); + if (!_dataSavedValue + || !_peer->isChannel() + || _originalRequestToJoin == data.requestToJoin) { + done(data); + return; + } + + requestInviteLinksCount([=](int count) { + if (count > 0) { + confirmApplyToInviteLinks(data, count, done); + } else { + done(data); + } + }); +} + +void Controller::requestInviteLinksCount(Fn done) { + _api.request(MTPmessages_GetAdminsWithInvites( + _peer->input() + )).done([=](const MTPmessages_ChatAdminsWithInvites &result) { + done(InviteLinksCount(result)); + }).fail([=] { + done(0); + }).send(); +} + +void Controller::confirmApplyToInviteLinks( + EditPeerTypeData data, + int count, + Fn done) { + const auto finish = [=](bool apply) { + auto result = data; + result.requestToJoinApplyToInvites = apply; + done(result); + }; + _show->showBox(Ui::MakeConfirmBox({ + .text = ApplyRequestToJoinToInvitesText( + data.requestToJoin, + _isGroup, + count), + .confirmed = [=](Fn close) { + close(); + finish(true); + }, + .cancelled = [=](Fn close) { + close(); + finish(false); + }, + .confirmText = tr::lng_manage_peer_request_apply_confirm(), + .cancelText = tr::lng_manage_peer_request_apply_skip(), + .title = tr::lng_manage_peer_request_apply_title(), + })); +} + void Controller::createContent() { _controls = Controls(); @@ -212,7 +382,7 @@ void Controller::createContent() { using namespace Settings; if (!_linkOnly) { - if (_peer->isMegagroup()) { + if (_peer->isChannel()) { _controls.whoSendWrap = _wrap->add( object_ptr>( _wrap.get(), @@ -220,7 +390,7 @@ void Controller::createContent() { const auto wrap = _controls.whoSendWrap->entity(); Ui::AddSkip(wrap); - if (_dataSavedValue->hasDiscussionLink) { + if (_peer->isMegagroup() && _dataSavedValue->hasDiscussionLink) { Ui::AddSubsectionTitle(wrap, tr::lng_manage_peer_send_title()); _controls.joinToWrite = wrap->add(EditPeerInfoBox::CreateButton( @@ -239,7 +409,7 @@ void Controller::createContent() { }, wrap->lifetime()); } else { _controls.whoSendWrap->toggle( - (_controls.privacy->current() == Privacy::HasUsername), + true, anim::type::instant); } auto joinToWrite = _controls.joinToWrite @@ -251,7 +421,9 @@ void Controller::createContent() { wrap, EditPeerInfoBox::CreateButton( wrap, - tr::lng_manage_peer_send_approve_members(), + (_isGroup + ? tr::lng_manage_peer_send_approve_members + : tr::lng_manage_peer_send_approve_subscribers)(), rpl::single(QString()), [=] {}, st::peerPermissionsButton, @@ -264,14 +436,22 @@ void Controller::createContent() { ) | rpl::on_next([=](bool toggled) { _dataSavedValue->requestToJoin = toggled; }, wrap->lifetime()); + auto requestToJoin = _controls.requestToJoin->toggledValue(); Ui::AddSkip(wrap); Ui::AddDividerText( wrap, - rpl::conditional( + rpl::combine( std::move(joinToWrite), - tr::lng_manage_peer_send_approve_members_about(), - tr::lng_manage_peer_send_only_members_about())); + std::move(requestToJoin) + ) | rpl::map([=](bool join, bool approval) { + return RequestToJoinAbout( + join, + approval, + _isGroup, + _dataSavedValue->guardBotUsername, + _dataSavedValue->guardBotLink); + })); } Ui::AddSkip(_wrap.get()); Ui::AddSubsectionTitle( @@ -313,6 +493,11 @@ void Controller::createContent() { _controls.usernameWrap->toggle( (forShowing == Privacy::HasUsername), anim::type::instant); + if (_controls.whoSendWrap) { + _controls.whoSendWrap->toggle( + requestsSectionShown(forShowing), + anim::type::instant); + } } } @@ -498,8 +683,7 @@ void Controller::privacyChanged(Privacy value) { return; } _controls.whoSendWrap->toggle( - (value == Privacy::HasUsername - || (_dataSavedValue && _dataSavedValue->hasDiscussionLink)), + requestsSectionShown(value), anim::type::instant); }; const auto refreshVisibilities = [&] { @@ -537,6 +721,10 @@ void Controller::privacyChanged(Privacy value) { setFocusUsername(); } +bool Controller::requestsSectionShown(Privacy value) const { + return _isGroup || value != Privacy::HasUsername; +} + void Controller::checkUsernameAvailability() { if (!_controls.usernameInput) { return; @@ -759,29 +947,11 @@ void EditPeerTypeBox::prepare() { if (_savedCallback.has_value()) { addButton(tr::lng_settings_save(), [=] { - const auto v = controller->getPrivacy(); - if ((v == Privacy::HasUsername) && !controller->goodUsername()) { - if (!controller->getUsernameInput().isEmpty() - || controller->usernamesOrder().empty()) { - controller->setFocusUsername(); - return; - } - } - - auto local = std::move(*_savedCallback); - local(EditPeerTypeData{ - .privacy = v, - .username = (v == Privacy::HasUsername - ? controller->getUsernameInput() - : QString()), - .usernamesOrder = (v == Privacy::HasUsername - ? controller->usernamesOrder() - : std::vector()), - .noForwards = controller->noForwards(), - .joinToWrite = controller->joinToWrite(), - .requestToJoin = controller->requestToJoin(), - }); // We don't need username with private type. - closeBox(); + controller->submit(crl::guard(this, [=](EditPeerTypeData data) { + auto local = std::move(*_savedCallback); + local(std::move(data)); + closeBox(); + })); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); } else { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h index 7b9aba6f9a..fefedb1984 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h @@ -41,6 +41,9 @@ struct EditPeerTypeData { bool noForwards = false; bool joinToWrite = false; bool requestToJoin = false; + std::optional requestToJoinApplyToInvites; + QString guardBotUsername; + QString guardBotLink; }; class EditPeerTypeBox : public Ui::BoxContent { diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 1b5c3fde27..4298a8c2b3 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -695,12 +695,16 @@ object_ptr CreateUserpicsTransfer( const auto last = state->buttons.back().get(); if (type != Type::AuctionRecipient) { const auto boosting = (type == Type::BoostReplace); - const auto back = boosting ? last : right; + const auto guard = (type == Type::GuardBotReplace); + const auto gradient = boosting || guard; + const auto back = gradient ? last : right; const auto add = st::boostReplaceIconAdd; - const auto &icon = boosting + const auto &icon = guard + ? st::guardBotReplaceIcon + : boosting ? st::boostReplaceIcon : st::starrefJoinIcon; - const auto skip = boosting ? st::boostReplaceIconSkip : 0; + const auto skip = gradient ? st::boostReplaceIconSkip : 0; const auto w = icon.width() + 2 * skip; const auto h = icon.height() + 2 * skip; const auto x = back->x() + back->width() - w + add.x(); @@ -1110,4 +1114,3 @@ object_ptr CreateGiftTransfer( }, overlay->lifetime()); return result; } - diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h index a505799c89..3c4d7c9567 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -66,6 +66,7 @@ enum class UserpicsTransferType { StarRefJoin, AuctionRecipient, ChannelFutureOwner, + GuardBotReplace, }; [[nodiscard]] object_ptr CreateUserpicsTransfer( not_null parent, diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 4aee47b812..8687dadc9f 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -1208,6 +1208,16 @@ TimeId ChannelData::subscriptionUntilDate() const { return _subscriptionUntilDate; } +UserData *ChannelData::guardBot() const { + return _guardBotId + ? owner().userLoaded(_guardBotId) + : nullptr; +} + +void ChannelData::setGuardBotId(UserId userId) { + _guardBotId = userId; +} + void ChannelData::updateSubscriptionUntilDate(TimeId subscriptionUntilDate) { _subscriptionUntilDate = subscriptionUntilDate; } @@ -1277,6 +1287,7 @@ void ApplyChannelUpdate( channel->setMessagesTTL(update.vttl_period().value_or_empty()); channel->setStarsPerMessage( update.vsend_paid_messages_stars().value_or_empty()); + channel->setGuardBotId(UserId(update.vguard_bot_id().value_or_empty())); using Flag = ChannelDataFlag; const auto mask = Flag::CanSetUsername | Flag::CanViewParticipants diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 0325eacb98..d10b6f055c 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -351,6 +351,11 @@ public: [[nodiscard]] bool requestToJoin() const { return flags() & Flag::RequestToJoin; } + [[nodiscard]] UserData *guardBot() const; + [[nodiscard]] UserId guardBotId() const { + return _guardBotId; + } + void setGuardBotId(UserId userId); [[nodiscard]] bool antiSpamMode() const { return flags() & Flag::AntiSpam; } @@ -615,6 +620,7 @@ private: int _pendingRequestsCount = 0; int _levelHint = 0; int _starsPerMessage = 0; + UserId _guardBotId = 0; Data::AllowedReactions _allowedReactions; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index 013f27d4f7..294ad232c0 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -39,6 +39,7 @@ enum class ChatAdminRight { DeleteStories = (1 << 16), ManageDirect = (1 << 17), ManageRanks = (1 << 18), + ProcessJoinRequests = (1 << 19), }; inline constexpr bool is_flag_type(ChatAdminRight) { return true; } using ChatAdminRights = base::flags; @@ -105,6 +106,7 @@ struct AdminRightsSetOptions { bool isGroup : 1 = false; bool isForum : 1 = false; bool anyoneCanAddMembers : 1 = false; + bool canProcessJoinRequests : 1 = false; }; struct RestrictionsSetOptions { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 3c64b719bc..be4a09b97f 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -603,6 +603,7 @@ not_null Session::processUser(const MTPUser &data) { info->userCreatesTopics = data.is_bot_forum_can_manage_topics(); info->canManageBots = data.is_bot_can_manage_bots(); info->supportsGuestChat = data.is_bot_guestchat(); + info->supportsGuard = data.is_bot_guard(); } } @@ -5580,6 +5581,15 @@ auto Session::webViewResultSent() const -> rpl::producer { return _webViewResultSent.events(); } +void Session::joinChatWebViewDecision(JoinChatWebViewDecision &&decision) { + return _joinChatWebViewDecision.fire(std::move(decision)); +} + +auto Session::joinChatWebViewDecision() const +-> rpl::producer { + return _joinChatWebViewDecision.events(); +} + rpl::producer> Session::peerDecorationsUpdated() const { return _peerDecorationsUpdated.events(); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 243427960f..a0103f29d0 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -959,6 +959,14 @@ public: void webViewResultSent(WebViewResultSent &&sent); [[nodiscard]] rpl::producer webViewResultSent() const; + struct JoinChatWebViewDecision { + PeerId peerId; + uint64 queryId = 0; + MTPJoinChatBotResult result; + }; + void joinChatWebViewDecision(JoinChatWebViewDecision &&decision); + [[nodiscard]] rpl::producer joinChatWebViewDecision() const; + void saveViewAsMessages(not_null forum, bool viewAsMessages); [[nodiscard]] auto peerDecorationsUpdated() const @@ -1359,6 +1367,7 @@ private: base::flat_map, MTP::DcId> _peerStatsDcIds; rpl::event_stream _webViewResultSent; + rpl::event_stream _joinChatWebViewDecision; rpl::event_stream> _peerDecorationsUpdated; base::flat_map< diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 8eb1076614..3b6f00df47 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -109,6 +109,7 @@ struct BotInfo { bool setBotPhotoHidden : 1 = false; bool canManageBots : 1 = false; bool supportsGuestChat : 1 = false; + bool supportsGuard : 1 = false; private: std::unique_ptr _forum; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 110da84d97..197b3e04ac 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -208,6 +208,33 @@ constexpr auto kPopularAppBotsLimit = 100; return result; } +[[nodiscard]] QString JoinChatDecisionToast( + not_null session, + PeerId peerId, + const MTPJoinChatBotResult &result) { + const auto isChannel = [&] { + if (const auto peer = session->data().peerLoaded(peerId)) { + return peer->isBroadcast(); + } + return false; + }(); + return result.match([&](const MTPDjoinChatBotResultApproved &) { + return isChannel + ? tr::lng_action_you_joined_by_request_channel(tr::now) + : tr::lng_action_you_joined_by_request(tr::now); + }, [&](const MTPDjoinChatBotResultDeclined &) { + return isChannel + ? tr::lng_group_request_declined_channel(tr::now) + : tr::lng_group_request_declined(tr::now); + }, [&](const MTPDjoinChatBotResultQueued &) { + return isChannel + ? tr::lng_group_request_sent_channel(tr::now) + : tr::lng_group_request_sent(tr::now); + }, [&](const MTPDjoinChatBotResultWebView &) { + return QString(); + }); +} + [[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig( not_null session) { const auto &appConfig = session->appConfig(); @@ -985,6 +1012,13 @@ void WebViewInstance::resolve() { } }, [&](WebViewSourceAgeVerification) { requestMain(); + }, [&](WebViewSourceJoinChat data) { + confirmOpen([=] { + show({ + .url = data.url, + .queryId = data.queryId, + }); + }, true); }); } @@ -1386,7 +1420,7 @@ void WebViewInstance::show(ShowArgs &&args) { not_null{ _bot }, &AttachWebViewBot::user); const auto hasOpenBot = v::is(_source) - || (_context.action->history->peer != _bot); + || (_context.action && _context.action->history->peer != _bot); const auto hasRemoveFromMenu = (attached != end(bots)) && (!attached->inactive || attached->inMainMenu) && (v::is(_source) @@ -1450,6 +1484,18 @@ void WebViewInstance::close() { } void WebViewInstance::started(uint64 queryId) { + if (v::is(_source)) { + if (!queryId) { + return; + } + _session->attachWebView().watchJoinChatWebView( + queryId, + _parentShow, + _context.controller, + this); + return; + } + Expects(_context.action.has_value()); if (!queryId) { @@ -2333,6 +2379,42 @@ AttachWebView::AttachWebView(not_null session) , _storage(std::make_unique(session)) , _refreshTimer([=] { requestBots(); }) { _refreshTimer.callEach(kRefreshBotsTimeout); + _session->data().joinChatWebViewDecision( + ) | rpl::on_next([=]( + const Data::Session::JoinChatWebViewDecision &decision) { + const auto i = _joinChatWebViews.find(decision.queryId); + if (i == end(_joinChatWebViews)) { + return; + } + auto data = std::move(i->second); + _joinChatWebViews.erase(i); + + const auto text = JoinChatDecisionToast( + _session, + decision.peerId, + decision.result); + const auto approved = (decision.result.type() + == mtpc_joinChatBotResultApproved); + const auto instance = data.instance.get(); + if (instance) { + close(instance); + if (approved) { + if (const auto peer = _session->data().peerLoaded( + decision.peerId)) { + if (const auto window = WindowForThread( + data.controller, + peer->owner().history(peer))) { + window->showPeerHistory( + peer, + Window::SectionShow::Way::Forward); + } + } + } + } + if (data.show && !text.isEmpty()) { + data.show->showToast(text); + } + }, _lifetime); } AttachWebView::~AttachWebView() { @@ -2385,6 +2467,18 @@ void AttachWebView::openByUsername( })); } +void AttachWebView::watchJoinChatWebView( + uint64 queryId, + std::shared_ptr show, + base::weak_ptr controller, + base::weak_ptr instance) { + _joinChatWebViews[queryId] = { + .show = std::move(show), + .controller = controller, + .instance = instance, + }; +} + void AttachWebView::close(not_null instance) { const auto i = ranges::find( _instances, diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 8d6e55839a..53cb0a6c7e 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -173,6 +173,15 @@ struct WebViewSourceAgeVerification { } }; +struct WebViewSourceJoinChat { + QString url; + uint64 queryId = 0; + + friend inline bool operator==( + const WebViewSourceJoinChat &, + const WebViewSourceJoinChat &) = default; +}; + struct WebViewSource : std::variant< WebViewSourceButton, WebViewSourceSwitch, @@ -184,7 +193,8 @@ struct WebViewSource : std::variant< WebViewSourceBotMenu, WebViewSourceGame, WebViewSourceBotProfile, - WebViewSourceAgeVerification> { + WebViewSourceAgeVerification, + WebViewSourceJoinChat> { using variant::variant; }; @@ -357,6 +367,11 @@ public: const QString &botUsername, const QString &startCommand, bool fullscreen); + void watchJoinChatWebView( + uint64 queryId, + std::shared_ptr show, + base::weak_ptr controller, + base::weak_ptr instance); void cancel(); @@ -445,11 +460,18 @@ private: rpl::event_stream<> _attachBotsUpdates; base::flat_set> _disclaimerAccepted; + struct JoinChatWebView { + std::shared_ptr show; + base::weak_ptr controller; + base::weak_ptr instance; + }; + base::flat_map _joinChatWebViews; std::vector> _instances; std::vector> _popularAppBots; mtpRequestId _popularAppBotsRequestId = 0; rpl::variable _popularAppBotsLoaded = false; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 2ad8d9c0dc..f2f907c7f0 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -86,7 +86,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#31774388 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?true bot_can_manage_bots:flags2.18?true bot_guestchat:flags2.19?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; +user#31774388 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?true bot_can_manage_bots:flags2.18?true bot_guestchat:flags2.19?true bot_guard:flags2.20?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -105,7 +105,7 @@ channel#1c32b11c flags:# creator:flags.0?true left:flags.2?true broadcast:flags. channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true monoforum:flags.10?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#e4e0b29d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab = ChatFull; +channelFull#a04e8d3a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab guard_bot_id:flags2.23?long = ChatFull; chatParticipant#38e79fde flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant; chatParticipantCreator#e1f867b8 flags:# user_id:long rank:flags.0?string = ChatParticipant; @@ -405,7 +405,7 @@ updateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update; updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector = Update; updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector = Update; -updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; +updateBotChatInviteRequester#7cb34d79 flags:# peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int query_id:flags.0?long = Update; updateMessageReactions#1e297bfa flags:# peer:Peer msg_id:int top_msg_id:flags.0?int saved_peer_id:flags.1?Peer reactions:MessageReactions = Update; updateAttachMenuBots#17b7a20b = Update; updateWebViewResultSent#1592b79d query_id:long = Update; @@ -467,6 +467,10 @@ updateChatParticipantRank#bd8367b9 chat_id:long user_id:long rank:string version updateManagedBot#4880ed9a user_id:long bot_id:long qts:int = Update; updateBotGuestChatQuery#cdd4093d flags:# query_id:long message:Message reference_messages:flags.0?Vector qts:int = Update; updateAiComposeTones#8c0f91fb = Update; +updateJoinChatWebViewDecision#bdac7e70 peer:Peer query_id:long result:JoinChatBotResult = Update; +updateNewBotConnection#b22083a6 flags:# confirmed:flags.0?true bot_id:long date:flags.1?int device:flags.1?string location:flags.1?string = Update; +updateWebBrowserSettings#c39a2ade flags:# open_external_browser:flags.0?true display_close_button:flags.1?true = Update; +updateWebBrowserException#140502d1 flags:# delete:flags.1?true open_external_browser:flags.0?Bool exception:WebDomainException = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1828,7 +1832,7 @@ inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; messages.quickReplies#c68d6695 quick_replies:Vector messages:Vector chats:Vector users:Vector = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; -connectedBot#cd64636c flags:# bot_id:long recipients:BusinessBotRecipients rights:BusinessBotRights = ConnectedBot; +connectedBot#33ed001 flags:# bot_id:long recipients:BusinessBotRecipients rights:BusinessBotRights device:flags.0?string date:flags.1?int location:flags.2?string = ConnectedBot; account.connectedBots#17d7f87b connected_bots:Vector users:Vector = account.ConnectedBots; @@ -2167,6 +2171,19 @@ aiComposeToneExample#f1d628ec from:TextWithEntities to:TextWithEntities = AiComp bots.accessSettings#dd1fbf93 flags:# restricted:flags.0?true add_users:flags.1?Vector = bots.AccessSettings; +messages.chatInviteJoinResultOk#445663a7 updates:Updates = messages.ChatInviteJoinResult; +messages.chatInviteJoinResultWebView#774bbdf4 bot_id:long url:string query_id:long users:Vector = messages.ChatInviteJoinResult; + +joinChatBotResultApproved#ae152a69 = JoinChatBotResult; +joinChatBotResultDeclined#efa0194 = JoinChatBotResult; +joinChatBotResultQueued#98a3a840 = JoinChatBotResult; +joinChatBotResultWebView#d6e3b813 url:string = JoinChatBotResult; + +webDomainException#933ca597 flags:# domain:string url:string title:string favicon:flags.0?long = WebDomainException; + +account.webBrowserSettingsNotModified#c31c8f4e = account.WebBrowserSettings; +account.webBrowserSettings#79eb8cb3 flags:# open_external_browser:flags.0?true display_close_button:flags.1?true external_exceptions:Vector inapp_exceptions:Vector hash:long = account.WebBrowserSettings; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2331,6 +2348,11 @@ account.initPasskeyRegistration#429547e8 = account.PasskeyRegistrationOptions; account.registerPasskey#55b41fd6 credential:InputPasskeyCredential = Passkey; account.getPasskeys#ea1f0c52 = account.Passkeys; account.deletePasskey#f5b5563f id:string = Bool; +account.confirmBotConnection#67ed1f68 bot_id:InputUser = Bool; +account.getWebBrowserSettings#56655768 hash:long = account.WebBrowserSettings; +account.updateWebBrowserSettings#9adf82fe flags:# open_external_browser:flags.0?true display_close_button:flags.1?true = account.WebBrowserSettings; +account.toggleWebBrowserSettingsException#2d0a0571 flags:# delete:flags.1?true open_external_browser:flags.0?Bool url:string = Bool; +account.deleteWebBrowserSettingsExceptions#86a0765d = account.WebBrowserSettings; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -2408,7 +2430,7 @@ messages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers; messages.getWebPagePreview#570d6f6f flags:# message:string entities:flags.3?Vector = messages.WebPagePreview; messages.exportChatInvite#a455de90 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string subscription_pricing:flags.5?StarsSubscriptionPricing = ExportedChatInvite; messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; -messages.importChatInvite#6c50051c hash:string = Updates; +messages.importChatInvite#de91436e hash:string = messages.ChatInviteJoinResult; messages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; @@ -2684,7 +2706,7 @@ channels.editTitle#566decd0 channel:InputChannel title:string = Updates; channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; channels.updateUsername#3514b3de channel:InputChannel username:string = Bool; -channels.joinChannel#24b524c5 channel:InputChannel = Updates; +channels.joinChannel#7f6a1e22 channel:InputChannel = messages.ChatInviteJoinResult; channels.leaveChannel#f836aa95 channel:InputChannel = Updates; channels.inviteToChannel#c9e33d54 channel:InputChannel users:Vector = messages.InvitedUsers; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; @@ -2707,7 +2729,7 @@ channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; channels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true for_live_stories:flags.1?true peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; -channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; +channels.toggleJoinRequest#ecc2618 flags:# apply_to_invites:flags.1?true channel:InputChannel enabled:Bool guard_bot:flags.0?InputUser = Updates; channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector = Bool; channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool; channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool; @@ -2766,6 +2788,7 @@ bots.requestWebViewButton#31a2a35e user_id:InputUser button:KeyboardButton = bot bots.getRequestedWebViewButton#bf25b7f3 bot:InputUser webapp_req_id:string = KeyboardButton; bots.getAccessSettings#213853a3 bot:InputUser = bots.AccessSettings; bots.editAccessSettings#31813cd8 flags:# restricted:flags.0?true bot:InputUser add_users:flags.1?Vector = Bool; +bots.setJoinChatResults#e71a4810 query_id:long result:JoinChatBotResult = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2976,4 +2999,4 @@ aicompose.getTone#b2e8ba03 tone:InputAiComposeTone = aicompose.Tones; aicompose.getTones#abd59201 hash:long = aicompose.Tones; aicompose.getToneExample#d1b4ab14 tone:InputAiComposeTone num:int = AiComposeToneExample; -// LAYER 225 +// LAYER 226 diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp index d72a0511bc..4d72f26f3b 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/vertical_list.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/number_input.h" @@ -43,6 +44,39 @@ constexpr auto kMaxLabelLength = 32; } } +[[nodiscard]] TextWithEntities RequestApprovalAbout( + bool enabled, + bool locked, + bool isGroup, + const QString &guardBotUsername, + const QString &guardBotLink) { + if (locked) { + return TextWithEntities{ + (isGroup + ? tr::lng_group_invite_about_approve_disabled + : tr::lng_group_invite_about_approve_disabled_channel)( + tr::now) + }; + } else if (enabled && !guardBotUsername.isEmpty()) { + return (isGroup + ? tr::lng_group_invite_about_approve_managed + : tr::lng_group_invite_about_approve_managed_channel)( + tr::now, + lt_bot, + tr::link('@' + guardBotUsername, guardBotLink), + tr::marked); + } + return TextWithEntities{ + enabled + ? (isGroup + ? tr::lng_group_invite_about_approve + : tr::lng_group_invite_about_approve_channel)(tr::now) + : (isGroup + ? tr::lng_group_invite_about_no_approve + : tr::lng_group_invite_about_no_approve_channel)(tr::now) + }; +} + } // namespace void EditInviteLinkBox( @@ -55,7 +89,11 @@ void EditInviteLinkBox( const auto link = data.link; const auto isGroup = data.isGroup; const auto isPublic = data.isPublic; + const auto globalRequestApproval = data.globalRequestApproval; + const auto guardBotUsername = data.guardBotUsername; + const auto guardBotLink = data.guardBotLink; const auto subscriptionLocked = data.subscriptionCredits > 0; + const auto requestApprovalLocked = isPublic && !globalRequestApproval; box->setTitle(link.isEmpty() ? tr::lng_group_invite_new_title() : tr::lng_group_invite_edit_title()); @@ -86,6 +124,20 @@ void EditInviteLinkBox( st::defaultBoxDividerLabelPadding), margins); }; + const auto addRichDivider = [&]( + not_null container, + rpl::producer text, + style::margins margins = style::margins()) { + container->add( + object_ptr( + container, + object_ptr( + container, + std::move(text), + st::boxDividerLabel), + st::defaultBoxDividerLabelPadding), + margins); + }; const auto now = base::unixtime::now(); const auto expire = data.expireDate ? data.expireDate : kMaxLimit; @@ -105,33 +157,10 @@ void EditInviteLinkBox( const auto state = box->lifetime().make_state(State{ .expireValue = expire, .usageValue = usage, - .requestApproval = (data.requestApproval && !isPublic), + .requestApproval = data.requestApproval, .subscription = false, }); - const auto requestApproval = (isPublic || subscriptionLocked) - ? nullptr - : container->add( - object_ptr( - container, - tr::lng_group_invite_request_approve(), - st::settingsButtonNoIcon), - style::margins{ 0, 0, 0, st::defaultVerticalListSkip }); - if (requestApproval) { - requestApproval->toggleOn(state->requestApproval.value(), true); - requestApproval->setClickedCallback([=] { - state->requestApproval.force_assign(!requestApproval->toggled()); - state->subscription.force_assign(false); - }); - addDivider(container, rpl::conditional( - state->requestApproval.value(), - (isGroup - ? tr::lng_group_invite_about_approve() - : tr::lng_group_invite_about_approve_channel()), - (isGroup - ? tr::lng_group_invite_about_no_approve() - : tr::lng_group_invite_about_no_approve_channel()))); - } auto credits = (Ui::NumberInput*)(nullptr); if (!isPublic && fillSubscription) { Ui::AddSkip(container); @@ -157,49 +186,57 @@ void EditInviteLinkBox( }); } - const auto labelField = container->add( - object_ptr( - container, - st::defaultInputField, - tr::lng_group_invite_label_header(), - data.label), - style::margins( - st::defaultSubsectionTitlePadding.left(), - st::defaultVerticalListSkip, - st::defaultSubsectionTitlePadding.right(), - st::defaultVerticalListSkip * 2)); - labelField->setMaxLength(kMaxLabelLength); - addDivider(container, tr::lng_group_invite_label_about()); - - const auto &saveLabel = link.isEmpty() - ? tr::lng_formatting_link_create - : tr::lng_settings_save; - box->addButton(saveLabel(), [=] { - const auto label = labelField->getLastText(); - const auto expireDate = (state->expireValue == kMaxLimit) - ? 0 - : (state->expireValue < 0) - ? (base::unixtime::now() - state->expireValue) - : state->expireValue; - const auto usageLimit = (state->usageValue == kMaxLimit) - ? 0 - : state->usageValue; - done(InviteLinkFields{ - .link = link, - .label = label, - .expireDate = expireDate, - .usageLimit = usageLimit, - .subscriptionCredits = credits - ? credits->getLastText().toInt() - : 0, - .requestApproval = state->requestApproval.current(), - .isGroup = isGroup, - .isPublic = isPublic, + const auto addLabelField = [=] { + const auto result = container->add( + object_ptr( + container, + st::defaultInputField, + tr::lng_group_invite_label_header(), + data.label), + style::margins( + st::defaultSubsectionTitlePadding.left(), + st::defaultVerticalListSkip, + st::defaultSubsectionTitlePadding.right(), + st::defaultVerticalListSkip * 2)); + result->setMaxLength(kMaxLabelLength); + addDivider(container, tr::lng_group_invite_label_about()); + return result; + }; + const auto addSaveButtons = [=](not_null labelField) { + const auto &saveLabel = link.isEmpty() + ? tr::lng_formatting_link_create + : tr::lng_settings_save; + box->addButton(saveLabel(), [=] { + const auto label = labelField->getLastText(); + const auto expireDate = (state->expireValue == kMaxLimit) + ? 0 + : (state->expireValue < 0) + ? (base::unixtime::now() - state->expireValue) + : state->expireValue; + const auto usageLimit = (state->usageValue == kMaxLimit) + ? 0 + : state->usageValue; + done(InviteLinkFields{ + .link = link, + .label = label, + .expireDate = expireDate, + .usageLimit = usageLimit, + .subscriptionCredits = credits + ? credits->getLastText().toInt() + : 0, + .requestApproval = state->requestApproval.current(), + .isGroup = isGroup, + .isPublic = isPublic, + .globalRequestApproval = globalRequestApproval, + .guardBotUsername = guardBotUsername, + .guardBotLink = guardBotLink, + }); }); - }); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; if (subscriptionLocked) { + addSaveButtons(addLabelField()); return; } addTitle(container, tr::lng_group_invite_expire_title()); @@ -373,6 +410,39 @@ void EditInviteLinkBox( usagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1)); usagesSlide->finishAnimating(); + + const auto skip = st::defaultVerticalListSkip; + const auto requestApproval = container->add( + object_ptr( + container, + (isGroup + ? tr::lng_group_invite_request_approve + : tr::lng_group_invite_request_approve_channel)(), + st::settingsButtonNoIcon), + style::margins{ 0, skip, 0, skip }); + requestApproval->toggleOn(state->requestApproval.value(), true); + requestApproval->setToggleLocked(requestApprovalLocked); + requestApproval->finishAnimating(); + requestApproval->setClickedCallback([=] { + if (requestApprovalLocked) { + return; + } + state->requestApproval.force_assign(!requestApproval->toggled()); + state->subscription.force_assign(false); + }); + addRichDivider( + container, + state->requestApproval.value( + ) | rpl::map([=](bool enabled) { + return RequestApprovalAbout( + enabled, + requestApprovalLocked, + isGroup, + guardBotUsername, + guardBotLink); + })); + + addSaveButtons(addLabelField()); } void CreateInviteLinkBox( @@ -380,11 +450,20 @@ void CreateInviteLinkBox( Fn fillSubscription, bool isGroup, bool isPublic, + bool globalRequestApproval, + const QString &guardBotUsername, + const QString &guardBotLink, Fn done) { EditInviteLinkBox( box, std::move(fillSubscription), - InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic }, + InviteLinkFields{ + .isGroup = isGroup, + .isPublic = isPublic, + .globalRequestApproval = globalRequestApproval, + .guardBotUsername = guardBotUsername, + .guardBotLink = guardBotLink, + }, std::move(done)); } diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link.h b/Telegram/SourceFiles/ui/boxes/edit_invite_link.h index 0b9805cd69..dfe35f5e1f 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link.h +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link.h @@ -22,6 +22,9 @@ struct InviteLinkFields { bool requestApproval = false; bool isGroup = false; bool isPublic = false; + bool globalRequestApproval = false; + QString guardBotUsername; + QString guardBotLink; }; struct InviteLinkSubscriptionToggle final { @@ -40,6 +43,9 @@ void CreateInviteLinkBox( Fn fillSubscription, bool isGroup, bool isPublic, + bool globalRequestApproval, + const QString &guardBotUsername, + const QString &guardBotLink, Fn done); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 794359f9ca..f5267ad4cd 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -343,6 +343,7 @@ boostReplaceUserpic: UserpicButton(defaultUserpicButton) { photoSize: 60px; } boostReplaceIcon: icon{{ "stories/boost_mini", premiumButtonFg }}; +guardBotReplaceIcon: icon{{ "chat/mini_shield_lock", premiumButtonFg }}; boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px);