/* 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/channel_statistics/boosts/create_giveaway_box.h" #include "api/api_credits.h" #include "api/api_premium.h" #include "base/call_delayed.h" #include "base/unixtime.h" #include "countries/countries_instance.h" #include "data/data_peer.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" #include "info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h" #include "info/channel_statistics/boosts/giveaway/giveaway_type_row.h" #include "info/channel_statistics/boosts/giveaway/select_countries_box.h" #include "info/channel_statistics/boosts/info_boosts_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "payments/payments_checkout_process.h" // Payments::CheckoutProcess #include "payments/payments_form.h" // Payments::InvoicePremiumGiftCode #include "settings/settings_common.h" #include "settings/settings_premium.h" // Settings::ShowPremium #include "ui/boxes/choose_date_time.h" #include "ui/boxes/confirm_box.h" #include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" #include "ui/ui_utility.h" #include "styles/style_credits.h" #include "styles/style_giveaway.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" #include "styles/style_statistics.h" #include // XXH64. namespace { constexpr auto kDoneTooltipDuration = 5 * crl::time(1000); constexpr auto kAdditionalPrizeLengthMax = 128; constexpr auto kColorIndexCredits = int(1); [[nodiscard]] QDateTime ThreeDaysAfterToday() { auto dateNow = QDateTime::currentDateTime(); dateNow = dateNow.addDays(3); auto timeNow = dateNow.time(); while (timeNow.minute() % 5) { timeNow = timeNow.addSecs(60); } dateNow.setTime(timeNow); return dateNow; } [[nodiscard]] uint64 UniqueIdFromCreditsOption( const Data::CreditsGiveawayOption &d, not_null peer) { const auto string = QString::number(d.credits) + d.storeProduct + d.currency + QString::number(d.amount) + QString::number(peer->id.value) + QString::number(peer->session().uniqueId()); return XXH64(string.data(), string.size() * sizeof(ushort), 0); } [[nodiscard]] Fn CreateErrorCallback( int max, tr::phrase phrase) { return [=](int count) { const auto error = (count >= max); if (error) { Ui::Toast::Show(phrase(tr::now, lt_count, max)); } return error; }; } [[nodiscard]] QWidget *FindFirstShadowInBox(not_null box) { for (const auto &child : box->children()) { if (child && child->isWidgetType()) { const auto w = static_cast(child); if (w->height() == st::lineWidth) { return w; } } } return nullptr; } void AddPremiumTopBarWithDefaultTitleBar( not_null box, rpl::producer<> showFinished, rpl::producer titleText, rpl::producer subtitleText) { struct State final { Ui::Animations::Simple animation; Ui::Text::String title; Ui::RpWidget close; }; const auto state = box->lifetime().make_state(); box->setNoContentMargin(true); std::move( titleText ) | rpl::start_with_next([=](const QString &s) { state->title.setText(st::startGiveawayBox.title.style, s); }, box->lifetime()); const auto hPadding = rect::m::sum::h(st::boxRowPadding); const auto titlePaintContext = Ui::Text::PaintContext{ .position = st::boxTitlePosition, .outerWidth = (st::boxWideWidth - hPadding), .availableWidth = (st::boxWideWidth - hPadding), }; const auto isCloseBarShown = [=] { return box->scrollTop() > 0; }; const auto closeTopBar = box->setPinnedToTopContent( object_ptr(box)); closeTopBar->resize(box->width(), st::boxTitleHeight); closeTopBar->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(closeTopBar); const auto r = closeTopBar->rect(); const auto radius = st::boxRadius; const auto progress = state->animation.value(isCloseBarShown() ? 1. : 0.); const auto resultRect = r + QMargins{ 0, 0, 0, radius }; { auto hq = PainterHighQualityEnabler(p); if (progress < 1.) { auto path = QPainterPath(); path.addRect(resultRect); path.addRect( st::boxRowPadding.left(), 0, resultRect.width() - hPadding, resultRect.height()); p.setClipPath(path); p.setPen(Qt::NoPen); p.setBrush(st::boxDividerBg); p.drawRoundedRect(resultRect, radius, radius); } if (progress > 0.) { p.setOpacity(progress); p.setClipping(false); p.setPen(Qt::NoPen); p.setBrush(st::boxBg); p.drawRoundedRect(resultRect, radius, radius); p.setPen(st::startGiveawayBox.title.textFg); p.setBrush(Qt::NoBrush); state->title.draw(p, titlePaintContext); } } }, closeTopBar->lifetime()); { const auto close = Ui::CreateChild( closeTopBar.get(), st::startGiveawayBoxTitleClose); close->setClickedCallback([=] { box->closeBox(); }); closeTopBar->widthValue( ) | rpl::start_with_next([=](int w) { const auto &pos = st::giveawayGiftCodeCoverClosePosition; close->moveToRight(pos.x(), pos.y()); }, box->lifetime()); close->show(); } const auto bar = Ui::CreateChild( box.get(), st::startGiveawayCover, Ui::Premium::TopBarDescriptor{ .clickContextOther = nullptr, .title = tr::lng_giveaway_new_title(), .about = std::move(subtitleText), .light = true, .optimizeMinistars = false, }); bar->setAttribute(Qt::WA_TransparentForMouseEvents); box->addRow( object_ptr( box.get(), st::giveawayGiftCodeTopHeight - st::boxTitleHeight + st::boxDividerHeight + st::defaultVerticalListSkip, st::boxDividerBg, RectPart::Bottom), {}); bar->setPaused(true); bar->setRoundEdges(false); bar->setMaximumHeight(st::giveawayGiftCodeTopHeight); bar->setMinimumHeight(st::infoLayerTopBarHeight); bar->resize(bar->width(), bar->maximumHeight()); box->widthValue( ) | rpl::start_with_next([=](int w) { bar->resizeToWidth(w - hPadding); bar->moveToLeft(st::boxRowPadding.left(), bar->y()); }, box->lifetime()); std::move( showFinished ) | rpl::take(1) | rpl::start_with_next([=] { closeTopBar->raise(); if (const auto shadow = FindFirstShadowInBox(box)) { bar->stackUnder(shadow); } bar->setPaused(false); box->scrolls( ) | rpl::map(isCloseBarShown) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool showBar) { state->animation.stop(); state->animation.start( [=] { closeTopBar->update(); }, showBar ? 0. : 1., showBar ? 1. : 0., st::slideWrapDuration); }, box->lifetime()); box->scrolls( ) | rpl::start_with_next([=] { bar->moveToLeft(bar->x(), -box->scrollTop()); }, box->lifetime()); }, box->lifetime()); bar->show(); } } // namespace void CreateGiveawayBox( not_null box, not_null navigation, not_null peer, Fn reloadOnDone, std::optional prepaid) { box->setWidth(st::boxWideWidth); const auto weakWindow = base::make_weak(navigation->parentController()); using GiveawayType = Giveaway::GiveawayTypeRow::Type; using GiveawayGroup = Ui::RadioenumGroup; using CreditsGroup = Ui::RadioenumGroup; struct State final { State(not_null p) : apiOptions(p), apiCreditsOptions(p) { } Api::PremiumGiftCodeOptions apiOptions; Api::CreditsGiveawayOptions apiCreditsOptions; rpl::lifetime lifetimeApi; std::vector> selectedToAward; rpl::event_stream<> toAwardAmountChanged; std::vector> selectedToSubscribe; rpl::variable typeValue; rpl::variable sliderValue; rpl::variable dateValue; rpl::variable> countriesValue; rpl::variable additionalPrize; rpl::variable chosenMonths; rpl::variable showWinners; rpl::variable confirmButtonBusy = true; }; const auto group = peer->isMegagroup(); const auto state = box->lifetime().make_state(peer); const auto typeGroup = std::make_shared(); const auto creditsGroup = std::make_shared(); const auto isPrepaidCredits = (prepaid && prepaid->credits); const auto isSpecificUsers = [=] { return !state->selectedToAward.empty(); }; const auto hideSpecificUsersOn = [=] { return rpl::combine( state->typeValue.value(), state->toAwardAmountChanged.events_starting_with( rpl::empty_value()) | rpl::type_erased() ) | rpl::map([=](GiveawayType type, auto) { return (type == GiveawayType::Credits) || !isSpecificUsers(); }); }; auto showFinished = Ui::BoxShowFinishes(box); AddPremiumTopBarWithDefaultTitleBar( box, rpl::duplicate(showFinished), rpl::conditional( hideSpecificUsersOn(), tr::lng_giveaway_start(), tr::lng_giveaway_award()), rpl::conditional( isPrepaidCredits ? rpl::single(true) | rpl::type_erased() : state->typeValue.value() | rpl::map( rpl::mappers::_1 == GiveawayType::Credits), (peer->isMegagroup() ? tr::lng_giveaway_credits_new_about_group() : tr::lng_giveaway_credits_new_about()), (peer->isMegagroup() ? tr::lng_giveaway_new_about_group() : tr::lng_giveaway_new_about()) ) | rpl::map(Ui::Text::RichLangValue)); { const auto &padding = st::giveawayGiftCodeCoverDividerPadding; Ui::AddSkip(box->verticalLayout(), padding.bottom()); } const auto loading = box->addRow( object_ptr>( box, object_ptr(box))); { loading->toggle(true, anim::type::instant); const auto container = loading->entity(); Ui::AddSkip(container); Ui::AddSkip(container); container->add( object_ptr>( box, object_ptr( box, tr::lng_contacts_loading(), st::giveawayLoadingLabel))); Ui::AddSkip(container); Ui::AddSkip(container); } const auto contentWrap = box->verticalLayout()->add( object_ptr>( box, object_ptr(box))); contentWrap->toggle(false, anim::type::instant); if (prepaid) { contentWrap->entity()->add( object_ptr( box, prepaid->credits ? GiveawayType::PrepaidCredits : GiveawayType::Prepaid, prepaid->credits ? kColorIndexCredits : prepaid->id, tr::lng_boosts_prepaid_giveaway_single(), prepaid->credits ? tr::lng_boosts_prepaid_giveaway_credits_status( lt_count, rpl::single(prepaid->quantity) | tr::to_count(), lt_amount, tr::lng_prize_credits_amount( lt_count_decimal, rpl::single(prepaid->credits) | tr::to_count())) : tr::lng_boosts_prepaid_giveaway_status( lt_count, rpl::single(prepaid->quantity) | tr::to_count(), lt_duration, tr::lng_premium_gift_duration_months( lt_count, rpl::single(prepaid->months) | tr::to_count())), QImage()) )->setAttribute(Qt::WA_TransparentForMouseEvents); } if (!prepaid) { const auto row = contentWrap->entity()->add( object_ptr( box, GiveawayType::Random, state->toAwardAmountChanged.events_starting_with( rpl::empty_value() ) | rpl::map([=] { const auto &selected = state->selectedToAward; return selected.empty() ? tr::lng_giveaway_create_subtitle() : (selected.size() == 1) ? rpl::single(selected.front()->name()) : tr::lng_giveaway_award_chosen( lt_count, rpl::single(selected.size()) | tr::to_count()); }) | rpl::flatten_latest(), group)); row->addRadio(typeGroup); row->setClickedCallback([=] { auto initBox = [=](not_null peersBox) { peersBox->setTitle(tr::lng_giveaway_award_option()); auto aboveOwned = object_ptr(peersBox); const auto above = aboveOwned.data(); peersBox->peerListSetAboveWidget(std::move(aboveOwned)); Ui::AddSkip(above); const auto buttonRandom = above->add( object_ptr( peersBox, tr::lng_giveaway_random_button(), st::settingsButtonLightNoIcon)); buttonRandom->setClickedCallback([=] { state->selectedToAward.clear(); state->toAwardAmountChanged.fire({}); state->typeValue.force_assign(GiveawayType::Random); peersBox->closeBox(); }); Ui::AddSkip(above); peersBox->addButton(tr::lng_settings_save(), [=] { state->selectedToAward = peersBox->collectSelectedRows(); state->toAwardAmountChanged.fire({}); state->typeValue.force_assign(GiveawayType::Random); peersBox->closeBox(); }); peersBox->addButton(tr::lng_cancel(), [=] { peersBox->closeBox(); }); }; using Controller = Giveaway::AwardMembersListController; auto listController = std::make_unique( navigation, peer, state->selectedToAward); listController->setCheckError(CreateErrorCallback( state->apiOptions.giveawayAddPeersMax(), tr::lng_giveaway_maximum_users_error)); box->uiShow()->showBox( Box( std::move(listController), std::move(initBox)), Ui::LayerOption::KeepOther); }); } const auto creditsOption = [=](int index) { const auto options = state->apiCreditsOptions.options(); return (index >= 0 && index < options.size()) ? options[index] : Data::CreditsGiveawayOption(); }; const auto creditsOptionWinners = [=](int index) { const auto winners = creditsOption(index).winners; return ranges::views::all( winners ) | ranges::views::transform([](const auto &w) { return w.users; }) | ranges::to_vector; }; const auto creditsTypeWrap = contentWrap->entity()->add( object_ptr(contentWrap->entity())); const auto fillCreditsTypeWrap = [=] { if (state->apiCreditsOptions.options().empty()) { return; } static constexpr auto kOutdated = 1735689600; auto badge = [&] { if (base::unixtime::now() > kOutdated) { return QImage(); } const auto badge = Ui::CreateChild>( creditsTypeWrap, object_ptr( creditsTypeWrap, tr::lng_premium_summary_new_badge(tr::now), st::settingsPremiumNewBadge), st::settingsPremiumNewBadgePadding); badge->setAttribute(Qt::WA_TransparentForMouseEvents); badge->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(badge); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::windowBgActive); const auto r = st::settingsPremiumNewBadgePadding.left(); p.drawRoundedRect(badge->rect(), r, r); }, badge->lifetime()); badge->show(); auto result = Ui::GrabWidget(badge).toImage(); badge->hide(); return result; }(); const auto row = creditsTypeWrap->add( object_ptr( box, GiveawayType::Credits, kColorIndexCredits, tr::lng_credits_summary_title(), tr::lng_giveaway_create_subtitle(), std::move(badge))); row->addRadio(typeGroup); row->setClickedCallback([=] { state->typeValue.force_assign(GiveawayType::Credits); }); }; { const auto &padding = st::giveawayGiftCodeTypeDividerPadding; Ui::AddSkip(contentWrap->entity(), padding.top()); Ui::AddDivider(contentWrap->entity()); Ui::AddSkip(contentWrap->entity(), padding.bottom()); } const auto randomWrap = contentWrap->entity()->add( object_ptr>( contentWrap, object_ptr(box))); state->typeValue.value( ) | rpl::start_with_next([=](GiveawayType type) { randomWrap->toggle(!isSpecificUsers(), anim::type::instant); }, randomWrap->lifetime()); randomWrap->toggleOn(hideSpecificUsersOn(), anim::type::instant); const auto randomCreditsWrap = randomWrap->entity()->add( object_ptr>( contentWrap, object_ptr(box))); randomCreditsWrap->toggleOn( state->typeValue.value( ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits), anim::type::instant); const auto fillCreditsOptions = [=] { randomCreditsWrap->entity()->clear(); const auto &st = st::giveawayTypeListItem; const auto &stButton = st::defaultSettingsButton; const auto &stStatus = st::defaultTextStyle; const auto buttonInnerSkip = st.height - stButton.height; const auto options = state->apiCreditsOptions.options(); const auto content = randomCreditsWrap->entity(); const auto title = Ui::AddSubsectionTitle( content, tr::lng_giveaway_credits_options_title()); const auto rightLabel = Ui::CreateChild( content, st::giveawayGiftCodeQuantitySubtitle); rightLabel->show(); rpl::combine( tr::lng_giveaway_quantity( lt_count, creditsGroup->value() | rpl::map([=](int i) -> float64 { return creditsOption(i).yearlyBoosts; })), title->positionValue(), content->geometryValue() ) | rpl::start_with_next([=](QString s, const QPoint &p, QRect) { rightLabel->setText(std::move(s)); rightLabel->moveToRight(st::boxRowPadding.right(), p.y()); }, rightLabel->lifetime()); const auto buttonHeight = st.height; const auto minCredits = 0; struct State final { rpl::variable isExtended = false; }; const auto creditsState = content->lifetime().make_state(); for (auto i = 0; i < options.size(); i++) { const auto &option = options[i]; if (option.credits < minCredits) { continue; } struct State final { std::optional text; QString status; bool hasStatus = false; }; const auto buttonWrap = content->add( object_ptr>( content, object_ptr( content, rpl::never(), stButton))); const auto button = buttonWrap->entity(); button->setPaddingOverride({ 0, buttonInnerSkip, 0, 0 }); const auto buttonState = button->lifetime().make_state(); buttonState->text.emplace( st.nameStyle, tr::lng_credits_summary_options_credits( tr::now, lt_count_decimal, option.credits)); buttonState->status = tr::lng_giveaway_credits_option_status( tr::now, lt_count_decimal, option.credits); const auto price = Ui::CreateChild( button, Ui::FillAmountAndCurrency(option.amount, option.currency), st::creditsTopupPrice); const auto inner = Ui::CreateChild(button); const auto stars = Ui::GenerateStars( st.nameStyle.font->height, (i + 1)); const auto textLeft = st.photoPosition.x() + (st.nameStyle.font->spacew * 2) + (stars.width() / style::DevicePixelRatio()); state->sliderValue.value( ) | rpl::start_with_next([=](int users) { const auto option = creditsOption(i); buttonState->hasStatus = false; for (const auto &winner : option.winners) { if (winner.users == users) { auto status = tr::lng_giveaway_credits_option_status( tr::now, lt_count_decimal, winner.perUserStars); buttonState->status = std::move(status); buttonState->hasStatus = true; inner->update(); return; } } inner->update(); }, button->lifetime()); inner->paintRequest( ) | rpl::start_with_next([=](const QRect &rect) { auto p = QPainter(inner); const auto namey = buttonState->hasStatus ? st.namePosition.y() : (buttonHeight - stStatus.font->height) / 2; p.drawImage(st.photoPosition.x(), namey, stars); p.setPen(st.nameFg); buttonState->text->draw(p, { .position = QPoint(textLeft, namey), .availableWidth = inner->width() - textLeft, .elisionLines = 1, }); if (buttonState->hasStatus) { p.setFont(stStatus.font); p.setPen(st.statusFg); p.setBrush(Qt::NoBrush); p.drawText( st.photoPosition.x(), st.statusPosition.y() + stStatus.font->ascent, buttonState->status); } }, inner->lifetime()); button->widthValue( ) | rpl::start_with_next([=](int width) { price->moveToRight( st::boxRowPadding.right(), (buttonHeight - price->height()) / 2); inner->moveToLeft(0, 0); inner->resize( width - price->width() - st::boxRowPadding.right() - st::boxRowPadding.left() / 2, buttonHeight); }, button->lifetime()); { const auto &st = st::defaultCheckbox; const auto radio = Ui::CreateChild>( button, creditsGroup, i, QString(), st); radio->moveToLeft( st::boxRowPadding.left(), (buttonHeight - radio->checkRect().height()) / 2); radio->setAttribute(Qt::WA_TransparentForMouseEvents); radio->show(); } button->setClickedCallback([=] { creditsGroup->setValue(i); }); if (option.isDefault) { creditsGroup->setValue(i); } buttonWrap->toggle( (!option.isExtended) || option.isDefault, anim::type::instant); if (option.isExtended) { buttonWrap->toggleOn(creditsState->isExtended.value()); } Ui::ToggleChildrenVisibility(button, true); } { Ui::AddSkip(content, st::settingsButton.padding.top()); const auto showMoreWrap = Info::Statistics::AddShowMoreButton( content, tr::lng_stories_show_more()); showMoreWrap->toggle(true, anim::type::instant); showMoreWrap->entity()->setClickedCallback([=] { showMoreWrap->toggle(false, anim::type::instant); creditsState->isExtended = true; }); } Ui::AddSkip(content); Ui::AddDividerText(content, tr::lng_giveaway_credits_options_about()); Ui::AddSkip(content); }; const auto sliderContainerWrap = randomWrap->entity()->add( object_ptr>( randomWrap, object_ptr(randomWrap))); const auto sliderContainer = sliderContainerWrap->entity(); sliderContainerWrap->toggle(true, anim::type::instant); const auto fillSliderContainer = [=] { const auto availablePresets = state->apiOptions.availablePresets(); const auto creditsOptions = state->apiCreditsOptions.options(); if (prepaid) { state->sliderValue = prepaid->quantity; return; } if (availablePresets.empty() && (creditsOptions.empty() || creditsOptions.front().winners.empty())) { return; } state->sliderValue = availablePresets.empty() ? creditsOptions.front().winners.front().users : availablePresets.front(); auto creditsValueType = typeGroup->value( ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits); const auto title = Ui::AddSubsectionTitle( sliderContainer, rpl::conditional( rpl::duplicate(creditsValueType), tr::lng_giveaway_credits_quantity_title(), tr::lng_giveaway_quantity_title())); const auto rightLabel = Ui::CreateChild( sliderContainer, st::giveawayGiftCodeQuantitySubtitle); rightLabel->show(); rpl::duplicate( creditsValueType ) | rpl::start_with_next([=](bool isCredits) { rightLabel->setVisible(!isCredits); }, rightLabel->lifetime()); const auto floatLabel = Ui::CreateChild( sliderContainer, st::giveawayGiftCodeQuantityFloat); floatLabel->show(); rpl::combine( tr::lng_giveaway_quantity( lt_count, state->sliderValue.value( ) | rpl::map([=](int v) -> float64 { return state->apiOptions.giveawayBoostsPerPremium() * v; })), title->positionValue(), sliderContainer->geometryValue() ) | rpl::start_with_next([=](QString s, const QPoint &p, QRect) { rightLabel->setText(std::move(s)); rightLabel->moveToRight(st::boxRowPadding.right(), p.y()); }, rightLabel->lifetime()); const auto &padding = st::giveawayGiftCodeSliderPadding; Ui::AddSkip(sliderContainer, padding.top()); const auto sliderParent = sliderContainer->add( object_ptr(sliderContainer), st::boxRowPadding); struct State final { Ui::MediaSliderWheelless *slider = nullptr; }; const auto sliderState = sliderParent->lifetime().make_state(); Ui::AddSkip(sliderContainer, padding.bottom()); rpl::combine( rpl::duplicate(creditsValueType), creditsGroup->value() ) | rpl::start_with_next([=](bool isCredits, int value) { while (sliderParent->count()) { delete sliderParent->widgetAt(0); } sliderState->slider = sliderParent->add( object_ptr( sliderContainer, st::settingsScale)); sliderState->slider->resize( sliderState->slider->width(), st::settingsScale.seekSize.height()); const auto &values = isCredits ? creditsOptionWinners(value) : availablePresets; const auto resultValue = [&] { const auto sliderValue = state->sliderValue.current(); return ranges::contains(values, sliderValue) ? sliderValue : values.front(); }(); state->sliderValue.force_assign(resultValue); if (values.size() <= 1) { sliderContainerWrap->toggle(false, anim::type::instant); return; } else { sliderContainerWrap->toggle(true, anim::type::instant); } sliderState->slider->setPseudoDiscrete( values.size(), [=](int index) { return values[index]; }, resultValue, [=](int boosts) { state->sliderValue = boosts; }, [](int) {}); }, sliderParent->lifetime()); rpl::combine( rpl::duplicate(creditsValueType), creditsGroup->value(), state->sliderValue.value() ) | rpl::start_with_next([=]( bool isCredits, int credits, int boosts) { floatLabel->setText(QString::number(boosts)); if (!sliderState->slider) { return; } const auto &values = isCredits ? creditsOptionWinners(credits) : availablePresets; const auto count = values.size(); if (count <= 1) { return; } const auto sliderWidth = sliderState->slider->width() - st::settingsScale.seekSize.width(); for (auto i = 0; i < count; i++) { if ((i + 1 == count || values[i + 1] > boosts) && values[i] <= boosts) { const auto x = (sliderWidth * i) / (count - 1); const auto mapped = sliderState->slider->mapTo( sliderContainer, sliderState->slider->pos()); floatLabel->moveToLeft( mapped.x() + x + st::settingsScale.seekSize.width() / 2 - floatLabel->width() / 2, mapped.y() - floatLabel->height() - st::giveawayGiftCodeSliderFloatSkip); break; } } }, floatLabel->lifetime()); Ui::AddSkip(sliderContainer); Ui::AddDividerText( sliderContainer, rpl::conditional( rpl::duplicate(creditsValueType), tr::lng_giveaway_credits_quantity_about(), tr::lng_giveaway_quantity_about())); Ui::AddSkip(sliderContainer); sliderContainer->resizeToWidth(box->width()); }; { const auto channelsContainer = randomWrap->entity()->add( object_ptr(randomWrap)); Ui::AddSubsectionTitle( channelsContainer, tr::lng_giveaway_channels_title(), st::giveawayGiftCodeChannelsSubsectionPadding); struct ListState final { ListState(not_null p) : controller(p) { } PeerListContentDelegateSimple delegate; Giveaway::SelectedChannelsListController controller; }; const auto listState = box->lifetime().make_state(peer); listState->delegate.setContent(channelsContainer->add( object_ptr( channelsContainer, &listState->controller))); listState->controller.setDelegate(&listState->delegate); listState->controller.channelRemoved( ) | rpl::start_with_next([=](not_null peer) { auto &list = state->selectedToSubscribe; list.erase(ranges::remove(list, peer), end(list)); }, box->lifetime()); listState->controller.setTopStatus((peer->isMegagroup() ? tr::lng_giveaway_channels_this_group : tr::lng_giveaway_channels_this)( lt_count, state->sliderValue.value( ) | rpl::map([=](int v) -> float64 { return (prepaid && prepaid->boosts) ? prepaid->boosts : (state->apiOptions.giveawayBoostsPerPremium() * v); }))); using IconType = Settings::IconType; Settings::AddButtonWithIcon( channelsContainer, tr::lng_giveaway_channels_add(), st::giveawayGiftCodeChannelsAddButton, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive } )->setClickedCallback([=] { auto initBox = [=](not_null peersBox) { peersBox->setTitle(tr::lng_giveaway_channels_add()); peersBox->addButton(tr::lng_settings_save(), [=] { const auto selected = peersBox->collectSelectedRows(); state->selectedToSubscribe = selected; listState->controller.rebuild(selected); peersBox->closeBox(); }); peersBox->addButton(tr::lng_cancel(), [=] { peersBox->closeBox(); }); }; using Controller = Giveaway::MyChannelsListController; auto controller = std::make_unique( peer, box->uiShow(), state->selectedToSubscribe); controller->setCheckError(CreateErrorCallback( state->apiOptions.giveawayAddPeersMax(), tr::lng_giveaway_maximum_channels_error)); box->uiShow()->showBox( Box(std::move(controller), std::move(initBox)), Ui::LayerOption::KeepOther); }); const auto &padding = st::giveawayGiftCodeChannelsDividerPadding; Ui::AddSkip(channelsContainer, padding.top()); Ui::AddDividerText( channelsContainer, tr::lng_giveaway_channels_about()); Ui::AddSkip(channelsContainer, padding.bottom()); } const auto membersGroup = std::make_shared(); { const auto countriesContainer = randomWrap->entity()->add( object_ptr(randomWrap)); Ui::AddSubsectionTitle( countriesContainer, tr::lng_giveaway_users_title()); membersGroup->setValue(GiveawayType::AllMembers); auto subtitle = state->countriesValue.value( ) | rpl::map([=](const std::vector &list) { return list.empty() ? tr::lng_giveaway_users_from_all_countries() : (list.size() == 1) ? tr::lng_giveaway_users_from_one_country( lt_country, rpl::single(Countries::Instance().countryNameByISO2( list.front()))) : tr::lng_giveaway_users_from_countries( lt_count, rpl::single(list.size()) | tr::to_count()); }) | rpl::flatten_latest(); const auto showBox = [=] { auto done = [=](std::vector list) { state->countriesValue = std::move(list); }; auto error = CreateErrorCallback( state->apiOptions.giveawayCountriesMax(), tr::lng_giveaway_maximum_countries_error); box->uiShow()->showBox(Box( Ui::SelectCountriesBox, state->countriesValue.current(), std::move(done), std::move(error))); }; const auto createCallback = [=](GiveawayType type) { return [=] { const auto was = membersGroup->current(); membersGroup->setValue(type); const auto now = membersGroup->current(); if (was == now) { base::call_delayed( st::defaultRippleAnimation.hideDuration, box, showBox); } }; }; { const auto row = countriesContainer->add( object_ptr( box, GiveawayType::AllMembers, rpl::duplicate(subtitle), group)); row->addRadio(membersGroup); row->setClickedCallback(createCallback(GiveawayType::AllMembers)); } const auto row = countriesContainer->add( object_ptr( box, GiveawayType::OnlyNewMembers, std::move(subtitle), group)); row->addRadio(membersGroup); row->setClickedCallback(createCallback(GiveawayType::OnlyNewMembers)); Ui::AddSkip(countriesContainer); Ui::AddDividerText( countriesContainer, (group ? tr::lng_giveaway_users_about_group() : tr::lng_giveaway_users_about())); Ui::AddSkip(countriesContainer); } const auto addTerms = [=](not_null c) { auto terms = object_ptr( c, tr::lng_premium_gift_terms( lt_link, tr::lng_premium_gift_terms_link( ) | rpl::map([](const QString &t) { return Ui::Text::Link(t, 1); }), Ui::Text::WithEntities), st::boxDividerLabel); terms->setLink(1, std::make_shared([=] { box->closeBox(); Settings::ShowPremium(&peer->session(), QString()); })); c->add(std::move(terms)); }; const auto durationGroup = std::make_shared(0); durationGroup->setChangedCallback([=](int value) { state->chosenMonths = state->apiOptions.monthsFromPreset(value); }); const auto listOptionsRandom = randomWrap->entity()->add( object_ptr(box)); const auto listOptionsSpecific = contentWrap->entity()->add( object_ptr(box)); const auto rebuildListOptions = [=](GiveawayType type, int usersCount) { if (prepaid) { return; } while (listOptionsRandom->count()) { delete listOptionsRandom->widgetAt(0); } while (listOptionsSpecific->count()) { delete listOptionsSpecific->widgetAt(0); } const auto listOptions = isSpecificUsers() ? listOptionsSpecific : listOptionsRandom; if (type != GiveawayType::Credits) { Ui::AddSubsectionTitle( listOptions, tr::lng_giveaway_duration_title( lt_count, rpl::single(usersCount) | tr::to_count()), st::giveawayGiftCodeChannelsSubsectionPadding); Ui::Premium::AddGiftOptions( listOptions, durationGroup, state->apiOptions.options(usersCount), st::giveawayGiftCodeGiftOption, true); Ui::AddSkip(listOptions); auto termsContainer = object_ptr(listOptions); addTerms(termsContainer.data()); listOptions->add(object_ptr( listOptions, std::move(termsContainer), st::defaultBoxDividerLabelPadding)); Ui::AddSkip(listOptions); } box->verticalLayout()->resizeToWidth(box->width()); }; if (!prepaid) { rpl::combine( state->sliderValue.value(), state->typeValue.value() ) | rpl::start_with_next([=](int users, GiveawayType type) { typeGroup->setValue(type); rebuildListOptions( type, isSpecificUsers() ? state->selectedToAward.size() : users); }, box->lifetime()); } else { typeGroup->setValue(GiveawayType::Random); } { const auto additionalWrap = randomWrap->entity()->add( object_ptr(randomWrap)); const auto additionalToggle = additionalWrap->add( object_ptr( additionalWrap, tr::lng_giveaway_additional_prizes(), st::defaultSettingsButton)); const auto additionalInner = additionalWrap->add( object_ptr>( additionalWrap, object_ptr( additionalWrap, st::giveawayGiftCodeAdditionalField, Ui::InputField::Mode::SingleLine, tr::lng_giveaway_additional_prizes_ph()), st::giveawayGiftCodeAdditionalPaddingMin)); const auto additionalPadded = additionalInner->wrapped(); const auto additional = additionalInner->entity(); additionalInner->hide(anim::type::instant); additional->setMaxLength(kAdditionalPrizeLengthMax); const auto fillAdditionalPrizeValue = [=] { state->additionalPrize = additional->getLastText().trimmed(); }; additionalToggle->toggleOn(rpl::single(false))->toggledChanges( ) | rpl::start_with_next([=](bool toggled) { if (!toggled && Ui::InFocusChain(additional)) { additionalWrap->setFocus(); state->additionalPrize = QString(); } additionalInner->toggle(toggled, anim::type::normal); if (toggled) { additional->setFocusFast(); fillAdditionalPrizeValue(); } }, additionalInner->lifetime()); additionalInner->finishAnimating(); additional->changes() | rpl::filter([=] { return additionalInner->toggled(); }) | rpl::start_with_next( fillAdditionalPrizeValue, additional->lifetime()); Ui::AddSkip(additionalWrap); auto monthsValue = prepaid ? (rpl::single(prepaid->months) | rpl::type_erased()) : state->chosenMonths.value(); const auto usersCountByType = [=](GiveawayType type) { if (!isSpecificUsers()) { return state->sliderValue.value() | rpl::type_erased(); } return state->toAwardAmountChanged.events_starting_with_copy( rpl::empty ) | rpl::map([=] { return int(state->selectedToAward.size()); }) | rpl::type_erased(); }; auto usersCountValue = prepaid ? (rpl::single(prepaid->quantity) | rpl::type_erased()) : state->typeValue.value( ) | rpl::map(usersCountByType) | rpl::flatten_latest(); const auto additionalLabel = Ui::CreateChild( additionalInner, rpl::duplicate(usersCountValue) | rpl::map([](int count) { return QString::number(count); }), st::giveawayGiftCodeAdditionalLabel); additionalLabel->widthValue() | rpl::start_with_next([=](int width) { const auto min = st::giveawayGiftCodeAdditionalPaddingMin; const auto skip = st::giveawayGiftCodeAdditionalLabelSkip; const auto added = std::max(width + skip - min.left(), 0); const auto &field = st::giveawayGiftCodeAdditionalField; const auto top = field.textMargins.top(); additionalLabel->moveToLeft(min.right(), min.top() + top); additionalPadded->setPadding(min + QMargins(added, 0, 0, 0)); }, additionalLabel->lifetime()); auto additionalAbout = rpl::combine( state->additionalPrize.value(), std::move(monthsValue), std::move(usersCountValue) ) | rpl::map([=](QString prize, int months, int users) { const auto duration = ((months >= 12) ? tr::lng_premium_gift_duration_years : tr::lng_premium_gift_duration_months)( tr::now, lt_count, (months >= 12) ? (months / 12) : months); if (prize.isEmpty()) { return tr::lng_giveaway_prizes_just_premium( tr::now, lt_count, users, lt_duration, TextWithEntities{ duration }, Ui::Text::RichLangValue); } return tr::lng_giveaway_prizes_additional( tr::now, lt_count, users, lt_prize, TextWithEntities{ prize }, lt_duration, TextWithEntities{ duration }, Ui::Text::RichLangValue); }); auto creditsAdditionalAbout = rpl::combine( state->additionalPrize.value(), state->sliderValue.value(), creditsGroup->value() ) | rpl::map([=](QString prize, int users, int creditsIndex) { const auto credits = creditsOption(creditsIndex).credits; return prize.isEmpty() ? tr::lng_giveaway_prizes_just_credits( tr::now, lt_count, credits, Ui::Text::RichLangValue) : tr::lng_giveaway_prizes_additional_credits( tr::now, lt_count, users, lt_prize, TextWithEntities{ prize }, lt_amount, tr::lng_giveaway_prizes_additional_credits_amount( tr::now, lt_count, credits, Ui::Text::RichLangValue), Ui::Text::RichLangValue); }); auto creditsValueType = typeGroup->value( ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits); Ui::AddDividerText( additionalWrap, rpl::conditional( additionalToggle->toggledValue(), rpl::conditional( rpl::duplicate(creditsValueType), std::move(creditsAdditionalAbout), std::move(additionalAbout)), rpl::conditional( rpl::duplicate(creditsValueType), tr::lng_giveaway_additional_credits_about(), tr::lng_giveaway_additional_about() ) | rpl::map(Ui::Text::WithEntities))); Ui::AddSkip(additionalWrap); } { const auto dateContainer = randomWrap->entity()->add( object_ptr(randomWrap)); Ui::AddSubsectionTitle( dateContainer, tr::lng_giveaway_date_title(), st::giveawayGiftCodeChannelsSubsectionPadding); state->dateValue = ThreeDaysAfterToday().toSecsSinceEpoch(); const auto button = Settings::AddButtonWithLabel( dateContainer, tr::lng_giveaway_date(), state->dateValue.value() | rpl::map( base::unixtime::parse ) | rpl::map(Ui::FormatDateTime), st::defaultSettingsButton); button->setClickedCallback([=] { box->uiShow()->showBox(Box([=](not_null b) { Ui::ChooseDateTimeBox(b, { .title = tr::lng_giveaway_date_select(), .submit = tr::lng_settings_save(), .done = [=](TimeId time) { state->dateValue = time; b->closeBox(); }, .min = QDateTime::currentSecsSinceEpoch, .time = state->dateValue.current(), .max = [=] { return QDateTime::currentSecsSinceEpoch() + state->apiOptions.giveawayPeriodMax(); }, }); })); }); Ui::AddSkip(dateContainer); if (prepaid) { auto terms = object_ptr(dateContainer); terms->add(object_ptr( terms, (group ? tr::lng_giveaway_date_about_group : tr::lng_giveaway_date_about)( lt_count, state->sliderValue.value() | tr::to_count()), st::boxDividerLabel)); Ui::AddSkip(terms.data()); Ui::AddSkip(terms.data()); addTerms(terms.data()); dateContainer->add(object_ptr( dateContainer, std::move(terms), st::defaultBoxDividerLabelPadding)); Ui::AddSkip(dateContainer); } else { Ui::AddDividerText( dateContainer, (group ? tr::lng_giveaway_date_about_group : tr::lng_giveaway_date_about)( lt_count, state->sliderValue.value() | tr::to_count())); Ui::AddSkip(dateContainer); } } { const auto winnersWrap = randomWrap->entity()->add( object_ptr(randomWrap)); const auto winnersToggle = winnersWrap->add( object_ptr( winnersWrap, tr::lng_giveaway_show_winners(), st::defaultSettingsButton)); state->showWinners = winnersToggle->toggleOn( rpl::single(false) )->toggledValue(); Ui::AddSkip(winnersWrap); Ui::AddDividerText( winnersWrap, tr::lng_giveaway_show_winners_about()); } { using namespace Info::Statistics; const auto &stButton = st::startGiveawayBox; box->setStyle(stButton); auto button = object_ptr( box, rpl::never(), st::giveawayGiftCodeStartButton); AddLabelWithBadgeToButton( button, rpl::conditional( hideSpecificUsersOn(), tr::lng_giveaway_start(), tr::lng_giveaway_award()), (prepaid && prepaid->boosts) ? rpl::single(prepaid->boosts) | rpl::type_erased() : rpl::conditional( state->typeValue.value( ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits), creditsGroup->value() | rpl::map([=](int v) { return creditsOption(v).yearlyBoosts; }), rpl::combine( state->sliderValue.value(), hideSpecificUsersOn() ) | rpl::map([=](int value, bool random) -> int { return state->apiOptions.giveawayBoostsPerPremium() * (random ? value : int(state->selectedToAward.size())); })), state->confirmButtonBusy.value() | rpl::map(!rpl::mappers::_1)); { const auto loadingAnimation = InfiniteRadialAnimationWidget( button, st::giveawayGiftCodeStartButton.height / 2); AddChildToWidgetCenter(button.data(), loadingAnimation); loadingAnimation->showOn(state->confirmButtonBusy.value()); } button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); state->typeValue.value( ) | rpl::start_with_next([=, raw = button.data()] { raw->resizeToWidth(box->width() - stButton.buttonPadding.left() - stButton.buttonPadding.right()); }, button->lifetime()); button->setClickedCallback([=] { if (state->confirmButtonBusy.current()) { return; } const auto type = typeGroup->current(); const auto isSpecific = isSpecificUsers(); const auto isRandom = (type == GiveawayType::Random); const auto isCredits = (type == GiveawayType::Credits); if (!isSpecific && !isRandom && !isCredits) { return; } auto invoice = [&] { if (isPrepaidCredits) { return Payments::InvoicePremiumGiftCode{ .creditsAmount = prepaid->credits, .randomId = prepaid->id, .users = prepaid->quantity, }; } else if (isCredits) { const auto option = creditsOption( creditsGroup->current()); return Payments::InvoicePremiumGiftCode{ .currency = option.currency, .storeProduct = option.storeProduct, .creditsAmount = option.credits, .randomId = UniqueIdFromCreditsOption(option, peer), .amount = option.amount, .users = state->sliderValue.current(), }; } return state->apiOptions.invoice( isSpecific ? state->selectedToAward.size() : state->sliderValue.current(), prepaid ? prepaid->months : state->apiOptions.monthsFromPreset( durationGroup->current())); }(); if (isSpecific) { if (state->selectedToAward.empty()) { return; } invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ ranges::views::all( state->selectedToAward ) | ranges::views::transform([]( const not_null p) { return not_null{ p->asUser() }; }) | ranges::to_vector, peer->asChannel(), }; } else if (isRandom || isCredits || isPrepaidCredits) { invoice.purpose = Payments::InvoicePremiumGiftCodeGiveaway{ .boostPeer = peer->asChannel(), .additionalChannels = ranges::views::all( state->selectedToSubscribe ) | ranges::views::transform([]( const not_null p) { return not_null{ p->asChannel() }; }) | ranges::to_vector, .countries = state->countriesValue.current(), .additionalPrize = state->additionalPrize.current(), .untilDate = state->dateValue.current(), .onlyNewSubscribers = (membersGroup->current() == GiveawayType::OnlyNewMembers), .showWinners = state->showWinners.current(), }; } state->confirmButtonBusy = true; const auto show = box->uiShow(); const auto weak = Ui::MakeWeak(box.get()); const auto done = [=](Payments::CheckoutResult result) { const auto isPaid = result == Payments::CheckoutResult::Paid; if (result == Payments::CheckoutResult::Pending || isPaid) { if (const auto strong = weak.data()) { strong->window()->setFocus(); strong->closeBox(); } } if (isPaid) { reloadOnDone(); const auto filter = [=](const auto &...) { if (const auto window = weakWindow.get()) { window->showSection(Info::Boosts::Make(peer)); } return false; }; const auto group = peer->isMegagroup(); const auto title = isSpecific ? tr::lng_giveaway_awarded_title : tr::lng_giveaway_created_title; const auto body = isSpecific ? (group ? tr::lng_giveaway_awarded_body_group : tr::lng_giveaway_awarded_body) : (group ? tr::lng_giveaway_created_body_group : tr::lng_giveaway_created_body); show->showToast({ .text = Ui::Text::Bold( title(tr::now)).append('\n').append( body( tr::now, lt_link, Ui::Text::Link( tr::lng_giveaway_created_link( tr::now)), Ui::Text::WithEntities)), .filter = filter, .adaptive = true, .duration = kDoneTooltipDuration, }); } else if (weak) { state->confirmButtonBusy = false; } }; const auto startPrepaid = [=](Fn close) { if (!weak) { close(); return; } state->apiOptions.applyPrepaid( invoice, prepaid->id ) | rpl::start_with_error_done([=](const QString &error) { if (const auto window = weakWindow.get()) { window->uiShow()->showToast(error); close(); done(Payments::CheckoutResult::Cancelled); } }, [=] { close(); done(Payments::CheckoutResult::Paid); }, box->lifetime()); }; if (prepaid) { const auto cancel = [=](Fn close) { if (weak) { state->confirmButtonBusy = false; } close(); }; show->show(Ui::MakeConfirmBox({ .text = tr::lng_giveaway_start_sure(tr::now), .confirmed = startPrepaid, .cancelled = cancel, })); } else { Payments::CheckoutProcess::Start(std::move(invoice), done); } }); box->addButton(std::move(button)); } state->typeValue.force_assign(GiveawayType::Random); std::move( showFinished ) | rpl::take(1) | rpl::start_with_next([=] { if (!loading->toggled()) { return; } const auto done = [=] { state->lifetimeApi.destroy(); loading->toggle(false, anim::type::instant); state->confirmButtonBusy = false; fillSliderContainer(); if (!prepaid) { state->chosenMonths = state->apiOptions.monthsFromPreset(0); } fillCreditsTypeWrap(); fillCreditsOptions(); rebuildListOptions(state->typeValue.current(), 1); contentWrap->toggle(true, anim::type::instant); contentWrap->resizeToWidth(box->width()); }; const auto receivedOptions = [=] { state->lifetimeApi.destroy(); state->lifetimeApi = state->apiCreditsOptions.request( ) | rpl::start_with_error_done([=](const QString &error) { box->uiShow()->showToast(error); box->closeBox(); }, done); }; if (prepaid) { return done(); } state->lifetimeApi = state->apiOptions.request( ) | rpl::start_with_error_done([=](const QString &error) { box->uiShow()->showToast(error); box->closeBox(); }, receivedOptions); }, box->lifetime()); }