create_giveaway_box.cpp 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "info/channel_statistics/boosts/create_giveaway_box.h"
  8. #include "api/api_credits.h"
  9. #include "api/api_premium.h"
  10. #include "base/call_delayed.h"
  11. #include "base/unixtime.h"
  12. #include "countries/countries_instance.h"
  13. #include "data/data_peer.h"
  14. #include "info/channel_statistics/boosts/giveaway/boost_badge.h"
  15. #include "info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h"
  16. #include "info/channel_statistics/boosts/giveaway/giveaway_type_row.h"
  17. #include "info/channel_statistics/boosts/giveaway/select_countries_box.h"
  18. #include "info/channel_statistics/boosts/info_boosts_widget.h"
  19. #include "info/info_controller.h"
  20. #include "info/info_memento.h"
  21. #include "info/statistics/info_statistics_list_controllers.h"
  22. #include "lang/lang_keys.h"
  23. #include "main/main_session.h"
  24. #include "payments/payments_checkout_process.h" // Payments::CheckoutProcess
  25. #include "payments/payments_form.h" // Payments::InvoicePremiumGiftCode
  26. #include "settings/settings_common.h"
  27. #include "settings/settings_premium.h" // Settings::ShowPremium
  28. #include "ui/boxes/choose_date_time.h"
  29. #include "ui/boxes/confirm_box.h"
  30. #include "ui/effects/credits_graphics.h"
  31. #include "ui/effects/premium_graphics.h"
  32. #include "ui/effects/premium_top_bar.h"
  33. #include "ui/layers/generic_box.h"
  34. #include "ui/painter.h"
  35. #include "ui/rect.h"
  36. #include "ui/text/format_values.h"
  37. #include "ui/text/text_utilities.h"
  38. #include "ui/toast/toast.h"
  39. #include "ui/vertical_list.h"
  40. #include "ui/widgets/checkbox.h"
  41. #include "ui/widgets/continuous_sliders.h"
  42. #include "ui/widgets/fields/input_field.h"
  43. #include "ui/widgets/labels.h"
  44. #include "ui/wrap/slide_wrap.h"
  45. #include "ui/ui_utility.h"
  46. #include "styles/style_color_indices.h"
  47. #include "styles/style_credits.h"
  48. #include "styles/style_giveaway.h"
  49. #include "styles/style_info.h"
  50. #include "styles/style_layers.h"
  51. #include "styles/style_premium.h"
  52. #include "styles/style_settings.h"
  53. #include "styles/style_statistics.h"
  54. #include <xxhash.h> // XXH64.
  55. namespace {
  56. constexpr auto kDoneTooltipDuration = 5 * crl::time(1000);
  57. constexpr auto kAdditionalPrizeLengthMax = 128;
  58. [[nodiscard]] QDateTime ThreeDaysAfterToday() {
  59. auto dateNow = QDateTime::currentDateTime();
  60. dateNow = dateNow.addDays(3);
  61. auto timeNow = dateNow.time();
  62. while (timeNow.minute() % 5) {
  63. timeNow = timeNow.addSecs(60);
  64. }
  65. dateNow.setTime(timeNow);
  66. return dateNow;
  67. }
  68. [[nodiscard]] uint64 UniqueIdFromCreditsOption(
  69. const Data::CreditsGiveawayOption &d,
  70. not_null<PeerData*> peer) {
  71. const auto string = QString::number(d.credits)
  72. + d.storeProduct
  73. + d.currency
  74. + QString::number(d.amount)
  75. + QString::number(peer->id.value)
  76. + QString::number(peer->session().uniqueId());
  77. return XXH64(string.data(), string.size() * sizeof(ushort), 0);
  78. }
  79. [[nodiscard]] Fn<bool(int)> CreateErrorCallback(
  80. int max,
  81. tr::phrase<lngtag_count> phrase) {
  82. return [=](int count) {
  83. const auto error = (count >= max);
  84. if (error) {
  85. Ui::Toast::Show(phrase(tr::now, lt_count, max));
  86. }
  87. return error;
  88. };
  89. }
  90. [[nodiscard]] QWidget *FindFirstShadowInBox(not_null<Ui::BoxContent*> box) {
  91. for (const auto &child : box->children()) {
  92. if (child && child->isWidgetType()) {
  93. const auto w = static_cast<QWidget*>(child);
  94. if (w->height() == st::lineWidth) {
  95. return w;
  96. }
  97. }
  98. }
  99. return nullptr;
  100. }
  101. void AddPremiumTopBarWithDefaultTitleBar(
  102. not_null<Ui::GenericBox*> box,
  103. rpl::producer<> showFinished,
  104. rpl::producer<QString> titleText,
  105. rpl::producer<TextWithEntities> subtitleText) {
  106. struct State final {
  107. Ui::Animations::Simple animation;
  108. Ui::Text::String title;
  109. Ui::RpWidget close;
  110. };
  111. const auto state = box->lifetime().make_state<State>();
  112. box->setNoContentMargin(true);
  113. std::move(
  114. titleText
  115. ) | rpl::start_with_next([=](const QString &s) {
  116. state->title.setText(st::startGiveawayBox.title.style, s);
  117. }, box->lifetime());
  118. const auto hPadding = rect::m::sum::h(st::boxRowPadding);
  119. const auto titlePaintContext = Ui::Text::PaintContext{
  120. .position = st::boxTitlePosition,
  121. .outerWidth = (st::boxWideWidth - hPadding),
  122. .availableWidth = (st::boxWideWidth - hPadding),
  123. };
  124. const auto isCloseBarShown = [=] { return box->scrollTop() > 0; };
  125. const auto closeTopBar = box->setPinnedToTopContent(
  126. object_ptr<Ui::RpWidget>(box));
  127. closeTopBar->resize(box->width(), st::boxTitleHeight);
  128. closeTopBar->paintRequest(
  129. ) | rpl::start_with_next([=] {
  130. auto p = Painter(closeTopBar);
  131. const auto r = closeTopBar->rect();
  132. const auto radius = st::boxRadius;
  133. const auto progress = state->animation.value(isCloseBarShown()
  134. ? 1.
  135. : 0.);
  136. const auto resultRect = r + QMargins{ 0, 0, 0, radius };
  137. {
  138. auto hq = PainterHighQualityEnabler(p);
  139. if (progress < 1.) {
  140. auto path = QPainterPath();
  141. path.addRect(resultRect);
  142. path.addRect(
  143. st::boxRowPadding.left(),
  144. 0,
  145. resultRect.width() - hPadding,
  146. resultRect.height());
  147. p.setClipPath(path);
  148. p.setPen(Qt::NoPen);
  149. p.setBrush(st::boxDividerBg);
  150. p.drawRoundedRect(resultRect, radius, radius);
  151. }
  152. if (progress > 0.) {
  153. p.setOpacity(progress);
  154. p.setClipping(false);
  155. p.setPen(Qt::NoPen);
  156. p.setBrush(st::boxBg);
  157. p.drawRoundedRect(resultRect, radius, radius);
  158. p.setPen(st::startGiveawayBox.title.textFg);
  159. p.setBrush(Qt::NoBrush);
  160. state->title.draw(p, titlePaintContext);
  161. }
  162. }
  163. }, closeTopBar->lifetime());
  164. {
  165. const auto close = Ui::CreateChild<Ui::IconButton>(
  166. closeTopBar.get(),
  167. st::startGiveawayBoxTitleClose);
  168. close->setClickedCallback([=] { box->closeBox(); });
  169. closeTopBar->widthValue(
  170. ) | rpl::start_with_next([=](int w) {
  171. const auto &pos = st::giveawayGiftCodeCoverClosePosition;
  172. close->moveToRight(pos.x(), pos.y());
  173. }, box->lifetime());
  174. close->show();
  175. }
  176. const auto bar = Ui::CreateChild<Ui::Premium::TopBar>(
  177. box.get(),
  178. st::startGiveawayCover,
  179. Ui::Premium::TopBarDescriptor{
  180. .clickContextOther = nullptr,
  181. .title = tr::lng_giveaway_new_title(),
  182. .about = std::move(subtitleText),
  183. .light = true,
  184. .optimizeMinistars = false,
  185. });
  186. bar->setAttribute(Qt::WA_TransparentForMouseEvents);
  187. box->addRow(
  188. object_ptr<Ui::BoxContentDivider>(
  189. box.get(),
  190. st::giveawayGiftCodeTopHeight
  191. - st::boxTitleHeight
  192. + st::boxDividerHeight
  193. + st::defaultVerticalListSkip,
  194. st::boxDividerBg,
  195. RectPart::Bottom),
  196. {});
  197. bar->setPaused(true);
  198. bar->setRoundEdges(false);
  199. bar->setMaximumHeight(st::giveawayGiftCodeTopHeight);
  200. bar->setMinimumHeight(st::infoLayerTopBarHeight);
  201. bar->resize(bar->width(), bar->maximumHeight());
  202. box->widthValue(
  203. ) | rpl::start_with_next([=](int w) {
  204. bar->resizeToWidth(w - hPadding);
  205. bar->moveToLeft(st::boxRowPadding.left(), bar->y());
  206. }, box->lifetime());
  207. std::move(
  208. showFinished
  209. ) | rpl::take(1) | rpl::start_with_next([=] {
  210. closeTopBar->raise();
  211. if (const auto shadow = FindFirstShadowInBox(box)) {
  212. bar->stackUnder(shadow);
  213. }
  214. bar->setPaused(false);
  215. box->scrolls(
  216. ) | rpl::map(isCloseBarShown) | rpl::distinct_until_changed(
  217. ) | rpl::start_with_next([=](bool showBar) {
  218. state->animation.stop();
  219. state->animation.start(
  220. [=] { closeTopBar->update(); },
  221. showBar ? 0. : 1.,
  222. showBar ? 1. : 0.,
  223. st::slideWrapDuration);
  224. }, box->lifetime());
  225. box->scrolls(
  226. ) | rpl::start_with_next([=] {
  227. bar->moveToLeft(bar->x(), -box->scrollTop());
  228. }, box->lifetime());
  229. }, box->lifetime());
  230. bar->show();
  231. }
  232. } // namespace
  233. void CreateGiveawayBox(
  234. not_null<Ui::GenericBox*> box,
  235. not_null<Window::SessionNavigation*> navigation,
  236. not_null<PeerData*> peer,
  237. Fn<void()> reloadOnDone,
  238. std::optional<Data::BoostPrepaidGiveaway> prepaid) {
  239. box->setWidth(st::boxWideWidth);
  240. const auto weakWindow = base::make_weak(navigation->parentController());
  241. using GiveawayType = Giveaway::GiveawayTypeRow::Type;
  242. using GiveawayGroup = Ui::RadioenumGroup<GiveawayType>;
  243. using CreditsGroup = Ui::RadioenumGroup<int>;
  244. struct State final {
  245. State(not_null<PeerData*> p) : apiOptions(p), apiCreditsOptions(p) {
  246. }
  247. Api::PremiumGiftCodeOptions apiOptions;
  248. Api::CreditsGiveawayOptions apiCreditsOptions;
  249. rpl::lifetime lifetimeApi;
  250. std::vector<not_null<PeerData*>> selectedToAward;
  251. rpl::event_stream<> toAwardAmountChanged;
  252. std::vector<not_null<PeerData*>> selectedToSubscribe;
  253. rpl::variable<GiveawayType> typeValue;
  254. rpl::variable<int> sliderValue;
  255. rpl::variable<TimeId> dateValue;
  256. rpl::variable<std::vector<QString>> countriesValue;
  257. rpl::variable<QString> additionalPrize;
  258. rpl::variable<int> chosenMonths;
  259. rpl::variable<bool> showWinners;
  260. rpl::variable<bool> confirmButtonBusy = true;
  261. };
  262. const auto group = peer->isMegagroup();
  263. const auto state = box->lifetime().make_state<State>(peer);
  264. const auto typeGroup = std::make_shared<GiveawayGroup>();
  265. const auto creditsGroup = std::make_shared<CreditsGroup>();
  266. const auto isPrepaidCredits = (prepaid && prepaid->credits);
  267. const auto isSpecificUsers = [=] {
  268. return !state->selectedToAward.empty();
  269. };
  270. const auto hideSpecificUsersOn = [=] {
  271. return rpl::combine(
  272. state->typeValue.value(),
  273. state->toAwardAmountChanged.events_starting_with(
  274. rpl::empty_value()) | rpl::type_erased()
  275. ) | rpl::map([=](GiveawayType type, auto) {
  276. return (type == GiveawayType::Credits) || !isSpecificUsers();
  277. });
  278. };
  279. auto showFinished = Ui::BoxShowFinishes(box);
  280. AddPremiumTopBarWithDefaultTitleBar(
  281. box,
  282. rpl::duplicate(showFinished),
  283. rpl::conditional(
  284. hideSpecificUsersOn(),
  285. tr::lng_giveaway_start(),
  286. tr::lng_giveaway_award()),
  287. rpl::conditional(
  288. isPrepaidCredits
  289. ? rpl::single(true) | rpl::type_erased()
  290. : state->typeValue.value() | rpl::map(
  291. rpl::mappers::_1 == GiveawayType::Credits),
  292. (peer->isMegagroup()
  293. ? tr::lng_giveaway_credits_new_about_group()
  294. : tr::lng_giveaway_credits_new_about()),
  295. (peer->isMegagroup()
  296. ? tr::lng_giveaway_new_about_group()
  297. : tr::lng_giveaway_new_about())
  298. ) | rpl::map(Ui::Text::RichLangValue));
  299. {
  300. const auto &padding = st::giveawayGiftCodeCoverDividerPadding;
  301. Ui::AddSkip(box->verticalLayout(), padding.bottom());
  302. }
  303. const auto loading = box->addRow(
  304. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  305. box,
  306. object_ptr<Ui::VerticalLayout>(box)));
  307. {
  308. loading->toggle(true, anim::type::instant);
  309. const auto container = loading->entity();
  310. Ui::AddSkip(container);
  311. Ui::AddSkip(container);
  312. container->add(
  313. object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
  314. box,
  315. object_ptr<Ui::FlatLabel>(
  316. box,
  317. tr::lng_contacts_loading(),
  318. st::giveawayLoadingLabel)));
  319. Ui::AddSkip(container);
  320. Ui::AddSkip(container);
  321. }
  322. const auto contentWrap = box->verticalLayout()->add(
  323. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  324. box,
  325. object_ptr<Ui::VerticalLayout>(box)));
  326. contentWrap->toggle(false, anim::type::instant);
  327. if (prepaid) {
  328. contentWrap->entity()->add(
  329. object_ptr<Giveaway::GiveawayTypeRow>(
  330. box,
  331. prepaid->credits
  332. ? GiveawayType::PrepaidCredits
  333. : GiveawayType::Prepaid,
  334. prepaid->credits ? st::colorIndexOrange : prepaid->id,
  335. tr::lng_boosts_prepaid_giveaway_single(),
  336. prepaid->credits
  337. ? tr::lng_boosts_prepaid_giveaway_credits_status(
  338. lt_count,
  339. rpl::single(prepaid->quantity) | tr::to_count(),
  340. lt_amount,
  341. tr::lng_prize_credits_amount(
  342. lt_count_decimal,
  343. rpl::single(prepaid->credits) | tr::to_count()))
  344. : tr::lng_boosts_prepaid_giveaway_status(
  345. lt_count,
  346. rpl::single(prepaid->quantity) | tr::to_count(),
  347. lt_duration,
  348. tr::lng_premium_gift_duration_months(
  349. lt_count,
  350. rpl::single(prepaid->months) | tr::to_count())),
  351. QImage())
  352. )->setAttribute(Qt::WA_TransparentForMouseEvents);
  353. }
  354. if (!prepaid) {
  355. const auto row = contentWrap->entity()->add(
  356. object_ptr<Giveaway::GiveawayTypeRow>(
  357. box,
  358. GiveawayType::Random,
  359. state->toAwardAmountChanged.events_starting_with(
  360. rpl::empty_value()
  361. ) | rpl::map([=] {
  362. const auto &selected = state->selectedToAward;
  363. return selected.empty()
  364. ? tr::lng_giveaway_create_subtitle()
  365. : (selected.size() == 1)
  366. ? rpl::single(selected.front()->name())
  367. : tr::lng_giveaway_award_chosen(
  368. lt_count,
  369. rpl::single(selected.size()) | tr::to_count());
  370. }) | rpl::flatten_latest(),
  371. group));
  372. row->addRadio(typeGroup);
  373. row->setClickedCallback([=] {
  374. auto initBox = [=](not_null<PeerListBox*> peersBox) {
  375. peersBox->setTitle(tr::lng_giveaway_award_option());
  376. auto aboveOwned = object_ptr<Ui::VerticalLayout>(peersBox);
  377. const auto above = aboveOwned.data();
  378. peersBox->peerListSetAboveWidget(std::move(aboveOwned));
  379. Ui::AddSkip(above);
  380. const auto buttonRandom = above->add(
  381. object_ptr<Ui::SettingsButton>(
  382. peersBox,
  383. tr::lng_giveaway_random_button(),
  384. st::settingsButtonLightNoIcon));
  385. buttonRandom->setClickedCallback([=] {
  386. state->selectedToAward.clear();
  387. state->toAwardAmountChanged.fire({});
  388. state->typeValue.force_assign(GiveawayType::Random);
  389. peersBox->closeBox();
  390. });
  391. Ui::AddSkip(above);
  392. peersBox->addButton(tr::lng_settings_save(), [=] {
  393. state->selectedToAward = peersBox->collectSelectedRows();
  394. state->toAwardAmountChanged.fire({});
  395. state->typeValue.force_assign(GiveawayType::Random);
  396. peersBox->closeBox();
  397. });
  398. peersBox->addButton(tr::lng_cancel(), [=] {
  399. peersBox->closeBox();
  400. });
  401. };
  402. using Controller = Giveaway::AwardMembersListController;
  403. auto listController = std::make_unique<Controller>(
  404. navigation,
  405. peer,
  406. state->selectedToAward);
  407. listController->setCheckError(CreateErrorCallback(
  408. state->apiOptions.giveawayAddPeersMax(),
  409. tr::lng_giveaway_maximum_users_error));
  410. box->uiShow()->showBox(
  411. Box<PeerListBox>(
  412. std::move(listController),
  413. std::move(initBox)),
  414. Ui::LayerOption::KeepOther);
  415. });
  416. }
  417. const auto creditsOption = [=](int index) {
  418. const auto options = state->apiCreditsOptions.options();
  419. return (index >= 0 && index < options.size())
  420. ? options[index]
  421. : Data::CreditsGiveawayOption();
  422. };
  423. const auto creditsOptionWinners = [=](int index) {
  424. const auto winners = creditsOption(index).winners;
  425. return ranges::views::all(
  426. winners
  427. ) | ranges::views::transform([](const auto &w) {
  428. return w.users;
  429. }) | ranges::to_vector;
  430. };
  431. const auto creditsTypeWrap = contentWrap->entity()->add(
  432. object_ptr<Ui::VerticalLayout>(contentWrap->entity()));
  433. const auto fillCreditsTypeWrap = [=] {
  434. if (state->apiCreditsOptions.options().empty()) {
  435. return;
  436. }
  437. const auto row = creditsTypeWrap->add(
  438. object_ptr<Giveaway::GiveawayTypeRow>(
  439. box,
  440. GiveawayType::Credits,
  441. st::colorIndexOrange,
  442. tr::lng_credits_summary_title(),
  443. tr::lng_giveaway_create_subtitle(),
  444. QImage()));
  445. row->addRadio(typeGroup);
  446. row->setClickedCallback([=] {
  447. state->typeValue.force_assign(GiveawayType::Credits);
  448. });
  449. };
  450. {
  451. const auto &padding = st::giveawayGiftCodeTypeDividerPadding;
  452. Ui::AddSkip(contentWrap->entity(), padding.top());
  453. Ui::AddDivider(contentWrap->entity());
  454. Ui::AddSkip(contentWrap->entity(), padding.bottom());
  455. }
  456. const auto randomWrap = contentWrap->entity()->add(
  457. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  458. contentWrap,
  459. object_ptr<Ui::VerticalLayout>(box)));
  460. state->typeValue.value(
  461. ) | rpl::start_with_next([=](GiveawayType type) {
  462. randomWrap->toggle(!isSpecificUsers(), anim::type::instant);
  463. }, randomWrap->lifetime());
  464. randomWrap->toggleOn(hideSpecificUsersOn(), anim::type::instant);
  465. const auto randomCreditsWrap = randomWrap->entity()->add(
  466. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  467. contentWrap,
  468. object_ptr<Ui::VerticalLayout>(box)));
  469. randomCreditsWrap->toggleOn(
  470. state->typeValue.value(
  471. ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits),
  472. anim::type::instant);
  473. const auto fillCreditsOptions = [=] {
  474. randomCreditsWrap->entity()->clear();
  475. const auto &st = st::giveawayTypeListItem;
  476. const auto &stButton = st::defaultSettingsButton;
  477. const auto &stStatus = st::defaultTextStyle;
  478. const auto buttonInnerSkip = st.height - stButton.height;
  479. const auto options = state->apiCreditsOptions.options();
  480. const auto content = randomCreditsWrap->entity();
  481. const auto title = Ui::AddSubsectionTitle(
  482. content,
  483. tr::lng_giveaway_credits_options_title());
  484. const auto rightLabel = Ui::CreateChild<Ui::FlatLabel>(
  485. content,
  486. st::giveawayGiftCodeQuantitySubtitle);
  487. rightLabel->show();
  488. rpl::combine(
  489. tr::lng_giveaway_quantity(
  490. lt_count,
  491. creditsGroup->value() | rpl::map([=](int i) -> float64 {
  492. return creditsOption(i).yearlyBoosts;
  493. })),
  494. title->positionValue(),
  495. content->geometryValue()
  496. ) | rpl::start_with_next([=](QString s, const QPoint &p, QRect) {
  497. rightLabel->setText(std::move(s));
  498. rightLabel->moveToRight(st::boxRowPadding.right(), p.y());
  499. }, rightLabel->lifetime());
  500. const auto buttonHeight = st.height;
  501. const auto minCredits = 0;
  502. struct State final {
  503. rpl::variable<bool> isExtended = false;
  504. };
  505. const auto creditsState = content->lifetime().make_state<State>();
  506. for (auto i = 0; i < options.size(); i++) {
  507. const auto &option = options[i];
  508. if (option.credits < minCredits) {
  509. continue;
  510. }
  511. struct State final {
  512. std::optional<Ui::Text::String> text;
  513. QString status;
  514. bool hasStatus = false;
  515. };
  516. const auto buttonWrap = content->add(
  517. object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  518. content,
  519. object_ptr<Ui::SettingsButton>(
  520. content,
  521. rpl::never<QString>(),
  522. stButton)));
  523. const auto button = buttonWrap->entity();
  524. button->setPaddingOverride({ 0, buttonInnerSkip, 0, 0 });
  525. const auto buttonState = button->lifetime().make_state<State>();
  526. buttonState->text.emplace(
  527. st.nameStyle,
  528. tr::lng_credits_summary_options_credits(
  529. tr::now,
  530. lt_count_decimal,
  531. option.credits));
  532. buttonState->status = tr::lng_giveaway_credits_option_status(
  533. tr::now,
  534. lt_count_decimal,
  535. option.credits);
  536. const auto price = Ui::CreateChild<Ui::FlatLabel>(
  537. button,
  538. Ui::FillAmountAndCurrency(option.amount, option.currency),
  539. st::creditsTopupPrice);
  540. const auto inner = Ui::CreateChild<Ui::RpWidget>(button);
  541. const auto stars = Ui::GenerateStars(
  542. st.nameStyle.font->height,
  543. (i + 1));
  544. const auto textLeft = st.photoPosition.x()
  545. + (st.nameStyle.font->spacew * 2)
  546. + (stars.width() / style::DevicePixelRatio());
  547. state->sliderValue.value(
  548. ) | rpl::start_with_next([=](int users) {
  549. const auto option = creditsOption(i);
  550. buttonState->hasStatus = false;
  551. for (const auto &winner : option.winners) {
  552. if (winner.users == users) {
  553. auto status = tr::lng_giveaway_credits_option_status(
  554. tr::now,
  555. lt_count_decimal,
  556. winner.perUserStars);
  557. buttonState->status = std::move(status);
  558. buttonState->hasStatus = true;
  559. inner->update();
  560. return;
  561. }
  562. }
  563. inner->update();
  564. }, button->lifetime());
  565. inner->paintRequest(
  566. ) | rpl::start_with_next([=](const QRect &rect) {
  567. auto p = QPainter(inner);
  568. const auto namey = buttonState->hasStatus
  569. ? st.namePosition.y()
  570. : (buttonHeight - stStatus.font->height) / 2;
  571. p.drawImage(st.photoPosition.x(), namey, stars);
  572. p.setPen(st.nameFg);
  573. buttonState->text->draw(p, {
  574. .position = QPoint(textLeft, namey),
  575. .availableWidth = inner->width() - textLeft,
  576. .elisionLines = 1,
  577. });
  578. if (buttonState->hasStatus) {
  579. p.setFont(stStatus.font);
  580. p.setPen(st.statusFg);
  581. p.setBrush(Qt::NoBrush);
  582. p.drawText(
  583. st.photoPosition.x(),
  584. st.statusPosition.y() + stStatus.font->ascent,
  585. buttonState->status);
  586. }
  587. }, inner->lifetime());
  588. button->widthValue(
  589. ) | rpl::start_with_next([=](int width) {
  590. price->moveToRight(
  591. st::boxRowPadding.right(),
  592. (buttonHeight - price->height()) / 2);
  593. inner->moveToLeft(0, 0);
  594. inner->resize(
  595. width
  596. - price->width()
  597. - st::boxRowPadding.right()
  598. - st::boxRowPadding.left() / 2,
  599. buttonHeight);
  600. }, button->lifetime());
  601. {
  602. const auto &st = st::defaultCheckbox;
  603. const auto radio = Ui::CreateChild<Ui::Radioenum<int>>(
  604. button,
  605. creditsGroup,
  606. i,
  607. QString(),
  608. st);
  609. radio->moveToLeft(
  610. st::boxRowPadding.left(),
  611. (buttonHeight - radio->checkRect().height()) / 2);
  612. radio->setAttribute(Qt::WA_TransparentForMouseEvents);
  613. radio->show();
  614. }
  615. button->setClickedCallback([=] {
  616. creditsGroup->setValue(i);
  617. });
  618. if (option.isDefault) {
  619. creditsGroup->setValue(i);
  620. }
  621. buttonWrap->toggle(
  622. (!option.isExtended) || option.isDefault,
  623. anim::type::instant);
  624. if (option.isExtended) {
  625. buttonWrap->toggleOn(creditsState->isExtended.value());
  626. }
  627. Ui::ToggleChildrenVisibility(button, true);
  628. }
  629. {
  630. Ui::AddSkip(content, st::settingsButton.padding.top());
  631. const auto showMoreWrap = Info::Statistics::AddShowMoreButton(
  632. content,
  633. tr::lng_stories_show_more());
  634. showMoreWrap->toggle(true, anim::type::instant);
  635. showMoreWrap->entity()->setClickedCallback([=] {
  636. showMoreWrap->toggle(false, anim::type::instant);
  637. creditsState->isExtended = true;
  638. });
  639. }
  640. Ui::AddSkip(content);
  641. Ui::AddDividerText(content, tr::lng_giveaway_credits_options_about());
  642. Ui::AddSkip(content);
  643. };
  644. const auto sliderContainerWrap = randomWrap->entity()->add(
  645. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  646. randomWrap,
  647. object_ptr<Ui::VerticalLayout>(randomWrap)));
  648. const auto sliderContainer = sliderContainerWrap->entity();
  649. sliderContainerWrap->toggle(true, anim::type::instant);
  650. const auto fillSliderContainer = [=] {
  651. const auto availablePresets = state->apiOptions.availablePresets();
  652. const auto creditsOptions = state->apiCreditsOptions.options();
  653. if (prepaid) {
  654. state->sliderValue = prepaid->quantity;
  655. return;
  656. }
  657. if (availablePresets.empty()
  658. && (creditsOptions.empty()
  659. || creditsOptions.front().winners.empty())) {
  660. return;
  661. }
  662. state->sliderValue = availablePresets.empty()
  663. ? creditsOptions.front().winners.front().users
  664. : availablePresets.front();
  665. auto creditsValueType = typeGroup->value(
  666. ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits);
  667. const auto title = Ui::AddSubsectionTitle(
  668. sliderContainer,
  669. rpl::conditional(
  670. rpl::duplicate(creditsValueType),
  671. tr::lng_giveaway_credits_quantity_title(),
  672. tr::lng_giveaway_quantity_title()));
  673. const auto rightLabel = Ui::CreateChild<Ui::FlatLabel>(
  674. sliderContainer,
  675. st::giveawayGiftCodeQuantitySubtitle);
  676. rightLabel->show();
  677. rpl::duplicate(
  678. creditsValueType
  679. ) | rpl::start_with_next([=](bool isCredits) {
  680. rightLabel->setVisible(!isCredits);
  681. }, rightLabel->lifetime());
  682. const auto floatLabel = Ui::CreateChild<Ui::FlatLabel>(
  683. sliderContainer,
  684. st::giveawayGiftCodeQuantityFloat);
  685. floatLabel->show();
  686. rpl::combine(
  687. tr::lng_giveaway_quantity(
  688. lt_count,
  689. state->sliderValue.value(
  690. ) | rpl::map([=](int v) -> float64 {
  691. return state->apiOptions.giveawayBoostsPerPremium() * v;
  692. })),
  693. title->positionValue(),
  694. sliderContainer->geometryValue()
  695. ) | rpl::start_with_next([=](QString s, const QPoint &p, QRect) {
  696. rightLabel->setText(std::move(s));
  697. rightLabel->moveToRight(st::boxRowPadding.right(), p.y());
  698. }, rightLabel->lifetime());
  699. const auto &padding = st::giveawayGiftCodeSliderPadding;
  700. Ui::AddSkip(sliderContainer, padding.top());
  701. const auto sliderParent = sliderContainer->add(
  702. object_ptr<Ui::VerticalLayout>(sliderContainer),
  703. st::boxRowPadding);
  704. struct State final {
  705. Ui::MediaSliderWheelless *slider = nullptr;
  706. };
  707. const auto sliderState = sliderParent->lifetime().make_state<State>();
  708. Ui::AddSkip(sliderContainer, padding.bottom());
  709. rpl::combine(
  710. rpl::duplicate(creditsValueType),
  711. creditsGroup->value()
  712. ) | rpl::start_with_next([=](bool isCredits, int value) {
  713. while (sliderParent->count()) {
  714. delete sliderParent->widgetAt(0);
  715. }
  716. sliderState->slider = sliderParent->add(
  717. object_ptr<Ui::MediaSliderWheelless>(
  718. sliderContainer,
  719. st::settingsScale));
  720. sliderState->slider->resize(
  721. sliderState->slider->width(),
  722. st::settingsScale.seekSize.height());
  723. const auto &values = isCredits
  724. ? creditsOptionWinners(value)
  725. : availablePresets;
  726. const auto resultValue = [&] {
  727. const auto sliderValue = state->sliderValue.current();
  728. return ranges::contains(values, sliderValue)
  729. ? sliderValue
  730. : values.front();
  731. }();
  732. state->sliderValue.force_assign(resultValue);
  733. if (values.size() <= 1) {
  734. sliderContainerWrap->toggle(false, anim::type::instant);
  735. return;
  736. } else {
  737. sliderContainerWrap->toggle(true, anim::type::instant);
  738. }
  739. sliderState->slider->setPseudoDiscrete(
  740. values.size(),
  741. [=](int index) { return values[index]; },
  742. resultValue,
  743. [=](int boosts) { state->sliderValue = boosts; },
  744. [](int) {});
  745. }, sliderParent->lifetime());
  746. rpl::combine(
  747. rpl::duplicate(creditsValueType),
  748. creditsGroup->value(),
  749. state->sliderValue.value()
  750. ) | rpl::start_with_next([=](
  751. bool isCredits,
  752. int credits,
  753. int boosts) {
  754. floatLabel->setText(QString::number(boosts));
  755. if (!sliderState->slider) {
  756. return;
  757. }
  758. const auto &values = isCredits
  759. ? creditsOptionWinners(credits)
  760. : availablePresets;
  761. const auto count = values.size();
  762. if (count <= 1) {
  763. return;
  764. }
  765. const auto sliderWidth = sliderState->slider->width()
  766. - st::settingsScale.seekSize.width();
  767. for (auto i = 0; i < count; i++) {
  768. if ((i + 1 == count || values[i + 1] > boosts)
  769. && values[i] <= boosts) {
  770. const auto x = (sliderWidth * i) / (count - 1);
  771. const auto mapped = sliderState->slider->mapTo(
  772. sliderContainer,
  773. sliderState->slider->pos());
  774. floatLabel->moveToLeft(
  775. mapped.x()
  776. + x
  777. + st::settingsScale.seekSize.width() / 2
  778. - floatLabel->width() / 2,
  779. mapped.y()
  780. - floatLabel->height()
  781. - st::giveawayGiftCodeSliderFloatSkip);
  782. break;
  783. }
  784. }
  785. }, floatLabel->lifetime());
  786. Ui::AddSkip(sliderContainer);
  787. Ui::AddDividerText(
  788. sliderContainer,
  789. rpl::conditional(
  790. rpl::duplicate(creditsValueType),
  791. tr::lng_giveaway_credits_quantity_about(),
  792. tr::lng_giveaway_quantity_about()));
  793. Ui::AddSkip(sliderContainer);
  794. sliderContainer->resizeToWidth(box->width());
  795. };
  796. {
  797. const auto channelsContainer = randomWrap->entity()->add(
  798. object_ptr<Ui::VerticalLayout>(randomWrap));
  799. Ui::AddSubsectionTitle(
  800. channelsContainer,
  801. tr::lng_giveaway_channels_title(),
  802. st::giveawayGiftCodeChannelsSubsectionPadding);
  803. struct ListState final {
  804. ListState(not_null<PeerData*> p) : controller(p) {
  805. }
  806. PeerListContentDelegateSimple delegate;
  807. Giveaway::SelectedChannelsListController controller;
  808. };
  809. const auto listState = box->lifetime().make_state<ListState>(peer);
  810. listState->delegate.setContent(channelsContainer->add(
  811. object_ptr<PeerListContent>(
  812. channelsContainer,
  813. &listState->controller)));
  814. listState->controller.setDelegate(&listState->delegate);
  815. listState->controller.channelRemoved(
  816. ) | rpl::start_with_next([=](not_null<PeerData*> peer) {
  817. auto &list = state->selectedToSubscribe;
  818. list.erase(ranges::remove(list, peer), end(list));
  819. }, box->lifetime());
  820. listState->controller.setTopStatus((peer->isMegagroup()
  821. ? tr::lng_giveaway_channels_this_group
  822. : tr::lng_giveaway_channels_this)(
  823. lt_count,
  824. state->sliderValue.value(
  825. ) | rpl::map([=](int v) -> float64 {
  826. return (prepaid && prepaid->boosts)
  827. ? prepaid->boosts
  828. : (state->apiOptions.giveawayBoostsPerPremium() * v);
  829. })));
  830. using IconType = Settings::IconType;
  831. Settings::AddButtonWithIcon(
  832. channelsContainer,
  833. tr::lng_giveaway_channels_add(),
  834. st::giveawayGiftCodeChannelsAddButton,
  835. { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }
  836. )->setClickedCallback([=] {
  837. auto initBox = [=](not_null<PeerListBox*> peersBox) {
  838. peersBox->setTitle(tr::lng_giveaway_channels_add());
  839. peersBox->addButton(tr::lng_settings_save(), [=] {
  840. const auto selected = peersBox->collectSelectedRows();
  841. state->selectedToSubscribe = selected;
  842. listState->controller.rebuild(selected);
  843. peersBox->closeBox();
  844. });
  845. peersBox->addButton(tr::lng_cancel(), [=] {
  846. peersBox->closeBox();
  847. });
  848. };
  849. using Controller = Giveaway::MyChannelsListController;
  850. auto controller = std::make_unique<Controller>(
  851. peer,
  852. box->uiShow(),
  853. state->selectedToSubscribe);
  854. controller->setCheckError(CreateErrorCallback(
  855. state->apiOptions.giveawayAddPeersMax(),
  856. tr::lng_giveaway_maximum_channels_error));
  857. box->uiShow()->showBox(
  858. Box<PeerListBox>(std::move(controller), std::move(initBox)),
  859. Ui::LayerOption::KeepOther);
  860. });
  861. const auto &padding = st::giveawayGiftCodeChannelsDividerPadding;
  862. Ui::AddSkip(channelsContainer, padding.top());
  863. Ui::AddDividerText(
  864. channelsContainer,
  865. tr::lng_giveaway_channels_about());
  866. Ui::AddSkip(channelsContainer, padding.bottom());
  867. }
  868. const auto membersGroup = std::make_shared<GiveawayGroup>();
  869. {
  870. const auto countriesContainer = randomWrap->entity()->add(
  871. object_ptr<Ui::VerticalLayout>(randomWrap));
  872. Ui::AddSubsectionTitle(
  873. countriesContainer,
  874. tr::lng_giveaway_users_title());
  875. membersGroup->setValue(GiveawayType::AllMembers);
  876. auto subtitle = state->countriesValue.value(
  877. ) | rpl::map([=](const std::vector<QString> &list) {
  878. return list.empty()
  879. ? tr::lng_giveaway_users_from_all_countries()
  880. : (list.size() == 1)
  881. ? tr::lng_giveaway_users_from_one_country(
  882. lt_country,
  883. rpl::single(Countries::Instance().countryNameByISO2(
  884. list.front())))
  885. : tr::lng_giveaway_users_from_countries(
  886. lt_count,
  887. rpl::single(list.size()) | tr::to_count());
  888. }) | rpl::flatten_latest();
  889. const auto showBox = [=] {
  890. auto done = [=](std::vector<QString> list) {
  891. state->countriesValue = std::move(list);
  892. };
  893. auto error = CreateErrorCallback(
  894. state->apiOptions.giveawayCountriesMax(),
  895. tr::lng_giveaway_maximum_countries_error);
  896. box->uiShow()->showBox(Box(
  897. Ui::SelectCountriesBox,
  898. state->countriesValue.current(),
  899. std::move(done),
  900. std::move(error)));
  901. };
  902. const auto createCallback = [=](GiveawayType type) {
  903. return [=] {
  904. const auto was = membersGroup->current();
  905. membersGroup->setValue(type);
  906. const auto now = membersGroup->current();
  907. if (was == now) {
  908. base::call_delayed(
  909. st::defaultRippleAnimation.hideDuration,
  910. box,
  911. showBox);
  912. }
  913. };
  914. };
  915. {
  916. const auto row = countriesContainer->add(
  917. object_ptr<Giveaway::GiveawayTypeRow>(
  918. box,
  919. GiveawayType::AllMembers,
  920. rpl::duplicate(subtitle),
  921. group));
  922. row->addRadio(membersGroup);
  923. row->setClickedCallback(createCallback(GiveawayType::AllMembers));
  924. }
  925. const auto row = countriesContainer->add(
  926. object_ptr<Giveaway::GiveawayTypeRow>(
  927. box,
  928. GiveawayType::OnlyNewMembers,
  929. std::move(subtitle),
  930. group));
  931. row->addRadio(membersGroup);
  932. row->setClickedCallback(createCallback(GiveawayType::OnlyNewMembers));
  933. Ui::AddSkip(countriesContainer);
  934. Ui::AddDividerText(
  935. countriesContainer,
  936. (group
  937. ? tr::lng_giveaway_users_about_group()
  938. : tr::lng_giveaway_users_about()));
  939. Ui::AddSkip(countriesContainer);
  940. }
  941. const auto addTerms = [=](not_null<Ui::VerticalLayout*> c) {
  942. auto terms = object_ptr<Ui::FlatLabel>(
  943. c,
  944. tr::lng_premium_gift_terms(
  945. lt_link,
  946. tr::lng_premium_gift_terms_link(
  947. ) | rpl::map([](const QString &t) {
  948. return Ui::Text::Link(t, 1);
  949. }),
  950. Ui::Text::WithEntities),
  951. st::boxDividerLabel);
  952. terms->setLink(1, std::make_shared<LambdaClickHandler>([=] {
  953. box->closeBox();
  954. Settings::ShowPremium(&peer->session(), QString());
  955. }));
  956. c->add(std::move(terms));
  957. };
  958. const auto durationGroup = std::make_shared<Ui::RadiobuttonGroup>(0);
  959. durationGroup->setChangedCallback([=](int value) {
  960. state->chosenMonths = state->apiOptions.monthsFromPreset(value);
  961. });
  962. const auto listOptionsRandom = randomWrap->entity()->add(
  963. object_ptr<Ui::VerticalLayout>(box));
  964. const auto listOptionsSpecific = contentWrap->entity()->add(
  965. object_ptr<Ui::VerticalLayout>(box));
  966. const auto rebuildListOptions = [=](GiveawayType type, int usersCount) {
  967. if (prepaid) {
  968. return;
  969. }
  970. while (listOptionsRandom->count()) {
  971. delete listOptionsRandom->widgetAt(0);
  972. }
  973. while (listOptionsSpecific->count()) {
  974. delete listOptionsSpecific->widgetAt(0);
  975. }
  976. const auto listOptions = isSpecificUsers()
  977. ? listOptionsSpecific
  978. : listOptionsRandom;
  979. if (type != GiveawayType::Credits) {
  980. Ui::AddSubsectionTitle(
  981. listOptions,
  982. tr::lng_giveaway_duration_title(
  983. lt_count,
  984. rpl::single(usersCount) | tr::to_count()),
  985. st::giveawayGiftCodeChannelsSubsectionPadding);
  986. Ui::Premium::AddGiftOptions(
  987. listOptions,
  988. durationGroup,
  989. state->apiOptions.options(usersCount),
  990. st::giveawayGiftCodeGiftOption,
  991. true);
  992. Ui::AddSkip(listOptions);
  993. auto termsContainer = object_ptr<Ui::VerticalLayout>(listOptions);
  994. addTerms(termsContainer.data());
  995. listOptions->add(object_ptr<Ui::DividerLabel>(
  996. listOptions,
  997. std::move(termsContainer),
  998. st::defaultBoxDividerLabelPadding));
  999. Ui::AddSkip(listOptions);
  1000. }
  1001. box->verticalLayout()->resizeToWidth(box->width());
  1002. };
  1003. if (!prepaid) {
  1004. rpl::combine(
  1005. state->sliderValue.value(),
  1006. state->typeValue.value()
  1007. ) | rpl::start_with_next([=](int users, GiveawayType type) {
  1008. typeGroup->setValue(type);
  1009. rebuildListOptions(
  1010. type,
  1011. isSpecificUsers() ? state->selectedToAward.size() : users);
  1012. }, box->lifetime());
  1013. } else {
  1014. typeGroup->setValue(GiveawayType::Random);
  1015. }
  1016. {
  1017. const auto additionalWrap = randomWrap->entity()->add(
  1018. object_ptr<Ui::VerticalLayout>(randomWrap));
  1019. const auto additionalToggle = additionalWrap->add(
  1020. object_ptr<Ui::SettingsButton>(
  1021. additionalWrap,
  1022. tr::lng_giveaway_additional_prizes(),
  1023. st::defaultSettingsButton));
  1024. const auto additionalInner = additionalWrap->add(
  1025. object_ptr<Ui::SlideWrap<Ui::InputField>>(
  1026. additionalWrap,
  1027. object_ptr<Ui::InputField>(
  1028. additionalWrap,
  1029. st::giveawayGiftCodeAdditionalField,
  1030. Ui::InputField::Mode::SingleLine,
  1031. tr::lng_giveaway_additional_prizes_ph()),
  1032. st::giveawayGiftCodeAdditionalPaddingMin));
  1033. const auto additionalPadded = additionalInner->wrapped();
  1034. const auto additional = additionalInner->entity();
  1035. additionalInner->hide(anim::type::instant);
  1036. additional->setMaxLength(kAdditionalPrizeLengthMax);
  1037. const auto fillAdditionalPrizeValue = [=] {
  1038. state->additionalPrize = additional->getLastText().trimmed();
  1039. };
  1040. additionalToggle->toggleOn(rpl::single(false))->toggledChanges(
  1041. ) | rpl::start_with_next([=](bool toggled) {
  1042. if (!toggled && Ui::InFocusChain(additional)) {
  1043. additionalWrap->setFocus();
  1044. state->additionalPrize = QString();
  1045. }
  1046. additionalInner->toggle(toggled, anim::type::normal);
  1047. if (toggled) {
  1048. additional->setFocusFast();
  1049. fillAdditionalPrizeValue();
  1050. }
  1051. }, additionalInner->lifetime());
  1052. additionalInner->finishAnimating();
  1053. additional->changes() | rpl::filter([=] {
  1054. return additionalInner->toggled();
  1055. }) | rpl::start_with_next(
  1056. fillAdditionalPrizeValue,
  1057. additional->lifetime());
  1058. Ui::AddSkip(additionalWrap);
  1059. auto monthsValue = prepaid
  1060. ? (rpl::single(prepaid->months) | rpl::type_erased())
  1061. : state->chosenMonths.value();
  1062. const auto usersCountByType = [=](GiveawayType type) {
  1063. if (!isSpecificUsers()) {
  1064. return state->sliderValue.value() | rpl::type_erased();
  1065. }
  1066. return state->toAwardAmountChanged.events_starting_with_copy(
  1067. rpl::empty
  1068. ) | rpl::map([=] {
  1069. return int(state->selectedToAward.size());
  1070. }) | rpl::type_erased();
  1071. };
  1072. auto usersCountValue = prepaid
  1073. ? (rpl::single(prepaid->quantity) | rpl::type_erased())
  1074. : state->typeValue.value(
  1075. ) | rpl::map(usersCountByType) | rpl::flatten_latest();
  1076. const auto additionalLabel = Ui::CreateChild<Ui::FlatLabel>(
  1077. additionalInner,
  1078. rpl::duplicate(usersCountValue) | rpl::map([](int count) {
  1079. return QString::number(count);
  1080. }),
  1081. st::giveawayGiftCodeAdditionalLabel);
  1082. additionalLabel->widthValue() | rpl::start_with_next([=](int width) {
  1083. const auto min = st::giveawayGiftCodeAdditionalPaddingMin;
  1084. const auto skip = st::giveawayGiftCodeAdditionalLabelSkip;
  1085. const auto added = std::max(width + skip - min.left(), 0);
  1086. const auto &field = st::giveawayGiftCodeAdditionalField;
  1087. const auto top = field.textMargins.top();
  1088. additionalLabel->moveToLeft(min.right(), min.top() + top);
  1089. additionalPadded->setPadding(min + QMargins(added, 0, 0, 0));
  1090. }, additionalLabel->lifetime());
  1091. auto additionalAbout = rpl::combine(
  1092. state->additionalPrize.value(),
  1093. std::move(monthsValue),
  1094. std::move(usersCountValue)
  1095. ) | rpl::map([=](QString prize, int months, int users) {
  1096. const auto duration = ((months >= 12)
  1097. ? tr::lng_premium_gift_duration_years
  1098. : tr::lng_premium_gift_duration_months)(
  1099. tr::now,
  1100. lt_count,
  1101. (months >= 12) ? (months / 12) : months);
  1102. if (prize.isEmpty()) {
  1103. return tr::lng_giveaway_prizes_just_premium(
  1104. tr::now,
  1105. lt_count,
  1106. users,
  1107. lt_duration,
  1108. TextWithEntities{ duration },
  1109. Ui::Text::RichLangValue);
  1110. }
  1111. return tr::lng_giveaway_prizes_additional(
  1112. tr::now,
  1113. lt_count,
  1114. users,
  1115. lt_prize,
  1116. TextWithEntities{ prize },
  1117. lt_duration,
  1118. TextWithEntities{ duration },
  1119. Ui::Text::RichLangValue);
  1120. });
  1121. auto creditsAdditionalAbout = rpl::combine(
  1122. state->additionalPrize.value(),
  1123. state->sliderValue.value(),
  1124. creditsGroup->value()
  1125. ) | rpl::map([=](QString prize, int users, int creditsIndex) {
  1126. const auto credits = creditsOption(creditsIndex).credits;
  1127. return prize.isEmpty()
  1128. ? tr::lng_giveaway_prizes_just_credits(
  1129. tr::now,
  1130. lt_count,
  1131. credits,
  1132. Ui::Text::RichLangValue)
  1133. : tr::lng_giveaway_prizes_additional_credits(
  1134. tr::now,
  1135. lt_count,
  1136. users,
  1137. lt_prize,
  1138. TextWithEntities{ prize },
  1139. lt_amount,
  1140. tr::lng_giveaway_prizes_additional_credits_amount(
  1141. tr::now,
  1142. lt_count,
  1143. credits,
  1144. Ui::Text::RichLangValue),
  1145. Ui::Text::RichLangValue);
  1146. });
  1147. auto creditsValueType = typeGroup->value(
  1148. ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits);
  1149. Ui::AddDividerText(
  1150. additionalWrap,
  1151. rpl::conditional(
  1152. additionalToggle->toggledValue(),
  1153. rpl::conditional(
  1154. rpl::duplicate(creditsValueType),
  1155. std::move(creditsAdditionalAbout),
  1156. std::move(additionalAbout)),
  1157. rpl::conditional(
  1158. rpl::duplicate(creditsValueType),
  1159. tr::lng_giveaway_additional_credits_about(),
  1160. tr::lng_giveaway_additional_about()
  1161. ) | rpl::map(Ui::Text::WithEntities)));
  1162. Ui::AddSkip(additionalWrap);
  1163. }
  1164. {
  1165. const auto dateContainer = randomWrap->entity()->add(
  1166. object_ptr<Ui::VerticalLayout>(randomWrap));
  1167. Ui::AddSubsectionTitle(
  1168. dateContainer,
  1169. tr::lng_giveaway_date_title(),
  1170. st::giveawayGiftCodeChannelsSubsectionPadding);
  1171. state->dateValue = ThreeDaysAfterToday().toSecsSinceEpoch();
  1172. const auto button = Settings::AddButtonWithLabel(
  1173. dateContainer,
  1174. tr::lng_giveaway_date(),
  1175. state->dateValue.value() | rpl::map(
  1176. base::unixtime::parse
  1177. ) | rpl::map(Ui::FormatDateTime),
  1178. st::defaultSettingsButton);
  1179. button->setClickedCallback([=] {
  1180. box->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> b) {
  1181. Ui::ChooseDateTimeBox(b, {
  1182. .title = tr::lng_giveaway_date_select(),
  1183. .submit = tr::lng_settings_save(),
  1184. .done = [=](TimeId time) {
  1185. state->dateValue = time;
  1186. b->closeBox();
  1187. },
  1188. .min = QDateTime::currentSecsSinceEpoch,
  1189. .time = state->dateValue.current(),
  1190. .max = [=] {
  1191. return QDateTime::currentSecsSinceEpoch()
  1192. + state->apiOptions.giveawayPeriodMax();
  1193. },
  1194. });
  1195. }));
  1196. });
  1197. Ui::AddSkip(dateContainer);
  1198. if (prepaid) {
  1199. auto terms = object_ptr<Ui::VerticalLayout>(dateContainer);
  1200. terms->add(object_ptr<Ui::FlatLabel>(
  1201. terms,
  1202. (group
  1203. ? tr::lng_giveaway_date_about_group
  1204. : tr::lng_giveaway_date_about)(
  1205. lt_count,
  1206. state->sliderValue.value() | tr::to_count()),
  1207. st::boxDividerLabel));
  1208. Ui::AddSkip(terms.data());
  1209. Ui::AddSkip(terms.data());
  1210. addTerms(terms.data());
  1211. dateContainer->add(object_ptr<Ui::DividerLabel>(
  1212. dateContainer,
  1213. std::move(terms),
  1214. st::defaultBoxDividerLabelPadding));
  1215. Ui::AddSkip(dateContainer);
  1216. } else {
  1217. Ui::AddDividerText(
  1218. dateContainer,
  1219. (group
  1220. ? tr::lng_giveaway_date_about_group
  1221. : tr::lng_giveaway_date_about)(
  1222. lt_count,
  1223. state->sliderValue.value() | tr::to_count()));
  1224. Ui::AddSkip(dateContainer);
  1225. }
  1226. }
  1227. {
  1228. const auto winnersWrap = randomWrap->entity()->add(
  1229. object_ptr<Ui::VerticalLayout>(randomWrap));
  1230. const auto winnersToggle = winnersWrap->add(
  1231. object_ptr<Ui::SettingsButton>(
  1232. winnersWrap,
  1233. tr::lng_giveaway_show_winners(),
  1234. st::defaultSettingsButton));
  1235. state->showWinners = winnersToggle->toggleOn(
  1236. rpl::single(false)
  1237. )->toggledValue();
  1238. Ui::AddSkip(winnersWrap);
  1239. Ui::AddDividerText(
  1240. winnersWrap,
  1241. tr::lng_giveaway_show_winners_about());
  1242. }
  1243. {
  1244. using namespace Info::Statistics;
  1245. const auto &stButton = st::startGiveawayBox;
  1246. box->setStyle(stButton);
  1247. auto button = object_ptr<Ui::RoundButton>(
  1248. box,
  1249. rpl::never<QString>(),
  1250. st::giveawayGiftCodeStartButton);
  1251. AddLabelWithBadgeToButton(
  1252. button,
  1253. rpl::conditional(
  1254. hideSpecificUsersOn(),
  1255. tr::lng_giveaway_start(),
  1256. tr::lng_giveaway_award()),
  1257. (prepaid && prepaid->boosts)
  1258. ? rpl::single(prepaid->boosts) | rpl::type_erased()
  1259. : rpl::conditional(
  1260. state->typeValue.value(
  1261. ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits),
  1262. creditsGroup->value() | rpl::map([=](int v) {
  1263. return creditsOption(v).yearlyBoosts;
  1264. }),
  1265. rpl::combine(
  1266. state->sliderValue.value(),
  1267. hideSpecificUsersOn()
  1268. ) | rpl::map([=](int value, bool random) -> int {
  1269. return state->apiOptions.giveawayBoostsPerPremium()
  1270. * (random
  1271. ? value
  1272. : int(state->selectedToAward.size()));
  1273. })),
  1274. state->confirmButtonBusy.value() | rpl::map(!rpl::mappers::_1));
  1275. {
  1276. const auto loadingAnimation = InfiniteRadialAnimationWidget(
  1277. button,
  1278. st::giveawayGiftCodeStartButton.height / 2);
  1279. AddChildToWidgetCenter(button.data(), loadingAnimation);
  1280. loadingAnimation->showOn(state->confirmButtonBusy.value());
  1281. }
  1282. button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
  1283. state->typeValue.value(
  1284. ) | rpl::start_with_next([=, raw = button.data()] {
  1285. raw->resizeToWidth(box->width()
  1286. - stButton.buttonPadding.left()
  1287. - stButton.buttonPadding.right());
  1288. }, button->lifetime());
  1289. button->setClickedCallback([=] {
  1290. if (state->confirmButtonBusy.current()) {
  1291. return;
  1292. }
  1293. const auto type = typeGroup->current();
  1294. const auto isSpecific = isSpecificUsers();
  1295. const auto isRandom = (type == GiveawayType::Random);
  1296. const auto isCredits = (type == GiveawayType::Credits);
  1297. if (!isSpecific && !isRandom && !isCredits) {
  1298. return;
  1299. }
  1300. auto invoice = [&] {
  1301. if (isPrepaidCredits) {
  1302. return Payments::InvoicePremiumGiftCode{
  1303. .giveawayCredits = prepaid->credits,
  1304. .randomId = prepaid->id,
  1305. .users = prepaid->quantity,
  1306. };
  1307. } else if (isCredits) {
  1308. const auto option = creditsOption(
  1309. creditsGroup->current());
  1310. return Payments::InvoicePremiumGiftCode{
  1311. .currency = option.currency,
  1312. .storeProduct = option.storeProduct,
  1313. .giveawayCredits = option.credits,
  1314. .randomId = UniqueIdFromCreditsOption(option, peer),
  1315. .amount = option.amount,
  1316. .users = state->sliderValue.current(),
  1317. };
  1318. }
  1319. return state->apiOptions.invoice(
  1320. isSpecific
  1321. ? state->selectedToAward.size()
  1322. : state->sliderValue.current(),
  1323. prepaid
  1324. ? prepaid->months
  1325. : state->apiOptions.monthsFromPreset(
  1326. durationGroup->current()));
  1327. }();
  1328. if (isSpecific) {
  1329. if (state->selectedToAward.empty()) {
  1330. return;
  1331. }
  1332. invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
  1333. ranges::views::all(
  1334. state->selectedToAward
  1335. ) | ranges::views::transform([](
  1336. const not_null<PeerData*> p) {
  1337. return not_null{ p->asUser() };
  1338. }) | ranges::to_vector,
  1339. peer->asChannel(),
  1340. };
  1341. } else if (isRandom || isCredits || isPrepaidCredits) {
  1342. invoice.purpose = Payments::InvoicePremiumGiftCodeGiveaway{
  1343. .boostPeer = peer->asChannel(),
  1344. .additionalChannels = ranges::views::all(
  1345. state->selectedToSubscribe
  1346. ) | ranges::views::transform([](
  1347. const not_null<PeerData*> p) {
  1348. return not_null{ p->asChannel() };
  1349. }) | ranges::to_vector,
  1350. .countries = state->countriesValue.current(),
  1351. .additionalPrize = state->additionalPrize.current(),
  1352. .untilDate = state->dateValue.current(),
  1353. .onlyNewSubscribers = (membersGroup->current()
  1354. == GiveawayType::OnlyNewMembers),
  1355. .showWinners = state->showWinners.current(),
  1356. };
  1357. }
  1358. state->confirmButtonBusy = true;
  1359. const auto show = box->uiShow();
  1360. const auto weak = Ui::MakeWeak(box.get());
  1361. const auto done = [=](Payments::CheckoutResult result) {
  1362. const auto isPaid = result == Payments::CheckoutResult::Paid;
  1363. if (result == Payments::CheckoutResult::Pending || isPaid) {
  1364. if (const auto strong = weak.data()) {
  1365. strong->window()->setFocus();
  1366. strong->closeBox();
  1367. }
  1368. }
  1369. if (isPaid) {
  1370. reloadOnDone();
  1371. const auto filter = [=](const auto &...) {
  1372. if (const auto window = weakWindow.get()) {
  1373. window->showSection(Info::Boosts::Make(peer));
  1374. }
  1375. return false;
  1376. };
  1377. const auto group = peer->isMegagroup();
  1378. const auto title = isSpecific
  1379. ? tr::lng_giveaway_awarded_title
  1380. : tr::lng_giveaway_created_title;
  1381. const auto body = isSpecific
  1382. ? (group
  1383. ? tr::lng_giveaway_awarded_body_group
  1384. : tr::lng_giveaway_awarded_body)
  1385. : (group
  1386. ? tr::lng_giveaway_created_body_group
  1387. : tr::lng_giveaway_created_body);
  1388. show->showToast({
  1389. .text = Ui::Text::Bold(
  1390. title(tr::now)).append('\n').append(
  1391. body(
  1392. tr::now,
  1393. lt_link,
  1394. Ui::Text::Link(
  1395. tr::lng_giveaway_created_link(
  1396. tr::now)),
  1397. Ui::Text::WithEntities)),
  1398. .filter = filter,
  1399. .adaptive = true,
  1400. .duration = kDoneTooltipDuration,
  1401. });
  1402. } else if (weak) {
  1403. state->confirmButtonBusy = false;
  1404. }
  1405. };
  1406. const auto startPrepaid = [=](Fn<void()> close) {
  1407. if (!weak) {
  1408. close();
  1409. return;
  1410. }
  1411. state->apiOptions.applyPrepaid(
  1412. invoice,
  1413. prepaid->id
  1414. ) | rpl::start_with_error_done([=](const QString &error) {
  1415. if (const auto window = weakWindow.get()) {
  1416. window->uiShow()->showToast(error);
  1417. close();
  1418. done(Payments::CheckoutResult::Cancelled);
  1419. }
  1420. }, [=] {
  1421. close();
  1422. done(Payments::CheckoutResult::Paid);
  1423. }, box->lifetime());
  1424. };
  1425. if (prepaid) {
  1426. const auto cancel = [=](Fn<void()> close) {
  1427. if (weak) {
  1428. state->confirmButtonBusy = false;
  1429. }
  1430. close();
  1431. };
  1432. show->show(Ui::MakeConfirmBox({
  1433. .text = tr::lng_giveaway_start_sure(tr::now),
  1434. .confirmed = startPrepaid,
  1435. .cancelled = cancel,
  1436. }));
  1437. } else {
  1438. Payments::CheckoutProcess::Start(std::move(invoice), done);
  1439. }
  1440. });
  1441. box->addButton(std::move(button));
  1442. }
  1443. state->typeValue.force_assign(GiveawayType::Random);
  1444. std::move(
  1445. showFinished
  1446. ) | rpl::take(1) | rpl::start_with_next([=] {
  1447. if (!loading->toggled()) {
  1448. return;
  1449. }
  1450. const auto done = [=] {
  1451. state->lifetimeApi.destroy();
  1452. loading->toggle(false, anim::type::instant);
  1453. state->confirmButtonBusy = false;
  1454. fillSliderContainer();
  1455. if (!prepaid) {
  1456. state->chosenMonths = state->apiOptions.monthsFromPreset(0);
  1457. }
  1458. fillCreditsTypeWrap();
  1459. fillCreditsOptions();
  1460. rebuildListOptions(state->typeValue.current(), 1);
  1461. contentWrap->toggle(true, anim::type::instant);
  1462. contentWrap->resizeToWidth(box->width());
  1463. };
  1464. const auto receivedOptions = [=] {
  1465. state->lifetimeApi.destroy();
  1466. state->lifetimeApi = state->apiCreditsOptions.request(
  1467. ) | rpl::start_with_error_done([=](const QString &error) {
  1468. box->uiShow()->showToast(error);
  1469. box->closeBox();
  1470. }, done);
  1471. };
  1472. if (prepaid) {
  1473. return done();
  1474. }
  1475. state->lifetimeApi = state->apiOptions.request(
  1476. ) | rpl::start_with_error_done([=](const QString &error) {
  1477. box->uiShow()->showToast(error);
  1478. box->closeBox();
  1479. }, receivedOptions);
  1480. }, box->lifetime());
  1481. }