Support complex invite-users responses.

This commit is contained in:
John Preston 2024-04-01 22:39:29 +04:00
parent 3cdb528dfe
commit c0117e72ad
15 changed files with 631 additions and 43 deletions

View File

@ -1319,6 +1319,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_photo_by_you" = "photo set by you";
"lng_profile_public_photo" = "public photo";
"lng_invite_upgrade_title" = "Upgrade to Premium";
"lng_invite_upgrade_group_invite#one" = "{users} only accepts invitations to groups from Contacts and **Premium** users.";
"lng_invite_upgrade_group_invite#other" = "{users} only accept invitations to groups from Contacts and **Premium** users.";
"lng_invite_upgrade_group_write#one" = "{users} only accepts messages and invitations to groups from Contacts and **Premium** users.";
"lng_invite_upgrade_group_write#other" = "{users} only accept messages and invitations to groups from Contacts and **Premium** users.";
"lng_invite_upgrade_channel_invite#one" = "{users} only accepts invitations to channels from Contacts and **Premium** users.";
"lng_invite_upgrade_channel_invite#other" = "{users} only accept invitations to channels from Contacts and **Premium** users.";
"lng_invite_upgrade_channel_write#one" = "{users} only accepts messages and invitations to channels from Contacts and **Premium** users.";
"lng_invite_upgrade_channel_write#other" = "{users} only accept messages and invitations to channels from Contacts and **Premium** users.";
"lng_invite_upgrade_users_few" = "{users} and {last}";
"lng_invite_upgrade_users_many#one" = "{users} and **{count}** more person";
"lng_invite_upgrade_users_many#other" = "{users} and **{count}** more people";
"lng_invite_upgrade_or" = "or";
"lng_invite_upgrade_via_title" = "Invite via Link";
"lng_invite_upgrade_via_group_about" = "You can send an invite link to the group in a private message instead.";
"lng_invite_upgrade_via_channel_about" = "You can send an invite link to the channel in a private message instead.";
"lng_invite_status_disabled" = "available only to Premium users";
"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.";

View File

@ -502,16 +502,24 @@ void ChatParticipants::add(
const auto &data = result.data();
chat->session().api().applyUpdates(data.vupdates());
if (done) done(true);
ChatInviteForbidden(
show,
chat,
CollectForbiddenUsers(&chat->session(), result));
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
ShowAddParticipantsError(show, type, peer, { 1, user });
ShowAddParticipantsError(show, type, peer, user);
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(show, "USER_BOT", peer, users);
ShowAddParticipantsError(
show,
u"USER_BOT"_q,
peer,
{ .users = users });
return;
}
auto list = QVector<MTPInputUser>();
@ -531,7 +539,9 @@ void ChatParticipants::add(
channel,
CollectForbiddenUsers(&channel->session(), result));
}).fail([=](const MTP::Error &error) {
ShowAddParticipantsError(show, error.type(), peer, users);
ShowAddParticipantsError(show, error.type(), peer, {
.users = users,
});
if (callback) callback(false);
}).afterDelay(kSmallDelayMs).send();
};

View File

@ -3862,7 +3862,7 @@ void ApiWrap::sendBotStart(
Expects(bot->isBot());
if (chat && chat->isChannel() && !chat->isMegagroup()) {
ShowAddParticipantsError(show, "USER_BOT", chat, { 1, bot });
ShowAddParticipantsError(show, "USER_BOT", chat, bot);
return;
}
@ -3894,7 +3894,7 @@ void ApiWrap::sendBotStart(
}).fail([=](const MTP::Error &error) {
if (chat) {
const auto type = error.type();
ShowAddParticipantsError(show, type, chat, { 1, bot });
ShowAddParticipantsError(show, type, chat, bot);
}
}).send();
}

View File

@ -172,16 +172,28 @@ void ShowAddParticipantsError(
std::shared_ptr<Ui::Show> show,
const QString &error,
not_null<PeerData*> chat,
const std::vector<not_null<UserData*>> &users) {
not_null<UserData*> user) {
ShowAddParticipantsError(
std::move(show),
error,
chat,
{ .users = { 1, user } });
}
void ShowAddParticipantsError(
std::shared_ptr<Ui::Show> show,
const QString &error,
not_null<PeerData*> chat,
const ForbiddenInvites &forbidden) {
if (error == u"USER_BOT"_q) {
const auto channel = chat->asChannel();
if ((users.size() == 1)
&& users.front()->isBot()
if ((forbidden.users.size() == 1)
&& forbidden.users.front()->isBot()
&& channel
&& !channel->isMegagroup()
&& channel->canAddAdmins()) {
const auto makeAdmin = [=] {
const auto user = users.front();
const auto user = forbidden.users.front();
const auto weak = std::make_shared<QPointer<EditAdminBox>>();
const auto close = [=](auto&&...) {
if (*weak) {
@ -213,7 +225,7 @@ void ShowAddParticipantsError(
return;
}
}
const auto hasBot = ranges::any_of(users, &UserData::isBot);
const auto hasBot = ranges::any_of(forbidden.users, &UserData::isBot);
if (error == u"PEER_FLOOD"_q) {
const auto type = (chat->isChat() || chat->isMegagroup())
? PeerFloodType::InviteGroup
@ -222,7 +234,7 @@ void ShowAddParticipantsError(
Ui::show(Ui::MakeInformBox(text), Ui::LayerOption::KeepOther);
return;
} else if (error == u"USER_PRIVACY_RESTRICTED"_q) {
ChatInviteForbidden(show, chat, users);
ChatInviteForbidden(show, chat, forbidden);
return;
}
const auto text = [&] {

View File

@ -43,6 +43,8 @@ enum class PeerFloodType {
InviteChannel,
};
struct ForbiddenInvites;
[[nodiscard]] TextWithEntities PeerFloodErrorText(
not_null<Main::Session*> session,
PeerFloodType type);
@ -50,7 +52,12 @@ void ShowAddParticipantsError(
std::shared_ptr<Ui::Show> show,
const QString &error,
not_null<PeerData*> chat,
const std::vector<not_null<UserData*>> &users);
const ForbiddenInvites &forbidden);
void ShowAddParticipantsError(
std::shared_ptr<Ui::Show> show,
const QString &error,
not_null<PeerData*> chat,
not_null<UserData*> user);
class AddContactBox : public Ui::BoxContent {
public:

View File

@ -1028,3 +1028,19 @@ birthdayLabel: FlatLabel(infoLabel) {
margin: margins(0px, 0px, 0px, 0px);
}
birthdayTodayIcon: icon {{ "menu/gift_premium", windowActiveTextFg }};
inviteForbiddenUserpicsPadding: margins(10px, 10px, 10px, 0px);
inviteForbiddenInfo: FlatLabel(defaultFlatLabel) {
minWidth: 240px;
align: align(top);
}
inviteForbiddenInfoPadding: margins(32px, 10px, 32px, 4px);
inviteForbiddenSubscribePadding: margins(16px, 12px, 16px, 16px);
inviteForbiddenOrLabelPadding: margins(32px, 0px, 32px, 0px);
inviteForbiddenTitle: FlatLabel(boxTitle) {
minWidth: 120px;
align: align(top);
}
inviteForbiddenTitlePadding: margins(32px, 4px, 32px, 0px);
inviteForbiddenLockBg: dialogsUnreadBgMuted;
inviteForbiddenLockIcon: icon {{ "emoji/premium_lock", dialogsUnreadFg }};

View File

@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#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"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/max_invite_box.h"
#include "lang/lang_keys.h"
#include "data/data_channel.h"
@ -20,32 +20,56 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_folder.h"
#include "data/data_changes.h"
#include "data/data_peer_values.h"
#include "history/history.h"
#include "dialogs/dialogs_indexed_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/show_or_premium_box.h"
#include "ui/effects/premium_graphics.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/widgets/gradient_round_button.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/painter.h"
#include "base/unixtime.h"
#include "main/main_session.h"
#include "mtproto/mtproto_config.h"
#include "settings/settings_premium.h"
#include "window/window_session_controller.h"
#include "info/profile/info_profile_icon.h"
#include "apiwrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
namespace {
constexpr auto kParticipantsFirstPageCount = 16;
constexpr auto kParticipantsPerPage = 200;
constexpr auto kUserpicsLimit = 3;
class ForbiddenRow final : public PeerListRow {
public:
ForbiddenRow(not_null<PeerData*> peer, bool locked);
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
private:
const bool _locked = false;
QImage _disabledFrame;
InMemoryKey _userpicKey;
int _paletteVersion = 0;
};
class InviteForbiddenController final : public PeerListController {
public:
InviteForbiddenController(
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> users);
ForbiddenInvites forbidden);
Main::Session &session() const override;
void prepare() override;
@ -67,9 +91,14 @@ private:
void appendRow(not_null<UserData*> user);
[[nodiscard]] std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const;
[[nodiscard]] bool canInvite(not_null<PeerData*> peer) const;
void setSimpleCover();
void setComplexCover();
const not_null<PeerData*> _peer;
const std::vector<not_null<UserData*>> _users;
const ForbiddenInvites _forbidden;
const std::vector<not_null<UserData*>> &_users;
const bool _can = false;
rpl::variable<int> _selected;
bool _sending = false;
@ -91,22 +120,226 @@ base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
return {};
}
void FillUpgradeToPremiumCover(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
const ForbiddenInvites &forbidden) {
const auto noneCanSend = (forbidden.premiumAllowsWrite.size()
== forbidden.users.size());
const auto &userpicUsers = (forbidden.premiumAllowsInvite.empty()
|| noneCanSend)
? forbidden.premiumAllowsWrite
: forbidden.premiumAllowsInvite;
Assert(!userpicUsers.empty());
auto userpicPeers = userpicUsers | ranges::views::transform([](auto u) {
return not_null<PeerData*>(u);
}) | ranges::to_vector;
container->add(object_ptr<Ui::PaddingWrap<>>(
container,
CreateUserpicsWithMoreBadge(
container,
rpl::single(std::move(userpicPeers)),
kUserpicsLimit),
st::inviteForbiddenUserpicsPadding)
)->entity()->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto users = int(userpicUsers.size());
const auto names = std::min(users, kUserpicsLimit);
const auto remaining = std::max(users - kUserpicsLimit, 0);
auto text = TextWithEntities();
for (auto i = 0; i != names; ++i) {
const auto name = userpicUsers[i]->shortName();
if (text.empty()) {
text = Ui::Text::Bold(name);
} else if (i == names - 1 && !remaining) {
text = tr::lng_invite_upgrade_users_few(
tr::now,
lt_users,
text,
lt_last,
Ui::Text::Bold(name),
Ui::Text::RichLangValue);
} else {
text.append(", ").append(Ui::Text::Bold(name));
}
}
if (remaining > 0) {
text = tr::lng_invite_upgrade_users_many(
tr::now,
lt_count,
remaining,
lt_users,
text,
Ui::Text::RichLangValue);
}
const auto inviteOnly = !forbidden.premiumAllowsInvite.empty()
&& (forbidden.premiumAllowsWrite.size() != forbidden.users.size());
text = (peer->isBroadcast()
? (inviteOnly
? tr::lng_invite_upgrade_channel_invite
: tr::lng_invite_upgrade_channel_write)
: (inviteOnly
? tr::lng_invite_upgrade_group_invite
: tr::lng_invite_upgrade_group_write))(
tr::now,
lt_count,
int(userpicUsers.size()),
lt_users,
text,
Ui::Text::RichLangValue);
container->add(
object_ptr<Ui::FlatLabel>(
container,
rpl::single(text),
st::inviteForbiddenInfo),
st::inviteForbiddenInfoPadding);
}
void SimpleForbiddenBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
const ForbiddenInvites &forbidden) {
box->setTitle(tr::lng_invite_upgrade_title());
box->setWidth(st::boxWideWidth);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
auto sshow = Main::MakeSessionShow(box->uiShow(), &peer->session());
const auto container = box->verticalLayout();
FillUpgradeToPremiumCover(container, sshow, peer, forbidden);
const auto &stButton = st::premiumGiftBox;
box->setStyle(stButton);
auto raw = Settings::CreateSubscribeButton(
sshow,
ChatHelpers::ResolveWindowDefault(),
{
.parent = container,
.computeRef = [] { return u"invite_privacy"_q; },
.text = tr::lng_messages_privacy_premium_button(),
.showPromo = true,
});
auto button = object_ptr<Ui::GradientButton>::fromRaw(raw);
button->resizeToWidth(st::boxWideWidth
- stButton.buttonPadding.left()
- stButton.buttonPadding.right());
box->setShowFinishedCallback([raw = button.data()] {
raw->startGlareAnimation();
});
box->addButton(std::move(button));
Data::AmPremiumValue(
&peer->session()
) | rpl::skip(1) | rpl::start_with_next([=] {
box->closeBox();
}, box->lifetime());
}
InviteForbiddenController::InviteForbiddenController(
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> users)
ForbiddenInvites forbidden)
: _peer(peer)
, _users(std::move(users))
, _forbidden(std::move(forbidden))
, _users(_forbidden.users)
, _can(peer->isChat()
? peer->asChat()->canHaveInviteLink()
: peer->asChannel()->canHaveInviteLink())
, _selected(_can ? int(_users.size()) : 0) {
, _selected(_can
? (int(_users.size()) - int(_forbidden.premiumAllowsWrite.size()))
: 0) {
}
Main::Session &InviteForbiddenController::session() const {
return _peer->session();
}
void InviteForbiddenController::prepare() {
ForbiddenRow::ForbiddenRow(not_null<PeerData*> peer, bool locked)
: PeerListRow(peer)
, _locked(locked) {
if (_locked) {
setCustomStatus(tr::lng_invite_status_disabled(tr::now));
}
}
PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
bool forceRound) {
const auto peer = this->peer();
const auto saved = peer->isSelf();
const auto replies = peer->isRepliesChat();
auto userpic = (saved || replies)
? Ui::PeerUserpicView()
: ensureUserpicView();
auto paint = [=](
Painter &p,
int x,
int y,
int outerWidth,
int size) mutable {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
};
if (!_locked) {
return paint;
}
return [=](
Painter &p,
int x,
int y,
int outerWidth,
int size) mutable {
const auto wide = size + style::ConvertScale(3);
const auto full = QSize(wide, wide) * style::DevicePixelRatio();
auto repaint = false;
if (_disabledFrame.size() != full) {
repaint = true;
_disabledFrame = QImage(
full,
QImage::Format_ARGB32_Premultiplied);
_disabledFrame.setDevicePixelRatio(style::DevicePixelRatio());
} else {
repaint = (_paletteVersion != style::PaletteVersion())
|| (!saved
&& !replies
&& (_userpicKey != peer->userpicUniqueKey(userpic)));
}
if (repaint) {
_paletteVersion = style::PaletteVersion();
_userpicKey = peer->userpicUniqueKey(userpic);
_disabledFrame.fill(Qt::transparent);
auto p = Painter(&_disabledFrame);
paint(p, 0, 0, wide, size);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::boxBg);
p.setPen(Qt::NoPen);
const auto lock = st::inviteForbiddenLockIcon.size();
const auto stroke = style::ConvertScale(2);
const auto inner = QRect(
size + (stroke / 2) - lock.width(),
size + (stroke / 2) - lock.height(),
lock.width(),
lock.height());
const auto half = stroke / 2.;
const auto rect = QRectF(inner).marginsAdded(
{ half, half, half, half });
auto pen = st::boxBg->p;
pen.setWidthF(stroke);
p.setPen(pen);
p.setBrush(st::inviteForbiddenLockBg);
p.drawEllipse(rect);
st::inviteForbiddenLockIcon.paintInCenter(p, inner);
}
p.drawImage(x, y, _disabledFrame);
};
}
void InviteForbiddenController::setSimpleCover() {
delegate()->peerListSetTitle(
_can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant());
const auto broadcast = _peer->isBroadcast();
const auto count = int(_users.size());
const auto phraseCounted = !_can
@ -135,8 +368,78 @@ void InviteForbiddenController::prepare() {
std::move(text),
st::requestPeerRestriction),
st::boxRowPadding));
delegate()->peerListSetTitle(
_can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant());
}
void InviteForbiddenController::setComplexCover() {
delegate()->peerListSetTitle(tr::lng_invite_upgrade_title());
auto cover = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = cover.data();
const auto show = delegate()->peerListUiShow();
FillUpgradeToPremiumCover(container, show, _peer, _forbidden);
container->add(
object_ptr<Ui::GradientButton>::fromRaw(
Settings::CreateSubscribeButton(
show,
ChatHelpers::ResolveWindowDefault(),
{
.parent = container,
.computeRef = [] { return u"invite_privacy"_q; },
.text = tr::lng_messages_privacy_premium_button(),
})),
st::inviteForbiddenSubscribePadding);
if (_forbidden.users.size() > _forbidden.premiumAllowsWrite.size()) {
if (_can) {
container->add(
MakeShowOrLabel(container, tr::lng_invite_upgrade_or()),
st::inviteForbiddenOrLabelPadding);
}
container->add(
object_ptr<Ui::FlatLabel>(
container,
(_can
? tr::lng_invite_upgrade_via_title()
: tr::lng_via_link_cant()),
st::inviteForbiddenTitle),
st::inviteForbiddenTitlePadding);
const auto about = _can
? (_peer->isBroadcast()
? tr::lng_invite_upgrade_via_channel_about
: tr::lng_invite_upgrade_via_group_about)(
tr::now,
Ui::Text::WithEntities)
: (_forbidden.users.size() == 1
? tr::lng_via_link_cant_one(
tr::now,
lt_user,
TextWithEntities{ _forbidden.users.front()->shortName() },
Ui::Text::RichLangValue)
: tr::lng_via_link_cant_many(
tr::now,
lt_count,
int(_forbidden.users.size()),
Ui::Text::RichLangValue));
container->add(
object_ptr<Ui::FlatLabel>(
container,
rpl::single(about),
st::inviteForbiddenInfo),
st::inviteForbiddenInfoPadding);
}
delegate()->peerListSetAboveWidget(std::move(cover));
}
void InviteForbiddenController::prepare() {
if (session().premium()
|| (_forbidden.premiumAllowsInvite.empty()
&& _forbidden.premiumAllowsWrite.empty())) {
setSimpleCover();
} else {
setComplexCover();
}
for (const auto &user : _users) {
appendRow(user);
@ -144,8 +447,16 @@ void InviteForbiddenController::prepare() {
delegate()->peerListRefreshRows();
}
bool InviteForbiddenController::canInvite(not_null<PeerData*> peer) const {
const auto user = peer->asUser();
Assert(user != nullptr);
return _can
&& !ranges::contains(_forbidden.premiumAllowsWrite, not_null(user));
}
void InviteForbiddenController::rowClicked(not_null<PeerListRow*> row) {
if (!_can) {
if (!canInvite(row->peer())) {
return;
}
const auto checked = row->checked();
@ -158,7 +469,7 @@ void InviteForbiddenController::appendRow(not_null<UserData*> user) {
auto row = createRow(user);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (_can) {
if (canInvite(user)) {
delegate()->peerListSetRowChecked(raw, true);
}
}
@ -228,7 +539,8 @@ void InviteForbiddenController::send(
std::unique_ptr<PeerListRow> InviteForbiddenController::createRow(
not_null<UserData*> user) const {
return std::make_unique<PeerListRow>(user);
const auto locked = _can && !canInvite(user);
return std::make_unique<ForbiddenRow>(user, locked);
}
} // namespace
@ -559,17 +871,23 @@ void AddParticipantsBoxController::Start(
Start(navigation, channel, {}, true);
}
std::vector<not_null<UserData*>> CollectForbiddenUsers(
ForbiddenInvites CollectForbiddenUsers(
not_null<Main::Session*> session,
const MTPmessages_InvitedUsers &result) {
const auto &data = result.data();
const auto owner = &session->data();
auto forbidden = std::vector<not_null<UserData*>>();
auto forbidden = ForbiddenInvites();
for (const auto &missing : data.vmissing_invitees().v) {
const auto user = owner->userLoaded(missing.data().vuser_id());
const auto &data = missing.data();
const auto user = owner->userLoaded(data.vuser_id());
if (user) {
// #TODO invites
forbidden.push_back(user);
forbidden.users.push_back(user);
if (data.is_premium_would_allow_invite()) {
forbidden.premiumAllowsInvite.push_back(user);
}
if (data.is_premium_required_for_pm()) {
forbidden.premiumAllowsWrite.push_back(user);
}
}
}
return forbidden;
@ -578,9 +896,14 @@ std::vector<not_null<UserData*>> CollectForbiddenUsers(
bool ChatInviteForbidden(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> forbidden) {
ForbiddenInvites forbidden) {
if (forbidden.empty() || !show || !show->valid()) {
return false;
} else if (forbidden.users.size() <= kUserpicsLimit
&& (forbidden.premiumAllowsWrite.size()
== forbidden.users.size())) {
show->show(Box(SimpleForbiddenBox, peer, forbidden));
return true;
}
auto controller = std::make_unique<InviteForbiddenController>(
peer,
@ -612,6 +935,12 @@ bool ChatInviteForbidden(
box->closeBox();
});
}, box->lifetime());
Data::AmPremiumValue(
&peer->session()
) | rpl::skip(1) | rpl::start_with_next([=] {
box->closeBox();
}, box->lifetime());
};
show->showBox(
Box<PeerListBox>(std::move(controller), std::move(initBox)));

View File

@ -73,13 +73,22 @@ private:
};
[[nodiscard]] std::vector<not_null<UserData*>> CollectForbiddenUsers(
struct ForbiddenInvites {
std::vector<not_null<UserData*>> users;
std::vector<not_null<UserData*>> premiumAllowsInvite;
std::vector<not_null<UserData*>> premiumAllowsWrite;
[[nodiscard]] bool empty() const {
return users.empty();
}
};
[[nodiscard]] ForbiddenInvites CollectForbiddenUsers(
not_null<Main::Session*> session,
const MTPmessages_InvitedUsers &result);
bool ChatInviteForbidden(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
std::vector<not_null<UserData*>> forbidden);
ForbiddenInvites forbidden);
// Adding an admin, banned or restricted user from channel members
// with search + contacts search + global search.

View File

@ -87,8 +87,12 @@ void AddChatParticipant(
if (onDone) {
onDone();
}
ChatInviteForbidden(
show,
chat,
CollectForbiddenUsers(&chat->session(), result));
}).fail([=](const MTP::Error &error) {
ShowAddParticipantsError(show, error.type(), chat, { 1, user });
ShowAddParticipantsError(show, error.type(), chat, user);
if (onFail) {
onFail();
}
@ -155,7 +159,7 @@ void SaveChannelAdmin(
onDone();
}
}).fail([=](const MTP::Error &error) {
ShowAddParticipantsError(show, error.type(), channel, { 1, user });
ShowAddParticipantsError(show, error.type(), channel, user);
if (onFail) {
onFail();
}

View File

@ -671,3 +671,143 @@ object_ptr<Ui::RpWidget> CreateBoostReplaceUserpics(
}, overlay->lifetime());
return result;
}
object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> peers,
int limit) {
struct State {
std::vector<not_null<PeerData*>> from;
std::vector<std::unique_ptr<Ui::UserpicButton>> buttons;
QImage layer;
QImage badge;
rpl::variable<int> count = 0;
bool painting = false;
};
const auto full = st::boostReplaceUserpic.size.height()
+ st::boostReplaceIconAdd.y()
+ st::lineWidth;
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
const auto raw = result.data();
const auto &st = st::boostReplaceUserpic;
const auto overlay = CreateChild<Ui::RpWidget>(raw);
const auto state = raw->lifetime().make_state<State>();
std::move(
peers
) | rpl::start_with_next([=](
const std::vector<not_null<PeerData*>> &list) {
const auto &st = st::boostReplaceUserpic;
auto was = base::take(state->from);
auto buttons = base::take(state->buttons);
state->from.reserve(list.size());
state->buttons.reserve(list.size());
for (const auto &peer : list | ranges::views::take(limit)) {
state->from.push_back(peer);
const auto i = ranges::find(was, peer);
if (i != end(was)) {
const auto index = int(i - begin(was));
Assert(buttons[index] != nullptr);
state->buttons.push_back(std::move(buttons[index]));
} else {
state->buttons.push_back(
std::make_unique<Ui::UserpicButton>(raw, peer, st));
const auto raw = state->buttons.back().get();
base::install_event_filter(raw, [=](not_null<QEvent*> e) {
return (e->type() == QEvent::Paint && !state->painting)
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
});
}
}
state->count.force_assign(int(list.size()));
overlay->update();
}, raw->lifetime());
rpl::combine(
raw->widthValue(),
state->count.value()
) | rpl::start_with_next([=](int width, int count) {
const auto &st = st::boostReplaceUserpic;
const auto single = st.size.width();
const auto left = width - single;
const auto used = std::min(count, int(state->buttons.size()));
const auto shift = std::min(
st::boostReplaceUserpicsShift,
(used > 1 ? (left / (used - 1)) : width));
const auto total = used ? (single + (used - 1) * shift) : 0;
auto x = (width - total) / 2;
for (const auto &single : state->buttons) {
single->moveToLeft(x, 0);
x += shift;
}
overlay->setGeometry(QRect(0, 0, width, raw->height()));
}, raw->lifetime());
overlay->paintRequest(
) | rpl::filter([=] {
return !state->buttons.empty();
}) | rpl::start_with_next([=] {
const auto outerw = overlay->width();
const auto ratio = style::DevicePixelRatio();
if (state->layer.size() != QSize(outerw, full) * ratio) {
state->layer = QImage(
QSize(outerw, full) * ratio,
QImage::Format_ARGB32_Premultiplied);
state->layer.setDevicePixelRatio(ratio);
}
state->layer.fill(Qt::transparent);
auto q = QPainter(&state->layer);
auto hq = PainterHighQualityEnabler(q);
const auto stroke = st::boostReplaceIconOutline;
const auto half = stroke / 2.;
auto pen = st::windowBg->p;
pen.setWidthF(stroke * 2.);
state->painting = true;
for (const auto &button : state->buttons) {
q.setPen(pen);
q.setBrush(Qt::NoBrush);
q.drawEllipse(button->geometry());
const auto position = button->pos();
button->render(&q, position, QRegion(), QWidget::DrawChildren);
}
state->painting = false;
const auto last = state->buttons.back().get();
const auto add = st::boostReplaceIconAdd;
const auto skip = st::boostReplaceIconSkip;
const auto w = st::boostReplaceIcon.width() + 2 * skip;
const auto h = st::boostReplaceIcon.height() + 2 * skip;
const auto x = last->x() + last->width() - w + add.x();
const auto y = last->y() + last->height() - h + add.y();
const auto text = (state->count.current() > limit)
? ('+' + QString::number(state->count.current() - limit))
: QString();
if (!text.isEmpty()) {
const auto &font = st::semiboldFont;
const auto width = font->width(text);
const auto padded = std::max(w, width + 2 * font->spacew);
const auto rect = QRect(x - (padded - w) / 2, y, padded, h);
auto brush = QLinearGradient(rect.bottomRight(), rect.topLeft());
brush.setStops(Ui::Premium::ButtonGradientStops());
q.setBrush(brush);
pen.setWidthF(stroke);
q.setPen(pen);
const auto rectf = QRectF(rect);
const auto radius = std::min(rect.width(), rect.height()) / 2.;
q.drawRoundedRect(
rectf.marginsAdded(QMarginsF{ half, half, half, half }),
radius,
radius);
q.setFont(font);
q.setPen(st::premiumButtonFg);
q.drawText(rect, Qt::AlignCenter, text);
}
q.end();
auto p = QPainter(overlay);
p.drawImage(0, 0, state->layer);
}, overlay->lifetime());
return result;
}

View File

@ -57,3 +57,8 @@ object_ptr<Ui::BoxContent> ReassignBoostsBox(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> from,
not_null<PeerData*> to);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> peers,
int limit);

View File

@ -1464,9 +1464,26 @@ not_null<Ui::GradientButton*> CreateSubscribeButton(
SubscribeButtonArgs &&args) {
Expects(args.show || args.controller);
if (!args.show && args.controller) {
args.show = args.controller->uiShow();
}
auto show = args.show ? std::move(args.show) : args.controller->uiShow();
auto resolve = [show](
not_null<Main::Session*> session,
ChatHelpers::WindowUsage usage) {
Expects(session == &show->session());
return show->resolveWindow(usage);
};
return CreateSubscribeButton(
std::move(show),
std::move(resolve),
std::move(args));
}
not_null<Ui::GradientButton*> CreateSubscribeButton(
std::shared_ptr<::Main::SessionShow> show,
Fn<Window::SessionController*(
not_null<::Main::Session*>,
ChatHelpers::WindowUsage)> resolveWindow,
SubscribeButtonArgs &&args) {
const auto result = Ui::CreateChild<Ui::GradientButton>(
args.parent.get(),
args.gradientStops
@ -1474,13 +1491,19 @@ not_null<Ui::GradientButton*> CreateSubscribeButton(
: Ui::Premium::ButtonGradientStops());
result->setClickedCallback([
show = args.show,
show,
resolveWindow,
promo = args.showPromo,
computeRef = args.computeRef,
computeBotUrl = args.computeBotUrl] {
const auto window = show->resolveWindow(
const auto window = resolveWindow(
&show->session(),
ChatHelpers::WindowUsage::PremiumPromo);
if (!window) {
return;
} else if (promo) {
Settings::ShowPremium(window, computeRef());
return;
}
const auto url = computeBotUrl ? computeBotUrl() : QString();
if (!url.isEmpty()) {
@ -1503,7 +1526,7 @@ not_null<Ui::GradientButton*> CreateSubscribeButton(
const auto &st = st::premiumPreviewBox.button;
result->resize(args.parent->width(), st.height);
const auto premium = &args.show->session().api().premium();
const auto premium = &show->session().api().premium();
premium->reload();
const auto computeCost = [=] {
const auto amount = premium->monthlyAmount();

View File

@ -79,6 +79,7 @@ struct SubscribeButtonArgs final {
std::optional<QGradientStops> gradientStops;
Fn<QString()> computeBotUrl; // nullable
std::shared_ptr<ChatHelpers::Show> show;
bool showPromo = false;
};
@ -91,6 +92,13 @@ struct SubscribeButtonArgs final {
[[nodiscard]] not_null<Ui::GradientButton*> CreateSubscribeButton(
SubscribeButtonArgs &&args);
[[nodiscard]] not_null<Ui::GradientButton*> CreateSubscribeButton(
std::shared_ptr<::Main::SessionShow> show,
Fn<Window::SessionController*(
not_null<::Main::Session*>,
ChatHelpers::WindowUsage)> resolveWindow,
SubscribeButtonArgs &&args);
[[nodiscard]] std::vector<PremiumFeature> PremiumFeaturesOrder(
not_null<::Main::Session*> session);

View File

@ -52,7 +52,9 @@ constexpr auto kShowOrLineOpacity = 0.3;
return result;
}
[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(
} // namespace
object_ptr<RpWidget> MakeShowOrLabel(
not_null<RpWidget*> parent,
rpl::producer<QString> text) {
auto result = object_ptr<FlatLabel>(
@ -80,8 +82,6 @@ constexpr auto kShowOrLineOpacity = 0.3;
return result;
}
} // namespace
void ShowOrPremiumBox(
not_null<GenericBox*> box,
ShowOrPremium type,

View File

@ -7,8 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
namespace Ui {
class RpWidget;
class GenericBox;
enum class ShowOrPremium : uchar {
@ -22,4 +25,8 @@ void ShowOrPremiumBox(
Fn<void()> justShow,
Fn<void()> toPremium);
[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(
not_null<RpWidget*> parent,
rpl::producer<QString> text);
} // namespace Ui