Suggest inviting by link if privacy disallows adding.

This commit is contained in:
John Preston 2023-03-10 18:43:20 +04:00
parent 7682ccf6a7
commit f3e15c7fcd
11 changed files with 329 additions and 21 deletions

View File

@ -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";

View File

@ -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<PeerData*> peer,
const std::vector<not_null<UserData*>> &users,
std::shared_ptr<Ui::Show> show,
bool passGroupHistory,
Fn<void(bool)> 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<MTPInputUser>();
@ -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();
};

View File

@ -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<PeerData*> peer,
const std::vector<not_null<UserData*>> &users,
std::shared_ptr<Ui::Show> show = nullptr,
bool passGroupHistory = true,
Fn<void(bool)> done = nullptr);

View File

@ -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();
}

View File

@ -112,7 +112,12 @@ void ChatCreateDone(
if (done) {
done(chat);
} else {
const auto show = std::make_shared<Window::Show>(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<PeerData*> chat,
const std::vector<not_null<UserData*>> &users) {
const std::vector<not_null<UserData*>> &users,
std::shared_ptr<Ui::Show> 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<PeerListBox*> 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));

View File

@ -48,7 +48,8 @@ enum class PeerFloodType {
void ShowAddParticipantsError(
const QString &error,
not_null<PeerData*> chat,
const std::vector<not_null<UserData*>> &users);
const std::vector<not_null<UserData*>> &users,
std::shared_ptr<Ui::Show> show = nullptr);
class AddContactBox : public Ui::BoxContent {
public:

View File

@ -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<PeerData*> peer,
std::vector<not_null<UserData*>> users);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
[[nodiscard]] bool canInvite() const {
return _can;
}
[[nodiscard]] rpl::producer<int> selectedValue() const {
return _selected.value();
}
void send(
std::vector<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> close);
private:
void appendRow(not_null<UserData*> user);
[[nodiscard]] std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const;
const not_null<PeerData*> _peer;
const std::vector<not_null<UserData*>> _users;
const bool _can = false;
rpl::variable<int> _selected;
bool _sending = false;
};
base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
if (!peer) {
return {};
@ -54,6 +92,148 @@ base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
return {};
}
InviteForbiddenController::InviteForbiddenController(
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> 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<float64>(count),
Ui::Text::RichLangValue)
: phraseNamed(
lt_user,
rpl::single(TextWithEntities{ _users.front()->name() }),
Ui::Text::RichLangValue);
delegate()->peerListSetAboveWidget(object_ptr<Ui::PaddingWrap<>>(
(QWidget*)nullptr,
object_ptr<Ui::FlatLabel>(
(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<PeerListRow*> row) {
if (!_can) {
return;
}
const auto checked = row->checked();
delegate()->peerListSetRowChecked(row, !checked);
_selected = _selected.current() + (checked ? -1 : 1);
}
void InviteForbiddenController::appendRow(not_null<UserData*> 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<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> 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<PeerListRow> InviteForbiddenController::createRow(
not_null<UserData*> user) const {
return std::make_unique<PeerListRow>(user);
}
} // namespace
AddParticipantsBoxController::AddParticipantsBoxController(
@ -245,14 +425,19 @@ void AddParticipantsBoxController::inviteSelectedUsers(
if (users.empty()) {
return;
}
const auto show = std::make_shared<Ui::BoxShow>(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<Ui::GenericBox*> box) {
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
auto checkbox = object_ptr<Ui::Checkbox>(
box.get(),
tr::lng_participant_invite_history(),
@ -371,6 +556,81 @@ void AddParticipantsBoxController::Start(
Start(navigation, channel, {}, true);
}
std::vector<not_null<UserData*>> CollectForbiddenUsers(
not_null<Main::Session*> session,
const MTPUpdates &updates) {
const auto owner = &session->data();
auto result = std::vector<not_null<UserData*>>();
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<MTPUpdate> &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<Ui::Show> show,
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> forbidden) {
if (forbidden.empty() || !show || !show->valid()) {
return false;
}
auto controller = std::make_unique<InviteForbiddenController>(
peer,
std::move(forbidden));
const auto weak = controller.get();
auto initBox = [=](not_null<PeerListBox*> 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<Ui::BoxShow>(box),
crl::guard(box, [=] { box->closeBox(); }));
});
}
box->addButton(tr::lng_create_group_skip(), [=] {
box->closeBox();
});
}, box->lifetime());
};
show->showBox(
Box<PeerListBox>(std::move(controller), std::move(initBox)),
Ui::LayerOption::KeepOther);
return true;
}
AddSpecialBoxController::AddSpecialBoxController(
not_null<PeerData*> peer,
Role role,

View File

@ -73,6 +73,14 @@ private:
};
[[nodiscard]] std::vector<not_null<UserData*>> CollectForbiddenUsers(
not_null<Main::Session*> session,
const MTPUpdates &updates);
bool ChatInviteForbidden(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> forbidden);
// Adding an admin, banned or restricted user from channel members
// with search + contacts search + global search.
class AddSpecialBoxController

View File

@ -219,12 +219,14 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
}
};
const auto inviteWithAdd = [=](
std::shared_ptr<Ui::Show> show,
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> finish) {
peer->session().api().chatParticipants().add(
peer,
nonMembers,
show,
true,
[=](bool) { invite(users); finish(); });
};
@ -257,7 +259,10 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
finish();
};
const auto done = [=] {
inviteWithAdd(users, nonMembers, finishWithConfirm);
const auto show = (*shared)
? std::make_shared<Ui::BoxShow>(*shared)
: nullptr;
inviteWithAdd(show, users, nonMembers, finishWithConfirm);
};
auto box = ConfirmBox({
.text = text,

View File

@ -360,7 +360,9 @@ void MainWindow::ensureLayerCreated() {
return;
}
_layer = base::make_unique_q<Ui::LayerStackWidget>(
bodyWidget());
bodyWidget(),
crl::guard(this, [=] {
return std::make_shared<Window::Show>(&controller()); }));
_layer->hideFinishEvents(
) | rpl::filter([=] {

@ -1 +1 @@
Subproject commit f3744c4ba3ddadd47e280a1ef5fbd002d357d10a
Subproject commit 849a84050356a1321eedc36eb5296374e42fadd6