From f3e15c7fcd09d0cd065db45fbf4c01b405c632de Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 10 Mar 2023 18:43:20 +0400 Subject: [PATCH] Suggest inviting by link if privacy disallows adding. --- Telegram/Resources/langs/lang.strings | 15 + .../SourceFiles/api/api_chat_participants.cpp | 13 +- .../SourceFiles/api/api_chat_participants.h | 5 + Telegram/SourceFiles/apiwrap.cpp | 3 +- .../SourceFiles/boxes/add_contact_box.cpp | 26 +- Telegram/SourceFiles/boxes/add_contact_box.h | 3 +- .../boxes/peers/add_participants_box.cpp | 264 +++++++++++++++++- .../boxes/peers/add_participants_box.h | 8 + .../group/calls_group_invite_controller.cpp | 7 +- Telegram/SourceFiles/mainwindow.cpp | 4 +- Telegram/lib_ui | 2 +- 11 files changed, 329 insertions(+), 21 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3071b81bf2..1dfb10eb9a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1110,6 +1110,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_copy_phone" = "Copy Phone Number"; "lng_profile_copy_fullname" = "Copy Name"; +"lng_via_link_group_one" = "**{user}** restricts adding them to groups.\nYou can send them an invite link as message instead."; +"lng_via_link_group_many#one" = "**{count} user** restricts adding them to groups.\nYou can send them an invite link as message instead."; +"lng_via_link_group_many#other" = "**{count} users** restrict adding them to groups.\nYou can send them an invite link as message instead."; +"lng_via_link_channel_one" = "**{user}** restricts adding them to channels.\nYou can send them an invite link as message instead."; +"lng_via_link_channel_many#one" = "**{count} user** restricts adding them to channels.\nYou can send them an invite link as message instead."; +"lng_via_link_channel_many#other" = "**{count} users** restrict adding them to channels.\nYou can send them an invite link as message instead."; +"lng_via_link_send" = "Send Invite Link"; +"lng_via_link_cant" = "You can't create a link"; +"lng_via_link_cant_one" = "**{user}** can only by invited via link, but you don't have permission to share invite links to this group."; +"lng_via_link_cant_many#one" = "**{count} user** can only by invited via link, but you don't have permission to share invite links to this group."; +"lng_via_link_cant_many#other" = "**{count} users** can only by invited via link, but you don't have permission to share invite links to this group."; +"lng_via_link_shared_one" = "Link shared with **{user}**."; +"lng_via_link_shared_many#one" = "Link shared with **{count} user**."; +"lng_via_link_shared_many#other" = "Link shared with **{count} users**."; + "lng_info_mobile_label" = "Mobile"; "lng_info_mobile_context_menu_fragment_about" = "This number is not tied to a SIM card and was acquired on {link}."; "lng_info_mobile_context_menu_fragment_about_link" = "Fragment"; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index 531ff17100..aeb7bd0a6a 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/add_contact_box.h" // ShowAddParticipantsError +#include "boxes/peers/add_participants_box.h" // ChatInviteForbidden #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_channel_admins.h" @@ -463,6 +464,7 @@ void ChatParticipants::requestCountDelayed( void ChatParticipants::add( not_null peer, const std::vector> &users, + std::shared_ptr show, bool passGroupHistory, Fn done) { if (const auto chat = peer->asChat()) { @@ -475,14 +477,15 @@ void ChatParticipants::add( chat->session().api().applyUpdates(result); if (done) done(true); }).fail([=](const MTP::Error &error) { - ShowAddParticipantsError(error.type(), peer, { 1, user }); + const auto type = error.type(); + ShowAddParticipantsError(type, peer, { 1, user }, show); if (done) done(false); }).afterDelay(kSmallDelayMs).send(); } } else if (const auto channel = peer->asChannel()) { const auto hasBot = ranges::any_of(users, &UserData::isBot); if (!peer->isMegagroup() && hasBot) { - ShowAddParticipantsError("USER_BOT", peer, users); + ShowAddParticipantsError("USER_BOT", peer, users, show); return; } auto list = QVector(); @@ -496,8 +499,12 @@ void ChatParticipants::add( channel->session().api().applyUpdates(result); requestCountDelayed(channel); if (callback) callback(true); + ChatInviteForbidden( + show, + channel, + CollectForbiddenUsers(&channel->session(), result)); }).fail([=](const MTP::Error &error) { - ShowAddParticipantsError(error.type(), peer, users); + ShowAddParticipantsError(error.type(), peer, users, show); if (callback) callback(false); }).afterDelay(kSmallDelayMs).send(); }; diff --git a/Telegram/SourceFiles/api/api_chat_participants.h b/Telegram/SourceFiles/api/api_chat_participants.h index 644a8bfec9..41eda6fe68 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.h +++ b/Telegram/SourceFiles/api/api_chat_participants.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class ApiWrap; class ChannelData; +namespace Ui { +class Show; +} // namespace Ui + namespace Api { class ChatParticipant final { @@ -95,6 +99,7 @@ public: void add( not_null peer, const std::vector> &users, + std::shared_ptr show = nullptr, bool passGroupHistory = true, Fn done = nullptr); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index f30b54c612..2826837412 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3709,7 +3709,8 @@ void ApiWrap::sendBotStart( applyUpdates(result); }).fail([=](const MTP::Error &error) { if (chat) { - ShowAddParticipantsError(error.type(), chat, { 1, bot }); + const auto type = error.type(); + ShowAddParticipantsError(type, chat, { 1, bot }); } }).send(); } diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 486570e484..a65a155ca4 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -112,7 +112,12 @@ void ChatCreateDone( if (done) { done(chat); } else { + const auto show = std::make_shared(navigation); navigation->showPeerHistory(chat); + ChatInviteForbidden( + show, + chat, + CollectForbiddenUsers(&chat->session(), updates)); } }; if (!success) { @@ -173,7 +178,8 @@ TextWithEntities PeerFloodErrorText( void ShowAddParticipantsError( const QString &error, not_null chat, - const std::vector> &users) { + const std::vector> &users, + std::shared_ptr show) { if (error == u"USER_BOT"_q) { const auto channel = chat->asChannel(); if ((users.size() == 1) @@ -220,6 +226,9 @@ void ShowAddParticipantsError( const auto text = PeerFloodErrorText(&chat->session(), type); Ui::show(Ui::MakeInformBox(text), Ui::LayerOption::KeepOther); return; + } else if (error == u"USER_PRIVACY_RESTRICTED"_q && show) { + ChatInviteForbidden(show, chat, users); + return; } const auto text = [&] { if (error == u"USER_BOT"_q) { @@ -689,9 +698,6 @@ void GroupInfoBox::createGroup( inputs.push_back(user->inputUser); } } - if (inputs.empty()) { - return; - } _creationRequestId = _api.request(MTPmessages_CreateChat( MTP_flags(_ttlPeriod ? MTPmessages_CreateChat::Flag::f_ttl_period @@ -703,6 +709,7 @@ void GroupInfoBox::createGroup( auto image = _photo->takeResultImage(); const auto period = _ttlPeriod; const auto navigation = _navigation; + const auto controller = navigation->parentController(); const auto done = _done; getDelegate()->hideLayer(); // Destroys 'this'. @@ -763,13 +770,10 @@ void GroupInfoBox::submit() { not_null box) { auto create = [box, title, weak] { if (const auto strong = weak.data()) { - auto rows = box->collectSelectedRows(); - if (!rows.empty()) { - strong->createGroup( - box.get(), - title, - std::move(rows)); - } + strong->createGroup( + box.get(), + title, + box->collectSelectedRows()); } }; box->addButton(tr::lng_create_group_create(), std::move(create)); diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index dd181349a8..5e62331aac 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -48,7 +48,8 @@ enum class PeerFloodType { void ShowAddParticipantsError( const QString &error, not_null chat, - const std::vector> &users); + const std::vector> &users, + std::shared_ptr show = nullptr); class AddContactBox : public Ui::BoxContent { public: diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 3686142b09..a7b3a6a2fd 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_participants_box.h" #include "api/api_chat_participants.h" +#include "api/api_invite_links.h" #include "boxes/peers/edit_participant_box.h" #include "boxes/peers/edit_peer_type_box.h" #include "ui/boxes/confirm_box.h" @@ -22,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "dialogs/dialogs_indexed_list.h" #include "ui/text/text_utilities.h" // Ui::Text::RichLangValue +#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/padding_wrap.h" @@ -33,12 +35,48 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" +#include "styles/style_settings.h" namespace { constexpr auto kParticipantsFirstPageCount = 16; constexpr auto kParticipantsPerPage = 200; +class InviteForbiddenController final : public PeerListController { +public: + InviteForbiddenController( + not_null peer, + std::vector> users); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + + [[nodiscard]] bool canInvite() const { + return _can; + } + [[nodiscard]] rpl::producer selectedValue() const { + return _selected.value(); + } + + void send( + std::vector> list, + Ui::ShowPtr show, + Fn close); + +private: + void appendRow(not_null user); + [[nodiscard]] std::unique_ptr createRow( + not_null user) const; + + const not_null _peer; + const std::vector> _users; + const bool _can = false; + rpl::variable _selected; + bool _sending = false; + +}; + base::flat_set> GetAlreadyInFromPeer(PeerData *peer) { if (!peer) { return {}; @@ -54,6 +92,148 @@ base::flat_set> GetAlreadyInFromPeer(PeerData *peer) { return {}; } +InviteForbiddenController::InviteForbiddenController( + not_null peer, + std::vector> users) +: _peer(peer) +, _users(std::move(users)) +, _can(peer->isChat() + ? peer->asChat()->canHaveInviteLink() + : peer->asChannel()->canHaveInviteLink()) +, _selected(_can ? int(_users.size()) : 0) { +} + +Main::Session &InviteForbiddenController::session() const { + return _peer->session(); +} + +void InviteForbiddenController::prepare() { + const auto broadcast = _peer->isBroadcast(); + const auto count = int(_users.size()); + const auto phraseCounted = !_can + ? tr::lng_via_link_cant_many + : broadcast + ? tr::lng_via_link_channel_many + : tr::lng_via_link_group_many; + const auto phraseNamed = !_can + ? tr::lng_via_link_cant_one + : broadcast + ? tr::lng_via_link_channel_one + : tr::lng_via_link_group_one; + auto text = (count != 1) + ? phraseCounted( + lt_count, + rpl::single(count), + Ui::Text::RichLangValue) + : phraseNamed( + lt_user, + rpl::single(TextWithEntities{ _users.front()->name() }), + Ui::Text::RichLangValue); + delegate()->peerListSetAboveWidget(object_ptr>( + (QWidget*)nullptr, + object_ptr( + (QWidget*)nullptr, + std::move(text), + st::requestPeerRestriction), + st::boxRowPadding)); + delegate()->peerListSetTitle( + _can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant()); + + for (const auto &user : _users) { + appendRow(user); + } + delegate()->peerListRefreshRows(); +} + +void InviteForbiddenController::rowClicked(not_null row) { + if (!_can) { + return; + } + const auto checked = row->checked(); + delegate()->peerListSetRowChecked(row, !checked); + _selected = _selected.current() + (checked ? -1 : 1); +} + +void InviteForbiddenController::appendRow(not_null user) { + if (!delegate()->peerListFindRow(user->id.value)) { + auto row = createRow(user); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + if (_can) { + delegate()->peerListSetRowChecked(raw, true); + } + } +} + +void InviteForbiddenController::send( + std::vector> list, + Ui::ShowPtr show, + Fn close) { + if (_sending || list.empty()) { + return; + } + _sending = true; + const auto chat = _peer->asChat(); + const auto channel = _peer->asChannel(); + const auto sendLink = [=] { + const auto link = chat ? chat->inviteLink() : channel->inviteLink(); + if (link.isEmpty()) { + return false; + } + auto &api = _peer->session().api(); + auto options = Api::SendOptions(); + for (const auto &to : list) { + const auto history = to->owner().history(to); + auto message = Api::MessageToSend( + Api::SendAction(history, options)); + message.textWithTags = { link }; + message.action.clearDraft = false; + api.sendMessage(std::move(message)); + } + auto text = (list.size() == 1) + ? tr::lng_via_link_shared_one( + tr::now, + lt_user, + TextWithEntities{ list.front()->name() }, + Ui::Text::RichLangValue) + : tr::lng_via_link_shared_many( + tr::now, + lt_count, + int(list.size()), + Ui::Text::RichLangValue); + close(); + Ui::Toast::Show( + show->toastParent(), + { .text = std::move(text), .st = &st::defaultToast }); + return true; + }; + const auto sendForFull = [=] { + if (!sendLink()) { + _peer->session().api().inviteLinks().create(_peer, [=](auto) { + if (!sendLink()) { + close(); + } + }); + } + }; + if (_peer->isFullLoaded()) { + sendForFull(); + } else if (!sendLink()) { + _peer->session().api().requestFullPeer(_peer); + _peer->session().changes().peerUpdates( + _peer, + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=] { + sendForFull(); + }, lifetime()); + } +} + +std::unique_ptr InviteForbiddenController::createRow( + not_null user) const { + return std::make_unique(user); +} + } // namespace AddParticipantsBoxController::AddParticipantsBoxController( @@ -245,14 +425,19 @@ void AddParticipantsBoxController::inviteSelectedUsers( if (users.empty()) { return; } + const auto show = std::make_shared(box); const auto request = [=](bool checked) { - _peer->session().api().chatParticipants().add(_peer, users, checked); + _peer->session().api().chatParticipants().add( + _peer, + users, + show, + checked); }; if (_peer->isChannel()) { request(false); return done(); } - Ui::BoxShow(box).showBox(Box([=](not_null box) { + show->showBox(Box([=](not_null box) { auto checkbox = object_ptr( box.get(), tr::lng_participant_invite_history(), @@ -371,6 +556,81 @@ void AddParticipantsBoxController::Start( Start(navigation, channel, {}, true); } +std::vector> CollectForbiddenUsers( + not_null session, + const MTPUpdates &updates) { + const auto owner = &session->data(); + auto result = std::vector>(); + const auto add = [&](const MTPUpdate &update) { + if (update.type() == mtpc_updateGroupInvitePrivacyForbidden) { + const auto user = owner->userLoaded(UserId( + update.c_updateGroupInvitePrivacyForbidden().vuser_id())); + if (user) { + result.push_back(user); + } + } + }; + const auto collect = [&](const MTPVector &updates) { + for (const auto &update : updates.v) { + add(update); + } + }; + updates.match([&](const MTPDupdates &data) { + collect(data.vupdates()); + }, [&](const MTPDupdatesCombined &data) { + collect(data.vupdates()); + }, [&](const MTPDupdateShort &data) { + add(data.vupdate()); + }, [](const auto &other) { + LOG(("Api Error: CollectForbiddenUsers for wrong updates type.")); + }); + return result; +} + +bool ChatInviteForbidden( + std::shared_ptr show, + not_null peer, + std::vector> forbidden) { + if (forbidden.empty() || !show || !show->valid()) { + return false; + } + auto controller = std::make_unique( + peer, + std::move(forbidden)); + const auto weak = controller.get(); + auto initBox = [=](not_null box) { + const auto can = weak->canInvite(); + if (!can) { + box->addButton(tr::lng_close(), [=] { + box->closeBox(); + }); + return; + } + weak->selectedValue( + ) | rpl::map( + rpl::mappers::_1 > 0 + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool has) { + box->clearButtons(); + if (has) { + box->addButton(tr::lng_via_link_send(), [=] { + weak->send( + box->collectSelectedRows(), + std::make_shared(box), + crl::guard(box, [=] { box->closeBox(); })); + }); + } + box->addButton(tr::lng_create_group_skip(), [=] { + box->closeBox(); + }); + }, box->lifetime()); + }; + show->showBox( + Box(std::move(controller), std::move(initBox)), + Ui::LayerOption::KeepOther); + return true; +} + AddSpecialBoxController::AddSpecialBoxController( not_null peer, Role role, diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.h b/Telegram/SourceFiles/boxes/peers/add_participants_box.h index a1c59c6973..c86c996877 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.h @@ -73,6 +73,14 @@ private: }; +[[nodiscard]] std::vector> CollectForbiddenUsers( + not_null session, + const MTPUpdates &updates); +bool ChatInviteForbidden( + std::shared_ptr show, + not_null peer, + std::vector> forbidden); + // Adding an admin, banned or restricted user from channel members // with search + contacts search + global search. class AddSpecialBoxController diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index d5cb2f1452..fd02256346 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -219,12 +219,14 @@ object_ptr PrepareInviteBox( } }; const auto inviteWithAdd = [=]( + std::shared_ptr show, const std::vector> &users, const std::vector> &nonMembers, Fn finish) { peer->session().api().chatParticipants().add( peer, nonMembers, + show, true, [=](bool) { invite(users); finish(); }); }; @@ -257,7 +259,10 @@ object_ptr PrepareInviteBox( finish(); }; const auto done = [=] { - inviteWithAdd(users, nonMembers, finishWithConfirm); + const auto show = (*shared) + ? std::make_shared(*shared) + : nullptr; + inviteWithAdd(show, users, nonMembers, finishWithConfirm); }; auto box = ConfirmBox({ .text = text, diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index 2425115810..e19cfeab8b 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -360,7 +360,9 @@ void MainWindow::ensureLayerCreated() { return; } _layer = base::make_unique_q( - bodyWidget()); + bodyWidget(), + crl::guard(this, [=] { + return std::make_shared(&controller()); })); _layer->hideFinishEvents( ) | rpl::filter([=] { diff --git a/Telegram/lib_ui b/Telegram/lib_ui index f3744c4ba3..849a840503 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit f3744c4ba3ddadd47e280a1ef5fbd002d357d10a +Subproject commit 849a84050356a1321eedc36eb5296374e42fadd6