/* 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 "info/bot/starref/info_bot_starref_common.h" #include "apiwrap.h" #include "boxes/peers/replace_boost_box.h" // CreateUserpicsTransfer. #include "boxes/send_credits_box.h" // Ui::CreditsEmoji. #include "chat_helpers/stickers_lottie.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_session.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/controls/who_reacted_context_action.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "ui/layers/generic_box.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/table_layout.h" #include "ui/wrap/vertical_layout.h" #include "ui/text/text_utilities.h" #include "ui/new_badges.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_giveaway.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" #include namespace Info::BotStarRef { namespace { void ConnectStarRef( not_null bot, not_null peer, Fn done, Fn fail) { bot->session().api().request(MTPpayments_ConnectStarRefBot( peer->input, bot->inputUser )).done([=](const MTPpayments_ConnectedStarRefBots &result) { const auto parsed = Parse(&bot->session(), result); if (parsed.empty()) { fail(u"EMPTY"_q); } else { done(parsed.front()); } }).fail([=](const MTP::Error &error) { fail(error.type()); }).send(); } [[nodiscard]] object_ptr CreateLinkIcon( not_null parent, not_null session, int users) { auto result = object_ptr(parent); const auto raw = result.data(); struct State { not_null icon; std::shared_ptr media; std::shared_ptr player; int counterWidth = 0; }; const auto outerSide = st::starrefLinkThumbOuter; const auto outerSkip = (outerSide - st::starrefLinkThumbInner) / 2; const auto innerSide = (outerSide - 2 * outerSkip); const auto add = st::starrefLinkCountAdd; const auto outer = QSize(outerSide, outerSide + add); const auto inner = QSize(innerSide, innerSide); const auto state = raw->lifetime().make_state(State{ .icon = ChatHelpers::GenerateLocalTgsSticker( session, u"starref_link"_q), }); state->icon->overrideEmojiUsesTextColor(true); state->media = state->icon->createMediaView(); state->player = std::make_unique( ChatHelpers::LottiePlayerFromDocument( state->media.get(), ChatHelpers::StickerLottieSize::MessageHistory, inner, Lottie::Quality::High)); const auto player = state->player.get(); player->setRepaintCallback([=] { raw->update(); }); const auto text = users ? Lang::FormatCountToShort(users).string : QString(); const auto length = st::starrefLinkCountFont->width(text); const auto contents = length + st::starrefLinkCountIcon.width(); const auto delta = (outer.width() - contents) / 2; const auto badge = QRect( delta, outer.height() - st::starrefLinkCountFont->height - st::lineWidth, outer.width() - 2 * delta, st::starrefLinkCountFont->height); const auto badgeRect = badge.marginsAdded(st::starrefLinkCountPadding); raw->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(raw); p.setPen(Qt::NoPen); p.setBrush(st::windowBgActive); auto hq = PainterHighQualityEnabler(p); const auto left = (raw->width() - outer.width()) / 2; p.drawEllipse(left, 0, outerSide, outerSide); if (!text.isEmpty()) { const auto rect = badgeRect.translated(left, 0); const auto textRect = badge.translated(left, 0); const auto radius = st::starrefLinkCountFont->height / 2.; p.setPen(st::historyPeerUserpicFg); p.setBrush(st::historyPeer2UserpicBg2); p.drawRoundedRect(rect, radius, radius); p.setFont(st::starrefLinkCountFont); const auto shift = QPoint( st::starrefLinkCountIcon.width(), st::starrefLinkCountFont->ascent); st::starrefLinkCountIcon.paint( p, textRect.topLeft() + st::starrefLinkCountIconPosition, raw->width()); p.drawText(textRect.topLeft() + shift, text); } if (player->ready()) { const auto now = crl::now(); const auto color = st::windowFgActive->c; auto info = player->frame(inner, color, false, now, false); p.drawImage( QRect(QPoint(left + outerSkip, outerSkip), inner), info.image); if (info.index + 1 < player->framesCount()) { player->markFrameShown(); } } }, raw->lifetime()); raw->resize(outer); return result; } void ChooseRecipient( not_null button, const std::vector> &list, not_null now, Fn)> done) { const auto menu = Ui::CreateChild( button, st::starrefPopupMenu); struct Entry { not_null action; std::shared_ptr userpic; }; auto actions = std::make_shared>(); actions->reserve(list.size()); for (const auto &peer : list) { auto view = peer->createUserpicView(); auto action = base::make_unique_q( menu->menu(), Data::ReactedMenuFactory(&list.front()->session()), menu->menu()->st(), Ui::WhoReactedEntryData()); const auto index = int(actions->size()); actions->push_back({ action.get(), Ui::MakeUserpicThumbnail(peer) }); const auto updateUserpic = [=] { const auto size = st::defaultWhoRead.photoSize; actions->at(index).action->setData({ .text = peer->name(), .date = (peer->isSelf() ? tr::lng_group_call_join_as_personal(tr::now) : peer->isUser() ? tr::lng_status_bot(tr::now) : peer->isBroadcast() ? tr::lng_channel_status(tr::now) : tr::lng_group_status(tr::now)), .type = (peer == now ? Ui::WhoReactedType::RefRecipientNow : Ui::WhoReactedType::RefRecipient), .userpic = actions->at(index).userpic->image(size), .callback = [=] { done(peer); }, }); }; actions->back().userpic->subscribeToUpdates(updateUserpic); menu->addAction(std::move(action)); updateUserpic(); } menu->popup(button->mapToGlobal(QPoint(button->width() / 2, 0))); } } // namespace QString FormatCommission(ushort commission) { return QString::number(commission / 10.) + '%'; } QString FormatProgramDuration(int durationMonths) { return !durationMonths ? tr::lng_star_ref_duration_forever(tr::now) : (durationMonths < 12) ? tr::lng_months(tr::now, lt_count, durationMonths) : tr::lng_years(tr::now, lt_count, durationMonths / 12); } rpl::producer FormatForProgramDuration( int durationMonths) { return !durationMonths ? tr::lng_star_ref_one_about_for_forever(Ui::Text::RichLangValue) : (durationMonths < 12) ? tr::lng_star_ref_one_about_for_months( lt_count, rpl::single(durationMonths * 1.), Ui::Text::RichLangValue) : tr::lng_star_ref_one_about_for_years( lt_count, rpl::single((durationMonths / 12) * 1.), Ui::Text::RichLangValue); } not_null AddViewListButton( not_null parent, rpl::producer title, rpl::producer subtitle, bool newBadge) { const auto &stLabel = st::defaultFlatLabel; const auto iconSize = st::settingsPremiumIconDouble.size(); const auto &titlePadding = st::settingsPremiumRowTitlePadding; const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; const auto button = Ui::CreateChild( parent, rpl::single(QString())); button->show(); const auto label = parent->add( object_ptr( parent, std::move(title) | Ui::Text::ToBold(), stLabel), titlePadding); label->setAttribute(Qt::WA_TransparentForMouseEvents); const auto description = parent->add( object_ptr( parent, std::move(subtitle), st::boxDividerLabel), descriptionPadding); description->setAttribute(Qt::WA_TransparentForMouseEvents); if (newBadge) { Ui::NewBadge::AddAfterLabel(parent, label); } const auto dummy = Ui::CreateChild(parent); dummy->setAttribute(Qt::WA_TransparentForMouseEvents); dummy->show(); parent->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { dummy->resize(s.width(), iconSize.height()); }, dummy->lifetime()); button->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { dummy->moveToLeft(0, r.y() + (r.height() - iconSize.height()) / 2); }, dummy->lifetime()); ::Settings::AddButtonIcon(dummy, st::settingsButton, { .icon = &st::settingsStarRefEarnStars, .backgroundBrush = st::premiumIconBg3, }); rpl::combine( parent->widthValue(), label->heightValue(), description->heightValue() ) | rpl::start_with_next([=, topPadding = titlePadding, bottomPadding = descriptionPadding]( int width, int topHeight, int bottomHeight) { button->resize( width, topPadding.top() + topHeight + topPadding.bottom() + bottomPadding.top() + bottomHeight + bottomPadding.bottom()); }, button->lifetime()); label->topValue( ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) { button->moveToLeft(0, top - padding); }, button->lifetime()); const auto arrow = Ui::CreateChild( button, st::backButton); arrow->setIconOverride( &st::settingsPremiumArrow, &st::settingsPremiumArrowOver); arrow->setAttribute(Qt::WA_TransparentForMouseEvents); button->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { const auto &point = st::settingsPremiumArrowShift; arrow->moveToRight( -point.x(), point.y() + (s.height() - arrow->height()) / 2); }, arrow->lifetime()); return button; } not_null AddFullWidthButton( not_null box, rpl::producer text, Fn callback, const style::RoundButton *stOverride) { const auto &boxSt = box->getDelegate()->style(); const auto result = box->addButton( std::move(text), std::move(callback), stOverride ? *stOverride : boxSt.button); rpl::combine( box->widthValue(), result->widthValue() ) | rpl::start_with_next([=](int width, int buttonWidth) { const auto correct = width - boxSt.buttonPadding.left() - boxSt.buttonPadding.right(); if (correct > 0 && buttonWidth != correct) { result->resizeToWidth(correct); result->moveToLeft( boxSt.buttonPadding.left(), boxSt.buttonPadding.top(), width); } }, result->lifetime()); return result; } void AddFullWidthButtonFooter( not_null box, not_null button, rpl::producer text) { const auto footer = Ui::CreateChild( button->parentWidget(), std::move(text), st::starrefJoinFooter); footer->setTryMakeSimilarLines(true); button->geometryValue() | rpl::start_with_next([=](QRect geometry) { footer->resizeToWidth(geometry.width()); const auto &st = box->getDelegate()->style(); const auto top = geometry.y() + geometry.height(); const auto available = st.buttonPadding.bottom(); footer->moveToLeft( geometry.left(), top + (available - footer->height()) / 2); }, footer->lifetime()); } object_ptr MakeLinkLabel( not_null parent, const QString &link, const style::InputField *stOverride) { const auto &st = stOverride ? *stOverride : st::dialogsFilter; const auto text = link.startsWith(u"https://"_q) ? link.mid(8) : link.startsWith(u"http://"_q) ? link.mid(7) : link; const auto margins = st.textMargins; const auto height = st.heightMin; const auto skip = margins.left(); auto result = object_ptr(parent); const auto raw = result.data(); raw->resize(height, height); raw->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(raw); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st.textBg); const auto radius = st::roundRadiusLarge; p.drawRoundedRect(0, 0, raw->width(), height, radius, radius); const auto font = st.style.font; p.setPen(st.textFg); p.setFont(font); const auto available = raw->width() - skip * 2; p.drawText( QRect(skip, margins.top(), available, font->height), style::al_top, font->elided(text, available)); }, raw->lifetime()); return result; } object_ptr StarRefLinkBox( ConnectedBot row, not_null peer) { return Box([=](not_null box) { const auto show = box->uiShow(); const auto bot = row.bot; const auto program = row.state.program; box->setStyle(st::starrefFooterBox); box->setNoContentMargin(true); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); box->addRow( CreateLinkIcon(box, &bot->session(), row.state.users), st::boxRowPadding + st::starrefJoinUserpicsPadding); box->addRow( object_ptr>( box, object_ptr( box, tr::lng_star_ref_link_title(), st::boxTitle)), st::boxRowPadding + st::starrefJoinTitlePadding); box->addRow( object_ptr( box, (peer->isSelf() ? tr::lng_star_ref_link_about_user : peer->isUser() ? tr::lng_star_ref_link_about_user : tr::lng_star_ref_link_about_channel)( lt_amount, rpl::single(Ui::Text::Bold( FormatCommission(program.commission))), lt_app, rpl::single(Ui::Text::Bold(bot->name())), lt_duration, FormatForProgramDuration(program.durationMonths), Ui::Text::WithEntities), st::starrefCenteredText), st::boxRowPadding); Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3); box->addRow( object_ptr( box, tr::lng_star_ref_link_recipient(), st::starrefCenteredText)); Ui::AddSkip(box->verticalLayout()); box->addRow(object_ptr::fromRaw( MakePeerBubbleButton(box, peer).release() ))->setAttribute(Qt::WA_TransparentForMouseEvents); Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2); const auto preview = box->addRow(MakeLinkLabel(box, row.state.link)); Ui::AddSkip(box->verticalLayout()); const auto copy = [=](bool close) { return [=] { QApplication::clipboard()->setText(row.state.link); box->uiShow()->showToast(tr::lng_username_copied(tr::now)); if (close) { box->closeBox(); } }; }; preview->setClickedCallback(copy(false)); const auto button = AddFullWidthButton( box, tr::lng_star_ref_link_copy(), copy(true), &st::starrefCopyButton); const auto name = TextWithEntities{ bot->name() }; AddFullWidthButtonFooter( box, button, (row.state.users > 0 ? tr::lng_star_ref_link_copy_users( lt_count, rpl::single(row.state.users * 1.), lt_app, rpl::single(name), Ui::Text::WithEntities) : tr::lng_star_ref_link_copy_none( lt_app, rpl::single(name), Ui::Text::WithEntities))); }); } object_ptr JoinStarRefBox( ConnectedBot row, not_null initialRecipient, std::vector> recipients, Fn done) { Expects(row.bot->isUser()); return Box([=](not_null box) { const auto show = box->uiShow(); const auto bot = row.bot; const auto program = row.state.program; auto list = recipients; if (!list.empty()) { list.erase(ranges::remove(list, bot), end(list)); if (!ranges::contains(list, initialRecipient)) { list.insert(begin(list), initialRecipient); } } box->setStyle(st::starrefFooterBox); box->setNoContentMargin(true); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); struct State { rpl::variable> recipient; QPointer weak; bool sent = false; }; const auto state = std::make_shared(State{ .recipient = initialRecipient, .weak = box.get(), }); const auto userpicsWrap = box->addRow( object_ptr(box), QMargins()); state->recipient.value( ) | rpl::start_with_next([=](not_null recipient) { while (userpicsWrap->count()) { delete userpicsWrap->widgetAt(0); } userpicsWrap->add( CreateUserpicsTransfer( box, rpl::single(std::vector{ not_null(bot) }), recipient, UserpicsTransferType::StarRefJoin), st::boxRowPadding + st::starrefJoinUserpicsPadding); userpicsWrap->resizeToWidth(box->width()); }, box->lifetime()); box->addRow( object_ptr>( box, object_ptr( box, tr::lng_star_ref_title(), st::boxTitle)), st::boxRowPadding + st::starrefJoinTitlePadding); box->addRow( object_ptr( box, tr::lng_star_ref_one_about( lt_app, rpl::single(Ui::Text::Bold(bot->name())), lt_amount, rpl::single(Ui::Text::Bold( FormatCommission(program.commission))), lt_duration, FormatForProgramDuration(program.durationMonths), Ui::Text::WithEntities), st::starrefCenteredText), st::boxRowPadding); Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3); if (const auto average = program.revenuePerUser) { const auto layout = box->verticalLayout(); const auto session = &initialRecipient->session(); auto text = Ui::Text::Colorized(Ui::CreditsEmoji(session)); text.append(Lang::FormatStarsAmountRounded(average)); layout->add( object_ptr( box, tr::lng_star_ref_one_daily_revenue( lt_amount, rpl::single( Ui::Text::Wrapped(text, EntityType::Bold)), Ui::Text::WithEntities), st::starrefRevenueText, st::defaultPopupMenu, Core::TextContext({ .session = session })), st::boxRowPadding); Ui::AddSkip(layout, st::defaultVerticalListSkip); } if (!list.empty()) { struct Name { not_null peer; QString name; }; auto names = ranges::views::transform(list, [](auto peer) { const auto name = TextUtilities::NameSortKey(peer->name()); return Name{ peer, name }; }) | ranges::to_vector; ranges::sort(names, ranges::less(), &Name::name); list = ranges::views::transform(names, &Name::peer) | ranges::to_vector; box->addRow( object_ptr( box, tr::lng_star_ref_link_recipient(), st::starrefCenteredText)); Ui::AddSkip(box->verticalLayout()); const auto recipientWrap = box->addRow( object_ptr(box), QMargins()); state->recipient.value( ) | rpl::start_with_next([=](not_null recipient) { while (recipientWrap->count()) { delete recipientWrap->widgetAt(0); } const auto selectable = (list.size() > 1); const auto bgOverride = selectable ? &st::lightButtonBgOver : nullptr; const auto right = selectable ? Ui::CreateChild(recipientWrap) : nullptr; if (right) { const auto skip = st::chatGiveawayPeerPadding.right(); const auto icon = &st::starrefRecipientArrow; const auto height = st::chatGiveawayPeerSize - st::chatGiveawayPeerPadding.top() * 2; right->resize(skip + icon->width(), height); right->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(right); icon->paint( p, skip, (height - icon->height()) / 2, right->width()); }, right->lifetime()); } const auto button = recipientWrap->add( object_ptr::fromRaw( MakePeerBubbleButton( box, recipient, right, bgOverride).release()), st::boxRowPadding); recipientWrap->resizeToWidth(box->width()); if (!selectable) { button->setAttribute(Qt::WA_TransparentForMouseEvents); return; } button->setClickedCallback([=] { const auto callback = [=](not_null peer) { state->recipient = peer; }; ChooseRecipient( button, list, state->recipient.current(), crl::guard(button, callback)); }); }, box->lifetime()); } const auto send = [=] { if (state->sent) { return; } state->sent = true; const auto recipient = state->recipient.current(); ConnectStarRef(bot->asUser(), recipient, [=](ConnectedBot info) { if (recipient == initialRecipient) { if (const auto onstack = done) { onstack(info.state); } } show->show(StarRefLinkBox(info, recipient)); if (const auto strong = state->weak.data()) { strong->closeBox(); } }, [=](const QString &error) { state->sent = false; show->showToast(u"Failed: "_q + error); }); }; const auto button = AddFullWidthButton( box, tr::lng_star_ref_one_join(), send); AddFullWidthButtonFooter( box, button, tr::lng_star_ref_one_join_text( lt_terms, tr::lng_star_ref_button_link( ) | Ui::Text::ToLink(tr::lng_star_ref_tos_url(tr::now)), Ui::Text::WithEntities)); }); } object_ptr ConfirmEndBox(Fn finish) { return Box([=](not_null box) { box->setTitle(tr::lng_star_ref_warning_title()); const auto skip = st::defaultVerticalListSkip; const auto margins = st::boxRowPadding + QMargins(0, 0, 0, skip); box->addRow( object_ptr( box, tr::lng_star_ref_warning_if_end(Ui::Text::RichLangValue), st::boxLabel), margins); const auto addPoint = [&](tr::phrase<> text) { const auto padded = box->addRow( object_ptr>( box, object_ptr( box, text(Ui::Text::RichLangValue), st::blockUserConfirmation), QMargins(st::boxTextFont->height, 0, 0, 0)), margins); padded->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(padded); auto hq = PainterHighQualityEnabler(p); const auto size = st::starrefEndBulletSize; const auto top = st::starrefEndBulletTop; p.setBrush(st::windowFg); p.setPen(Qt::NoPen); p.drawEllipse(0, top, size, size); }, padded->lifetime()); }; addPoint(tr::lng_star_ref_warning_if_end1); addPoint(tr::lng_star_ref_warning_if_end2); addPoint(tr::lng_star_ref_warning_if_end3); const auto done = [=] { box->closeBox(); finish(); }; box->addButton( tr::lng_star_ref_warning_end(), done, st::attentionBoxButton); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }); } void ResolveRecipients( not_null session, Fn>)> done) { struct State { not_null session; std::vector> list; Fn>)> done; }; const auto state = std::make_shared(State{ .session = session, .done = std::move(done), }); const auto finish1 = [state](const MTPmessages_Chats &result) { const auto already = int(state->list.size()); const auto session = state->session; result.match([&](const auto &data) { const auto &list = data.vchats().v; state->list.reserve(list.size() + (already ? already : 1)); if (!already) { state->list.push_back(session->user()); } for (const auto &chat : list) { const auto peer = session->data().processChat(chat); if (const auto channel = peer->asBroadcast()) { if (channel->canPostMessages()) { state->list.push_back(channel); } } } if (already) { base::take(state->done)(base::take(state->list)); } }); }; const auto finish2 = [state](const MTPVector &result) { const auto already = int(state->list.size()); const auto session = state->session; const auto &list = result.v; state->list.reserve(list.size() + (already ? already : 1)); if (!already) { state->list.push_back(session->user()); } for (const auto &user : list) { state->list.push_back(session->data().processUser(user)); } if (already) { base::take(state->done)(base::take(state->list)); } }; session->api().request(MTPchannels_GetAdminedPublicChannels( MTP_flags(0) )).done(finish1).fail([=] { finish1(MTP_messages_chats(MTP_vector(0))); }).send(); state->session->api().request(MTPbots_GetAdminedBots( )).done(finish2).fail([=] { finish2(MTP_vector(0)); }).send(); } std::unique_ptr MakePeerBubbleButton( not_null parent, not_null peer, Ui::RpWidget *right, const style::color *bgOverride) { class Button final : public Ui::AbstractButton { public: Button(QWidget *parent, not_null innerWidth) : AbstractButton(parent) , _innerWidth(innerWidth) { } void mouseMoveEvent(QMouseEvent *e) override { const auto inner = *_innerWidth; const auto skip = (width() - inner) / 2; const auto p = e->pos(); const auto over = QRect(skip, 0, inner, height()).contains(p); setOver(over, StateChangeSource::ByHover); } private: const not_null _innerWidth; }; auto ownedWidth = std::make_unique(); const auto width = ownedWidth.get(); auto result = std::make_unique