diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 706824f92f..1fedad6806 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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."; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index 0c2fb2030b..6761d2fff0 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -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(); @@ -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(); }; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 09a307700c..e53e7a5d79 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -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(); } diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 56e4f68a9c..ba35e8d981 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -172,16 +172,28 @@ void ShowAddParticipantsError( std::shared_ptr show, const QString &error, not_null chat, - const std::vector> &users) { + not_null user) { + ShowAddParticipantsError( + std::move(show), + error, + chat, + { .users = { 1, user } }); +} + +void ShowAddParticipantsError( + std::shared_ptr show, + const QString &error, + not_null 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>(); 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 = [&] { diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index ed6adc6555..9bc7de6520 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -43,6 +43,8 @@ enum class PeerFloodType { InviteChannel, }; +struct ForbiddenInvites; + [[nodiscard]] TextWithEntities PeerFloodErrorText( not_null session, PeerFloodType type); @@ -50,7 +52,12 @@ void ShowAddParticipantsError( std::shared_ptr show, const QString &error, not_null chat, - const std::vector> &users); + const ForbiddenInvites &forbidden); +void ShowAddParticipantsError( + std::shared_ptr show, + const QString &error, + not_null chat, + not_null user); class AddContactBox : public Ui::BoxContent { public: diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index c7a23f366d..2ae5bba8d3 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -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 }}; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 1815c0db48..04ef172767 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -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 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 peer, - std::vector> users); + ForbiddenInvites forbidden); Main::Session &session() const override; void prepare() override; @@ -67,9 +91,14 @@ private: void appendRow(not_null user); [[nodiscard]] std::unique_ptr createRow( not_null user) const; + [[nodiscard]] bool canInvite(not_null peer) const; + + void setSimpleCover(); + void setComplexCover(); const not_null _peer; - const std::vector> _users; + const ForbiddenInvites _forbidden; + const std::vector> &_users; const bool _can = false; rpl::variable _selected; bool _sending = false; @@ -91,22 +120,226 @@ base::flat_set> GetAlreadyInFromPeer(PeerData *peer) { return {}; } +void FillUpgradeToPremiumCover( + not_null container, + std::shared_ptr show, + not_null 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(u); + }) | ranges::to_vector; + container->add(object_ptr>( + 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( + container, + rpl::single(text), + st::inviteForbiddenInfo), + st::inviteForbiddenInfoPadding); +} + +void SimpleForbiddenBox( + not_null box, + not_null 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::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 peer, - std::vector> 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 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((QWidget*)nullptr); + const auto container = cover.data(); + const auto show = delegate()->peerListUiShow(); + FillUpgradeToPremiumCover(container, show, _peer, _forbidden); + + container->add( + object_ptr::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( + 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( + 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 peer) const { + const auto user = peer->asUser(); + Assert(user != nullptr); + + return _can + && !ranges::contains(_forbidden.premiumAllowsWrite, not_null(user)); +} + void InviteForbiddenController::rowClicked(not_null row) { - if (!_can) { + if (!canInvite(row->peer())) { return; } const auto checked = row->checked(); @@ -158,7 +469,7 @@ void InviteForbiddenController::appendRow(not_null 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 InviteForbiddenController::createRow( not_null user) const { - return std::make_unique(user); + const auto locked = _can && !canInvite(user); + return std::make_unique(user, locked); } } // namespace @@ -559,17 +871,23 @@ void AddParticipantsBoxController::Start( Start(navigation, channel, {}, true); } -std::vector> CollectForbiddenUsers( +ForbiddenInvites CollectForbiddenUsers( not_null session, const MTPmessages_InvitedUsers &result) { const auto &data = result.data(); const auto owner = &session->data(); - auto forbidden = std::vector>(); + 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> CollectForbiddenUsers( bool ChatInviteForbidden( std::shared_ptr show, not_null peer, - std::vector> 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( 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(std::move(controller), std::move(initBox))); diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.h b/Telegram/SourceFiles/boxes/peers/add_participants_box.h index 8ac842cf10..24d5e1031d 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.h @@ -73,13 +73,22 @@ private: }; -[[nodiscard]] std::vector> CollectForbiddenUsers( +struct ForbiddenInvites { + std::vector> users; + std::vector> premiumAllowsInvite; + std::vector> premiumAllowsWrite; + + [[nodiscard]] bool empty() const { + return users.empty(); + } +}; +[[nodiscard]] ForbiddenInvites CollectForbiddenUsers( not_null session, const MTPmessages_InvitedUsers &result); bool ChatInviteForbidden( std::shared_ptr show, not_null peer, - std::vector> forbidden); + ForbiddenInvites forbidden); // Adding an admin, banned or restricted user from channel members // with search + contacts search + global search. diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index e627306a63..7254270070 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -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(); } diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index f9a4c5d8f5..a13295d00b 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -671,3 +671,143 @@ object_ptr CreateBoostReplaceUserpics( }, overlay->lifetime()); return result; } + +object_ptr CreateUserpicsWithMoreBadge( + not_null parent, + rpl::producer>> peers, + int limit) { + struct State { + std::vector> from; + std::vector> buttons; + QImage layer; + QImage badge; + rpl::variable count = 0; + bool painting = false; + }; + const auto full = st::boostReplaceUserpic.size.height() + + st::boostReplaceIconAdd.y() + + st::lineWidth; + auto result = object_ptr(parent, full); + const auto raw = result.data(); + const auto &st = st::boostReplaceUserpic; + const auto overlay = CreateChild(raw); + + const auto state = raw->lifetime().make_state(); + std::move( + peers + ) | rpl::start_with_next([=]( + const std::vector> &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(raw, peer, st)); + const auto raw = state->buttons.back().get(); + base::install_event_filter(raw, [=](not_null 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; +} diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h index 0f7d011061..c84b0d306c 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -57,3 +57,8 @@ object_ptr ReassignBoostsBox( not_null parent, rpl::producer>> from, not_null to); + +[[nodiscard]] object_ptr CreateUserpicsWithMoreBadge( + not_null parent, + rpl::producer>> peers, + int limit); diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 2addb7fe40..d1962e4071 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1464,9 +1464,26 @@ not_null 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 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 CreateSubscribeButton( + std::shared_ptr<::Main::SessionShow> show, + Fn, + ChatHelpers::WindowUsage)> resolveWindow, + SubscribeButtonArgs &&args) { const auto result = Ui::CreateChild( args.parent.get(), args.gradientStops @@ -1474,13 +1491,19 @@ not_null 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 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(); diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 6dd5718124..65d7d18dad 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -79,6 +79,7 @@ struct SubscribeButtonArgs final { std::optional gradientStops; Fn computeBotUrl; // nullable std::shared_ptr show; + bool showPromo = false; }; @@ -91,6 +92,13 @@ struct SubscribeButtonArgs final { [[nodiscard]] not_null CreateSubscribeButton( SubscribeButtonArgs &&args); +[[nodiscard]] not_null CreateSubscribeButton( + std::shared_ptr<::Main::SessionShow> show, + Fn, + ChatHelpers::WindowUsage)> resolveWindow, + SubscribeButtonArgs &&args); + [[nodiscard]] std::vector PremiumFeaturesOrder( not_null<::Main::Session*> session); diff --git a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp index 5f00e1b7e2..485af50a8a 100644 --- a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp @@ -52,7 +52,9 @@ constexpr auto kShowOrLineOpacity = 0.3; return result; } -[[nodiscard]] object_ptr MakeShowOrLabel( +} // namespace + +object_ptr MakeShowOrLabel( not_null parent, rpl::producer text) { auto result = object_ptr( @@ -80,8 +82,6 @@ constexpr auto kShowOrLineOpacity = 0.3; return result; } -} // namespace - void ShowOrPremiumBox( not_null box, ShowOrPremium type, diff --git a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h index 4c0a101044..25bcfc9f90 100644 --- a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h +++ b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.h @@ -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 justShow, Fn toPremium); +[[nodiscard]] object_ptr MakeShowOrLabel( + not_null parent, + rpl::producer text); + } // namespace Ui