/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_chat_invite.h" #include "apiwrap.h" #include "api/api_credits.h" #include "boxes/premium_limits_box.h" #include "core/application.h" #include "data/components/credits.h" #include "data/data_channel.h" #include "data/data_file_origin.h" #include "data/data_forum.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" #include "info/profile/info_profile_badge.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_credits_graphics.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/empty_userpic.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_premium.h" namespace Api { namespace { struct InviteParticipant { not_null user; Ui::PeerUserpicView userpic; }; struct ChatInvite { QString title; QString about; PhotoData *photo = nullptr; int participantsCount = 0; std::vector participants; bool isPublic = false; bool isChannel = false; bool isMegagroup = false; bool isBroadcast = false; bool isRequestNeeded = false; bool isFake = false; bool isScam = false; bool isVerified = false; }; [[nodiscard]] ChatInvite ParseInvite( not_null session, const MTPDchatInvite &data) { auto participants = std::vector(); if (const auto list = data.vparticipants()) { participants.reserve(list->v.size()); for (const auto &participant : list->v) { if (const auto user = session->data().processUser(participant)) { participants.push_back(InviteParticipant{ user }); } } } const auto photo = session->data().processPhoto(data.vphoto()); return { .title = qs(data.vtitle()), .about = data.vabout().value_or_empty(), .photo = (photo->isNull() ? nullptr : photo.get()), .participantsCount = data.vparticipants_count().v, .participants = std::move(participants), .isPublic = data.is_public(), .isChannel = data.is_channel(), .isMegagroup = data.is_megagroup(), .isBroadcast = data.is_broadcast(), .isRequestNeeded = data.is_request_needed(), .isFake = data.is_fake(), .isScam = data.is_scam(), .isVerified = data.is_verified(), }; } [[nodiscard]] Info::Profile::BadgeType BadgeForInvite( const ChatInvite &invite) { using Type = Info::Profile::BadgeType; return invite.isVerified ? Type::Verified : invite.isScam ? Type::Scam : invite.isFake ? Type::Fake : Type::None; } void SubmitChatInvite( base::weak_ptr weak, not_null session, const QString &hash, bool isGroup) { session->api().request(MTPmessages_ImportChatInvite( MTP_string(hash) )).done([=](const MTPUpdates &result) { session->api().applyUpdates(result); const auto strongController = weak.get(); if (!strongController) { return; } strongController->hideLayer(); const auto handleChats = [&](const MTPVector &chats) { if (chats.v.isEmpty()) { return; } const auto peerId = chats.v[0].match([](const MTPDchat &data) { return peerFromChat(data.vid().v); }, [](const MTPDchannel &data) { return peerFromChannel(data.vid().v); }, [](auto&&) { return PeerId(0); }); if (const auto peer = session->data().peerLoaded(peerId)) { // Shows in the primary window anyway. strongController->showPeerHistory( peer, Window::SectionShow::Way::Forward); } }; result.match([&](const MTPDupdates &data) { handleChats(data.vchats()); }, [&](const MTPDupdatesCombined &data) { handleChats(data.vchats()); }, [&](auto &&) { LOG(("API Error: unexpected update cons %1 " "(ApiWrap::importChatInvite)").arg(result.type())); }); }).fail([=](const MTP::Error &error) { const auto &type = error.type(); const auto strongController = weak.get(); if (!strongController) { return; } else if (type == u"CHANNELS_TOO_MUCH"_q) { strongController->show( Box(ChannelsLimitBox, &strongController->session())); return; } strongController->hideLayer(); strongController->showToast([&] { if (type == u"INVITE_REQUEST_SENT"_q) { return isGroup ? tr::lng_group_request_sent(tr::now) : tr::lng_group_request_sent_channel(tr::now); } else if (type == u"USERS_TOO_MUCH"_q) { return tr::lng_group_invite_no_room(tr::now); } else { return tr::lng_group_invite_bad_link(tr::now); } }(), ApiWrap::kJoinErrorDuration); }).send(); } void ConfirmSubscriptionBox( not_null box, not_null session, const QString &hash, const MTPDchatInvite *data) { box->setWidth(st::boxWideWidth); const auto amount = data->vsubscription_pricing()->data().vamount().v; const auto formId = data->vsubscription_form_id()->v; const auto name = qs(data->vtitle()); const auto maybePhoto = session->data().processPhoto(data->vphoto()); const auto photo = maybePhoto->isNull() ? nullptr : maybePhoto.get(); struct State final { std::shared_ptr photoMedia; std::unique_ptr photoEmpty; QImage frame; std::optional api; Ui::RpWidget* saveButton = nullptr; rpl::variable loading; }; const auto state = box->lifetime().make_state(); const auto content = box->verticalLayout(); Ui::AddSkip(content, st::confirmInvitePhotoTop); const auto userpic = content->add( object_ptr(content), style::al_top); const auto photoSize = st::confirmInvitePhotoSize; userpic->resize(Size(photoSize)); userpic->setNaturalWidth(photoSize); const auto creditsIconSize = photoSize / 3; const auto creditsIconCallback = Ui::PaintOutlinedColoredCreditsIconCallback( creditsIconSize, 1.5); state->frame = QImage( Size(photoSize * style::DevicePixelRatio()), QImage::Format_ARGB32_Premultiplied); state->frame.setDevicePixelRatio(style::DevicePixelRatio()); const auto options = Images::Option::RoundCircle; userpic->paintRequest( ) | rpl::on_next([=, small = Data::PhotoSize::Small] { state->frame.fill(Qt::transparent); { auto p = QPainter(&state->frame); if (state->photoMedia) { if (const auto image = state->photoMedia->image(small)) { p.drawPixmap( 0, 0, image->pix(Size(photoSize), { .options = options })); } } else if (state->photoEmpty) { state->photoEmpty->paintCircle( p, 0, 0, userpic->width(), photoSize); } if (creditsIconCallback) { p.translate( photoSize - creditsIconSize, photoSize - creditsIconSize); creditsIconCallback(p); } } auto p = QPainter(userpic); p.drawImage(0, 0, state->frame); }, userpic->lifetime()); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); if (photo) { state->photoMedia = photo->createMediaView(); state->photoMedia->wanted(Data::PhotoSize::Small, Data::FileOrigin()); if (!state->photoMedia->image(Data::PhotoSize::Small)) { session->downloaderTaskFinished( ) | rpl::on_next([=] { userpic->update(); }, userpic->lifetime()); } } else { state->photoEmpty = std::make_unique( Ui::EmptyUserpic::UserpicColor(0), name); } Ui::AddSkip(content); Ui::AddSkip(content); Settings::AddMiniStars( content, Ui::CreateChild(content), photoSize, box->width(), 2.); box->addRow( object_ptr( box, tr::lng_channel_invite_subscription_title(), st::inviteLinkSubscribeBoxTitle), style::al_top); box->addRow( object_ptr( box, tr::lng_channel_invite_subscription_about( lt_channel, rpl::single(tr::bold(name)), lt_price, tr::lng_credits_summary_options_credits( lt_count, rpl::single(amount) | tr::to_count(), tr::bold), tr::marked), st::inviteLinkSubscribeBoxAbout), style::al_top); Ui::AddSkip(content); box->addRow( object_ptr( box, tr::lng_channel_invite_subscription_terms( lt_link, rpl::combine( tr::lng_paid_react_agree_link(), tr::lng_group_invite_subscription_about_url() ) | rpl::map([](const QString &text, const QString &url) { return tr::link(text, url); }), tr::rich), st::inviteLinkSubscribeBoxTerms), style::al_top); { const auto balance = Settings::AddBalanceWidget( content, session, session->credits().balanceValue(), true); session->credits().load(true); rpl::combine( balance->sizeValue(), content->sizeValue() ) | rpl::on_next([=](const QSize &, const QSize &) { balance->moveToRight( st::creditsHistoryRightSkip * 2, st::creditsHistoryRightSkip); balance->update(); }, balance->lifetime()); } const auto sendCredits = [=, weak = base::make_weak(box)] { const auto show = box->uiShow(); const auto buttonWidth = state->saveButton ? state->saveButton->width() : 0; const auto finish = [=] { state->api = std::nullopt; state->loading.force_assign(false); if (const auto strong = weak.get()) { strong->closeBox(); } }; state->api->request( MTPpayments_SendStarsForm( MTP_long(formId), MTP_inputInvoiceChatInviteSubscription(MTP_string(hash))) ).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { session->api().applyUpdates(data.vupdates()); }, [](const MTPDpayments_paymentVerificationNeeded &data) { }); const auto refill = session->data().activeCreditsSubsRebuilder(); const auto strong = weak.get(); if (!strong) { return; } if (!refill) { return finish(); } const auto api = strong->lifetime().make_state( session->user(), true, true); api->requestSubscriptions({}, [=](Data::CreditsStatusSlice d) { refill->fire(std::move(d)); finish(); }); }).fail([=](const MTP::Error &error) { const auto id = error.type(); if (weak) { state->api = std::nullopt; } show->showToast(id); state->loading.force_assign(false); }).send(); if (state->saveButton) { state->saveButton->resizeToWidth(buttonWidth); } }; auto confirmText = tr::lng_channel_invite_subscription_button(); state->saveButton = box->addButton(std::move(confirmText), [=] { if (state->api) { return; } state->api.emplace(&session->mtp()); state->loading.force_assign(true); const auto done = [=](Settings::SmallBalanceResult result) { if (result == Settings::SmallBalanceResult::Success || result == Settings::SmallBalanceResult::Already) { sendCredits(); } else { state->api = std::nullopt; state->loading.force_assign(false); } }; Settings::MaybeRequestBalanceIncrease( Main::MakeSessionShow(box->uiShow(), session), amount, Settings::SmallBalanceSubscription{ .name = name }, done); }); if (const auto saveButton = state->saveButton) { using namespace Info::Statistics; const auto loadingAnimation = InfiniteRadialAnimationWidget( saveButton, saveButton->height() / 2, &st::editStickerSetNameLoading); AddChildToWidgetCenter(saveButton, loadingAnimation); loadingAnimation->showOn( state->loading.value() | rpl::map(rpl::mappers::_1)); } box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void ConfirmInviteBox( not_null box, not_null session, const MTPDchatInvite *invitePtr, ChannelData *invitePeekChannel, Fn submit) { auto invite = ParseInvite(session, *invitePtr); const auto isChannel = invite.isChannel && !invite.isMegagroup; const auto requestApprove = invite.isRequestNeeded; const auto count = invite.participantsCount; struct State { std::shared_ptr photoMedia; std::unique_ptr photoEmpty; std::vector participants; }; const auto state = box->lifetime().make_state(); state->participants = std::move(invite.participants); const auto status = [&] { return invitePeekChannel ? tr::lng_channel_invite_private(tr::now) : (!state->participants.empty() && int(state->participants.size()) < count) ? tr::lng_group_invite_members(tr::now, lt_count, count) : (count > 0 && isChannel) ? tr::lng_chat_status_subscribers( tr::now, lt_count_decimal, count) : (count > 0) ? tr::lng_chat_status_members(tr::now, lt_count_decimal, count) : isChannel ? tr::lng_channel_status(tr::now) : tr::lng_group_status(tr::now); }(); box->setNoContentMargin(true); box->setWidth(st::boxWideWidth); const auto content = box->verticalLayout(); Ui::AddSkip(content, st::confirmInvitePhotoTop); const auto userpic = content->add( object_ptr(content), style::al_top); const auto photoSize = st::confirmInvitePhotoSize; userpic->resize(Size(photoSize)); userpic->setNaturalWidth(photoSize); userpic->paintRequest( ) | rpl::on_next([=, small = Data::PhotoSize::Small] { auto p = QPainter(userpic); if (state->photoMedia) { if (const auto image = state->photoMedia->image(small)) { p.drawPixmap( 0, 0, image->pix( Size(photoSize), { .options = Images::Option::RoundCircle })); } } else if (state->photoEmpty) { state->photoEmpty->paintCircle( p, 0, 0, userpic->width(), photoSize); } }, userpic->lifetime()); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); if (const auto photo = invite.photo) { state->photoMedia = photo->createMediaView(); state->photoMedia->wanted( Data::PhotoSize::Small, Data::FileOrigin()); if (!state->photoMedia->image(Data::PhotoSize::Small)) { session->downloaderTaskFinished( ) | rpl::on_next([=] { userpic->update(); }, userpic->lifetime()); } } else { state->photoEmpty = std::make_unique( Ui::EmptyUserpic::UserpicColor(0), invite.title); } Ui::AddSkip(content); const auto title = box->addRow( object_ptr( box, invite.title, st::confirmInviteTitle), style::al_top); const auto badgeType = BadgeForInvite(invite); if (badgeType != Info::Profile::BadgeType::None) { const auto badgeParent = title->parentWidget(); const auto badge = box->lifetime().make_state( badgeParent, st::infoPeerBadge, session, rpl::single(Info::Profile::Badge::Content{ badgeType }), nullptr, [] { return false; }); title->geometryValue( ) | rpl::on_next([=](const QRect &r) { badge->move(r.x() + r.width(), r.y(), r.y() + r.height()); }, title->lifetime()); } box->addRow( object_ptr( box, status, st::confirmInviteStatus), style::al_top); if (!invite.about.isEmpty()) { box->addRow( object_ptr( box, invite.about, st::confirmInviteAbout), st::confirmInviteAboutPadding, style::al_top); } if (requestApprove) { box->addRow( object_ptr( box, (isChannel ? tr::lng_group_request_about_channel(tr::now) : tr::lng_group_request_about(tr::now)), st::confirmInviteStatus), st::confirmInviteAboutRequestsPadding, style::al_top); } if (!state->participants.empty()) { while (state->participants.size() > 4) { state->participants.pop_back(); } const auto padding = (st::confirmInviteUsersWidth - 4 * st::confirmInviteUserPhotoSize) / 10; const auto userWidth = st::confirmInviteUserPhotoSize + 2 * padding; auto strip = object_ptr(content); const auto rawStrip = strip.data(); rawStrip->resize(st::boxWideWidth, st::confirmInviteUserHeight); rawStrip->setNaturalWidth(st::boxWideWidth); const auto shown = int(state->participants.size()); const auto sumWidth = shown * userWidth; const auto baseLeft = (st::boxWideWidth - sumWidth) / 2; for (auto i = 0; i != shown; ++i) { const auto &participant = state->participants[i]; const auto name = Ui::CreateChild( rawStrip, st::confirmInviteUserName); name->resizeToWidth( st::confirmInviteUserPhotoSize + padding); name->setText(participant.user->firstName.isEmpty() ? participant.user->name() : participant.user->firstName); name->moveToLeft( baseLeft + i * userWidth + (padding / 2), st::confirmInviteUserNameTop - st::confirmInviteUserPhotoTop); } rawStrip->paintRequest( ) | rpl::on_next([=] { auto p = Painter(rawStrip); const auto total = int(state->participants.size()); const auto totalWidth = total * userWidth; auto left = (rawStrip->width() - totalWidth) / 2; for (auto &participant : state->participants) { participant.user->paintUserpicLeft( p, participant.userpic, left + (userWidth - st::confirmInviteUserPhotoSize) / 2, 0, rawStrip->width(), st::confirmInviteUserPhotoSize); left += userWidth; } }, rawStrip->lifetime()); Ui::AddSkip(content, st::boxPadding.bottom()); content->add(std::move(strip), style::margins()); } box->addButton((requestApprove ? tr::lng_group_request_to_join() : isChannel ? tr::lng_profile_join_channel() : tr::lng_profile_join_group()), submit); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } } // namespace void CheckChatInvite( not_null controller, const QString &hash, ChannelData *invitePeekChannel, Fn loaded) { const auto session = &controller->session(); const auto weak = base::make_weak(controller); session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) { const auto strong = weak.get(); if (!strong) { return; } if (loaded) { loaded(); } Core::App().hideMediaView(); const auto show = [&](not_null chat) { const auto way = Window::SectionShow::Way::Forward; if (const auto forum = chat->forum()) { strong->showForum(forum, way); } else { strong->showPeerHistory(chat, way); } }; result.match([=](const MTPDchatInvite &data) { const auto isGroup = !data.is_broadcast(); const auto hasPricing = !!data.vsubscription_pricing(); const auto canRefulfill = data.is_can_refulfill_subscription(); if (hasPricing && !canRefulfill && !data.vsubscription_form_id()) { strong->uiShow()->showToast( tr::lng_confirm_phone_link_invalid(tr::now)); return; } const auto box = (hasPricing && !canRefulfill) ? strong->show(Box( ConfirmSubscriptionBox, session, hash, &data)) : strong->show(Box( ConfirmInviteBox, session, &data, invitePeekChannel, [=] { SubmitChatInvite(weak, session, hash, isGroup); })); if (invitePeekChannel) { box->boxClosing( ) | rpl::filter([=] { return !invitePeekChannel->amIn(); }) | rpl::on_next([=] { if (const auto strong = weak.get()) { strong->clearSectionStack(Window::SectionShow( Window::SectionShow::Way::ClearStack, anim::type::normal, anim::activation::background)); } }, box->lifetime()); } }, [=](const MTPDchatInviteAlready &data) { if (const auto chat = session->data().processChat(data.vchat())) { if (const auto channel = chat->asChannel()) { channel->clearInvitePeek(); } show(chat); } }, [=](const MTPDchatInvitePeek &data) { if (const auto chat = session->data().processChat(data.vchat())) { if (const auto channel = chat->asChannel()) { channel->setInvitePeek(hash, data.vexpires().v); show(chat); } } }); }, [=](const MTP::Error &error) { if (MTP::IsFloodError(error)) { if (const auto strong = weak.get()) { strong->show(Ui::MakeInformBox(tr::lng_flood_error())); } return; } if (error.code() != 400) { return; } Core::App().hideMediaView(); if (const auto strong = weak.get()) { strong->show(Ui::MakeInformBox(tr::lng_group_invite_bad_link())); } }); } } // namespace Api