/* 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 "settings/settings_credits_graphics.h" #include "api/api_credits.h" #include "api/api_earn.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/gift_premium_box.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "core/click_handler_types.h" // UrlClickHandler #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" // HistoryServicePaymentRefund. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "payments/payments_checkout_process.h" #include "payments/payments_form.h" #include "settings/settings_common_session.h" #include "statistics/widgets/chart_header_widget.h" #include "ui/controls/userpic_button.h" #include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" #include "ui/image/image_prepare.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/buttons.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/fields/number_input.h" #include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" #include "styles/style_channel_earn.h" #include "styles/style_chat.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. #include namespace Settings { namespace { [[nodiscard]] uint64 UniqueIdFromOption( const Data::CreditTopupOption &d) { const auto string = QString::number(d.credits) + d.product + d.currency + QString::number(d.amount); return XXH64(string.data(), string.size() * sizeof(ushort), 0); } [[nodiscard]] int WithdrawalMin(not_null session) { const auto key = u"stars_revenue_withdrawal_min"_q; return session->appConfig().get(key, 1000); } class Balance final : public Ui::RpWidget , public Ui::AbstractTooltipShower { public: using Ui::RpWidget::RpWidget; void setBalance(uint64 balance) { _balance = balance; _tooltip = Lang::FormatCountDecimal(balance); } void enterEventHook(QEnterEvent *e) override { if (_balance >= 10'000) { Ui::Tooltip::Show(1000, this); } } void leaveEventHook(QEvent *e) override { Ui::Tooltip::Hide(); } QString tooltipText() const override { return _tooltip; } QPoint tooltipPos() const override { return QCursor::pos(); } bool tooltipWindowActive() const override { return Ui::AppInFocus() && Ui::InFocusChain(window()); } private: QString _tooltip; uint64 _balance = 0; }; void AddViewMediaHandler( not_null thumb, not_null controller, const Data::CreditsHistoryEntry &e) { if (e.extended.empty()) { return; } thumb->setCursor(style::cur_pointer); struct State { ~State() { if (item) { item->destroy(); } } HistoryItem *item = nullptr; bool pressed = false; bool over = false; }; const auto state = thumb->lifetime().make_state(); const auto session = &controller->session(); const auto owner = &session->data(); const auto peerId = e.barePeerId ? PeerId(e.barePeerId) : session->userPeerId(); const auto history = owner->history(session->user()); state->item = history->makeMessage({ .id = history->nextNonHistoryEntryId(), .flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry, .from = peerId, .date = base::unixtime::serialize(e.date), }, TextWithEntities(), MTP_messageMediaEmpty()); auto fake = std::vector>(); fake.reserve(e.extended.size()); for (const auto &item : e.extended) { if (item.type == Data::CreditsHistoryMediaType::Photo) { fake.push_back(std::make_unique( state->item, owner->photo(item.id), false)); // spoiler } else { fake.push_back(std::make_unique( state->item, owner->document(item.id), true, // skipPremiumEffect false, // spoiler 0)); // ttlSeconds } } state->item->overrideMedia(std::make_unique( state->item, Data::Invoice{ .amount = uint64(std::abs(int64(e.credits))), .currency = Ui::kCreditsCurrency, .extendedMedia = std::move(fake), .isPaidMedia = true, })); const auto showMedia = crl::guard(controller, [=] { if (const auto media = state->item->media()) { if (const auto invoice = media->invoice()) { if (!invoice->extendedMedia.empty()) { const auto first = invoice->extendedMedia[0].get(); if (const auto photo = first->photo()) { controller->openPhoto(photo, { .id = state->item->fullId(), }); } else if (const auto document = first->document()) { controller->openDocument(document, true, { .id = state->item->fullId(), }); } } } } }); thumb->events() | rpl::start_with_next([=](not_null e) { if (e->type() == QEvent::MouseButtonPress) { const auto mouse = static_cast(e.get()); if (mouse->button() == Qt::LeftButton) { state->over = true; state->pressed = true; } } else if (e->type() == QEvent::MouseButtonRelease && state->over && state->pressed) { showMedia(); } else if (e->type() == QEvent::Enter) { state->over = true; } else if (e->type() == QEvent::Leave) { state->over = false; } }, thumb->lifetime()); } } // namespace void FillCreditOptions( not_null controller, not_null container, int minimumCredits, Fn paid) { const auto options = container->add( object_ptr>( container, object_ptr(container))); const auto content = options->entity(); Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); const auto singleStarWidth = Ui::GenerateStars( st::creditsTopupButton.height, 1).width() / style::DevicePixelRatio(); const auto fill = [=](Data::CreditTopupOptions options) { while (content->count()) { delete content->widgetAt(0); } Ui::AddSubsectionTitle( content, tr::lng_credits_summary_options_subtitle()); const auto &st = st::creditsTopupButton; const auto diffBetweenTextAndStar = st.padding.left() - st.iconLeft - singleStarWidth; const auto buttonHeight = st.height + rect::m::sum::v(st.padding); const auto minCredits = (!options.empty() && (minimumCredits > options.back().credits)) ? 0 : minimumCredits; for (auto i = 0; i < options.size(); i++) { const auto &option = options[i]; if (option.credits < minCredits) { continue; } const auto button = content->add(object_ptr( content, rpl::never(), st)); const auto text = button->lifetime().make_state( st.style, tr::lng_credits_summary_options_credits( 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.height, (i + 1)); inner->paintRequest( ) | rpl::start_with_next([=](const QRect &rect) { auto p = QPainter(inner); p.drawImage(0, 0, stars); const auto textLeft = diffBetweenTextAndStar + stars.width() / style::DevicePixelRatio(); p.setPen(st.textFg); text->draw(p, { .position = QPoint(textLeft, 0), .availableWidth = inner->width() - textLeft, }); }, inner->lifetime()); button->sizeValue( ) | rpl::start_with_next([=](const QSize &size) { price->moveToRight(st.padding.right(), st.padding.top()); inner->moveToLeft(st.iconLeft, st.padding.top()); inner->resize( size.width() - rect::m::sum::h(st.padding) - price->width(), buttonHeight); }, button->lifetime()); button->setClickedCallback([=] { const auto invoice = Payments::InvoiceCredits{ .session = &controller->session(), .randomId = UniqueIdFromOption(option), .credits = option.credits, .product = option.product, .currency = option.currency, .amount = option.amount, .extended = option.extended, }; const auto weak = Ui::MakeWeak(button); const auto done = [=](Payments::CheckoutResult result) { if (const auto strong = weak.data()) { strong->window()->setFocus(); if (result == Payments::CheckoutResult::Paid) { if (paid) { paid(); } } } }; Payments::CheckoutProcess::Start(std::move(invoice), done); }); Ui::ToggleChildrenVisibility(button, true); } // Footer. { auto text = tr::lng_credits_summary_options_about( lt_link, rpl::combine( tr::lng_credits_summary_options_about_link(), tr::lng_credits_summary_options_about_url() ) | rpl::map([](const QString &text, const QString &url) { return Ui::Text::Link(text, url); }), Ui::Text::RichLangValue); Ui::AddSkip(content); Ui::AddDividerText(content, std::move(text)); } content->resizeToWidth(container->width()); }; using ApiOptions = Api::CreditsTopupOptions; const auto apiCredits = content->lifetime().make_state( controller->session().user()); if (controller->session().premiumPossible()) { apiCredits->request( ) | rpl::start_with_error_done([=](const QString &error) { controller->showToast(error); }, [=] { fill(apiCredits->options()); }, content->lifetime()); } controller->session().premiumPossibleValue( ) | rpl::start_with_next([=](bool premiumPossible) { if (!premiumPossible) { fill({}); } }, content->lifetime()); } not_null AddBalanceWidget( not_null parent, rpl::producer balanceValue, bool rightAlign) { const auto balance = Ui::CreateChild(parent); const auto balanceStar = balance->lifetime().make_state( Ui::GenerateStars(st::creditsBalanceStarHeight, 1)); const auto starSize = balanceStar->size() / style::DevicePixelRatio(); const auto label = balance->lifetime().make_state( st::defaultTextStyle, tr::lng_credits_summary_balance(tr::now)); const auto count = balance->lifetime().make_state( st::semiboldTextStyle, tr::lng_contacts_loading(tr::now)); const auto diffBetweenStarAndCount = count->style()->font->spacew; const auto resize = [=] { balance->resize( std::max( label->maxWidth(), count->maxWidth() + starSize.width() + diffBetweenStarAndCount), label->style()->font->height + starSize.height()); }; std::move(balanceValue) | rpl::start_with_next([=](uint64 value) { count->setText( st::semiboldTextStyle, Lang::FormatCountToShort(value).string); balance->setBalance(value); resize(); }, balance->lifetime()); balance->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(balance); p.setPen(st::boxTextFg); label->draw(p, { .position = QPoint( rightAlign ? (balance->width() - label->maxWidth()) : 0, 0), .availableWidth = balance->width(), }); count->draw(p, { .position = QPoint( balance->width() - count->maxWidth(), label->minHeight() + (starSize.height() - count->minHeight()) / 2), .availableWidth = balance->width(), }); p.drawImage( balance->width() - count->maxWidth() - starSize.width() - diffBetweenStarAndCount, label->minHeight(), *balanceStar); }, balance->lifetime()); return balance; } void ReceiptCreditsBox( not_null box, not_null controller, PeerData *premiumBot, const Data::CreditsHistoryEntry &e) { box->setStyle(st::giveawayGiftCodeBox); box->setNoContentMargin(true); const auto star = Ui::GenerateStars(st::creditsTopupButton.height, 1); const auto content = box->verticalLayout(); Ui::AddSkip(content); Ui::AddSkip(content); Ui::AddSkip(content); using Type = Data::CreditsHistoryEntry::PeerType; const auto &stUser = st::boostReplaceUserpic; const auto session = &controller->session(); const auto peer = (e.peerType == Type::PremiumBot) ? nullptr : e.barePeerId ? session->data().peer(PeerId(e.barePeerId)).get() : nullptr; if (const auto callback = Ui::PaintPreviewCallback(session, e)) { const auto thumb = content->add(object_ptr>( content, GenericEntryPhoto(content, callback, stUser.photoSize))); AddViewMediaHandler(thumb->entity(), controller, e); } else if (peer) { content->add(object_ptr>( content, object_ptr(content, peer, stUser))); } else { const auto widget = content->add( object_ptr>( content, object_ptr(content)))->entity(); using Draw = Fn; const auto draw = widget->lifetime().make_state( Ui::GenerateCreditsPaintUserpicCallback(e)); widget->resize(Size(stUser.photoSize)); widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); (*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize); }, widget->lifetime()); } Ui::AddSkip(content); Ui::AddSkip(content); box->addRow(object_ptr>( box, object_ptr( box, rpl::single( !e.title.isEmpty() ? e.title : peer ? peer->name() : Ui::GenerateEntryName(e).text), st::creditsBoxAboutTitle))); Ui::AddSkip(content); { constexpr auto kMinus = QChar(0x2212); auto &lifetime = content->lifetime(); const auto text = lifetime.make_state( st::semiboldTextStyle, (e.in ? QChar('+') : kMinus) + Lang::FormatCountDecimal(std::abs(int64(e.credits)))); const auto roundedText = e.refunded ? tr::lng_channel_earn_history_return(tr::now) : e.pending ? tr::lng_channel_earn_history_pending(tr::now) : e.failed ? tr::lng_channel_earn_history_failed(tr::now) : QString(); const auto rounded = !roundedText.isEmpty() ? lifetime.make_state( st::defaultTextStyle, roundedText) : (Ui::Text::String*)(nullptr); const auto amount = content->add( object_ptr( content, star.height() / style::DevicePixelRatio())); const auto font = text->style()->font; const auto roundedFont = st::defaultTextStyle.font; const auto starWidth = star.width() / style::DevicePixelRatio(); const auto roundedSkip = roundedFont->spacew * 2; const auto roundedWidth = rounded ? roundedFont->width(roundedText) + roundedSkip + roundedFont->height : 0; const auto fullWidth = text->maxWidth() + font->spacew * 1 + starWidth + roundedWidth; amount->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(amount); p.setPen(e.pending ? st::creditsStroke : e.in ? st::boxTextFgGood : st::menuIconAttentionColor); const auto x = (amount->width() - fullWidth) / 2; text->draw(p, Ui::Text::PaintContext{ .position = QPoint( x, (amount->height() - font->height) / 2), .outerWidth = amount->width(), .availableWidth = amount->width(), }); p.drawImage( x + fullWidth - starWidth - roundedWidth, 0, star); if (rounded) { const auto roundedLeft = fullWidth + x - roundedWidth + roundedSkip; const auto pen = p.pen(); auto color = pen.color(); color.setAlphaF(color.alphaF() * 0.15); p.setPen(Qt::NoPen); p.setBrush(color); { auto hq = PainterHighQualityEnabler(p); p.drawRoundedRect( roundedLeft, (amount->height() - roundedFont->height) / 2, roundedWidth - roundedSkip, roundedFont->height, roundedFont->height / 2, roundedFont->height / 2); } p.setPen(pen); rounded->draw(p, Ui::Text::PaintContext{ .position = QPoint( roundedLeft + roundedFont->height / 2, (amount->height() - roundedFont->height) / 2), .outerWidth = roundedWidth, .availableWidth = roundedWidth, }); } }, amount->lifetime()); } if (!e.description.isEmpty()) { Ui::AddSkip(content); box->addRow(object_ptr>( box, object_ptr( box, rpl::single(e.description), st::defaultFlatLabel))); } Ui::AddSkip(content); Ui::AddSkip(content); AddCreditsHistoryEntryTable( controller, box->verticalLayout(), e); Ui::AddSkip(content); box->addRow(object_ptr>( box, object_ptr( box, tr::lng_credits_box_out_about( lt_link, tr::lng_payments_terms_link( ) | rpl::map([](const QString &t) { using namespace Ui::Text; return Link(t, u"https://telegram.org/tos"_q); }), Ui::Text::WithEntities), st::creditsBoxAboutDivider))); Ui::AddSkip(content); if (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) { const auto widget = Ui::CreateChild(content); using ColoredMiniStars = Ui::Premium::ColoredMiniStars; const auto stars = widget->lifetime().make_state( widget, false, Ui::Premium::MiniStars::Type::BiStars); stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); widget->resize( st::boxWidth - stUser.photoSize, stUser.photoSize * 2); content->sizeValue( ) | rpl::start_with_next([=](const QSize &size) { widget->moveToLeft(stUser.photoSize / 2, 0); const auto starsRect = Rect(widget->size()); stars->setPosition(starsRect.topLeft()); stars->setSize(starsRect.size()); widget->lower(); }, widget->lifetime()); widget->paintRequest( ) | rpl::start_with_next([=](const QRect &r) { auto p = QPainter(widget); p.fillRect(r, Qt::transparent); stars->paint(p); }, widget->lifetime()); } const auto button = box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); const auto buttonWidth = st::boxWidth - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); button->widthValue() | rpl::filter([=] { return (button->widthNoMargins() != buttonWidth); }) | rpl::start_with_next([=] { button->resizeToWidth(buttonWidth); }, button->lifetime()); } void ShowRefundInfoBox( not_null controller, FullMsgId refundItemId) { const auto owner = &controller->session().data(); const auto item = owner->message(refundItemId); const auto refund = item ? item->Get() : nullptr; if (!refund) { return; } Assert(refund->peer != nullptr); auto info = Data::CreditsHistoryEntry(); info.id = refund->transactionId; info.date = base::unixtime::parse(item->date()); info.credits = refund->amount; info.barePeerId = refund->peer->id.value; info.peerType = Data::CreditsHistoryEntry::PeerType::Peer; info.refunded = true; info.in = true; controller->show(Box( ::Settings::ReceiptCreditsBox, controller, nullptr, // premiumBot info)); } object_ptr GenericEntryPhoto( not_null parent, Fn(Fn)> callback, int photoSize) { auto owned = object_ptr(parent); const auto widget = owned.data(); widget->resize(Size(photoSize)); const auto draw = callback( crl::guard(widget, [=] { widget->update(); })); widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); draw(p, 0, 0, photoSize, photoSize); }, widget->lifetime()); return owned; } object_ptr HistoryEntryPhoto( not_null parent, not_null photo, int photoSize) { return GenericEntryPhoto( parent, [=](Fn update) { return Ui::GenerateCreditsPaintEntryCallback(photo, update); }, photoSize); } object_ptr PaidMediaThumbnail( not_null parent, not_null photo, PhotoData *second, int totalCount, int photoSize) { return GenericEntryPhoto( parent, [=](Fn update) { return Ui::GeneratePaidMediaPaintCallback( photo, second, totalCount, update); }, photoSize); } void SmallBalanceBox( not_null box, not_null controller, int creditsNeeded, UserId botId, Fn paid) { box->setWidth(st::boxWideWidth); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); const auto done = [=] { box->closeBox(); paid(); }; const auto bot = controller->session().data().user(botId).get(); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { const auto weak = base::make_weak(controller); const auto clickContextOther = [=] { return QVariant::fromValue(ClickHandlerContext{ .sessionWindow = weak, .botStartAutoSubmit = true, }); }; return box->setPinnedToTopContent(object_ptr( box, st::creditsLowBalancePremiumCover, Ui::Premium::TopBarDescriptor{ .clickContextOther = clickContextOther, .title = tr::lng_credits_small_balance_title( lt_count, rpl::single(creditsNeeded) | tr::to_count()), .about = tr::lng_credits_small_balance_about( lt_bot, rpl::single(TextWithEntities{ bot->name() }), Ui::Text::RichLangValue), .light = true, .gradientStops = Ui::Premium::CreditsIconGradientStops(), })); }(); FillCreditOptions(controller, box->verticalLayout(), creditsNeeded, done); content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight); content->setMinimumHeight(st::infoLayerTopBarHeight); content->resize(content->width(), content->maximumHeight()); content->additionalHeight( ) | rpl::start_with_next([=](int additionalHeight) { const auto wasMax = (content->height() == content->maximumHeight()); content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight + additionalHeight); if (wasMax) { content->resize(content->width(), content->maximumHeight()); } }, content->lifetime()); { const auto balance = AddBalanceWidget( content, controller->session().creditsValue(), true); const auto api = balance->lifetime().make_state( controller->session().user()); api->request({}, [=](Data::CreditsStatusSlice slice) { controller->session().setCredits(slice.balance); }); rpl::combine( balance->sizeValue(), content->sizeValue() ) | rpl::start_with_next([=](const QSize &, const QSize &) { balance->moveToRight( st::creditsHistoryRightSkip * 2, st::creditsHistoryRightSkip); balance->update(); }, balance->lifetime()); } } void AddWithdrawalWidget( not_null container, not_null controller, not_null peer, rpl::producer secondButtonUrl, rpl::producer availableBalanceValue, rpl::producer dateValue, rpl::producer lockedValue, rpl::producer usdValue) { Ui::AddSkip(container); const auto labels = container->add( object_ptr>( container, object_ptr(container)))->entity(); const auto majorLabel = Ui::CreateChild( labels, rpl::duplicate(availableBalanceValue) | rpl::map([](uint64 v) { return Lang::FormatCountDecimal(v); }), st::channelEarnBalanceMajorLabel); const auto icon = Ui::CreateSingleStarWidget( labels, majorLabel->height()); majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); majorLabel->sizeValue( ) | rpl::start_with_next([=](const QSize &majorSize) { const auto skip = st::channelEarnBalanceMinorLabelSkip; labels->resize( majorSize.width() + icon->width() + skip, majorSize.height()); majorLabel->moveToLeft(icon->width() + skip, 0); }, labels->lifetime()); Ui::ToggleChildrenVisibility(labels, true); Ui::AddSkip(container); container->add( object_ptr>( container, object_ptr( container, std::move(usdValue), st::channelEarnOverviewSubMinorLabel))); Ui::AddSkip(container); const auto input = Ui::AddInputFieldForCredits( container, rpl::duplicate(availableBalanceValue)); Ui::AddSkip(container); Ui::AddSkip(container); const auto &stButton = st::defaultActiveButton; const auto buttonsContainer = container->add( Ui::CreateSkipWidget(container, stButton.height), st::boxRowPadding); const auto button = Ui::CreateChild( buttonsContainer, rpl::never(), stButton); const auto buttonCredits = Ui::CreateChild( buttonsContainer, tr::lng_bot_earn_balance_button_buy_ads(), stButton); buttonCredits->setTextTransform( Ui::RoundButton::TextTransform::NoTransform); Ui::ToggleChildrenVisibility(buttonsContainer, true); rpl::combine( std::move(secondButtonUrl), buttonsContainer->sizeValue() ) | rpl::start_with_next([=](const QString &url, const QSize &size) { if (url.isEmpty()) { button->resize(size.width(), size.height()); buttonCredits->resize(0, 0); } else { const auto w = size.width() - st::boxRowPadding.left() / 2; button->resize(w / 2, size.height()); buttonCredits->resize(w / 2, size.height()); buttonCredits->moveToRight(0, 0); buttonCredits->setClickedCallback([=] { UrlClickHandler::Open(url); }); } }, buttonsContainer->lifetime()); rpl::duplicate( lockedValue ) | rpl::start_with_next([=](bool v) { button->setAttribute(Qt::WA_TransparentForMouseEvents, v); }, button->lifetime()); const auto session = &controller->session(); const auto label = Ui::CreateChild( button, tr::lng_channel_earn_balance_button(tr::now), st::channelEarnSemiboldLabel); const auto processInputChange = [&] { const auto buttonEmoji = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( st::settingsPremiumIconStar, { 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, true)); const auto context = Core::MarkedTextContext{ .session = session, .customEmojiRepaint = [=] { label->update(); }, }; using Balance = rpl::variable; const auto currentBalance = input->lifetime().make_state( rpl::duplicate(availableBalanceValue)); const auto process = [=] { const auto amount = input->getLastText().toDouble(); if (amount >= currentBalance->current()) { label->setText( tr::lng_bot_earn_balance_button_all(tr::now)); } else { label->setMarkedText( tr::lng_bot_earn_balance_button( tr::now, lt_count, amount, lt_emoji, buttonEmoji, Ui::Text::RichLangValue), context); } }; QObject::connect(input, &Ui::MaskedInputField::changed, process); process(); return process; }(); label->setTextColorOverride(stButton.textFg->c); label->setAttribute(Qt::WA_TransparentForMouseEvents); rpl::combine( rpl::duplicate(lockedValue), button->sizeValue(), label->sizeValue() ) | rpl::start_with_next([=](bool v, const QSize &b, const QSize &l) { label->moveToLeft( (b.width() - l.width()) / 2, (v ? -10 : 1) * (b.height() - l.height()) / 2); }, label->lifetime()); const auto lockedColor = anim::with_alpha(stButton.textFg->c, .5); const auto lockedLabelTop = Ui::CreateChild( button, tr::lng_bot_earn_balance_button_locked(), st::botEarnLockedButtonLabel); lockedLabelTop->setTextColorOverride(lockedColor); lockedLabelTop->setAttribute(Qt::WA_TransparentForMouseEvents); const auto lockedLabelBottom = Ui::CreateChild( button, QString(), st::botEarnLockedButtonLabel); lockedLabelBottom->setTextColorOverride(lockedColor); lockedLabelBottom->setAttribute(Qt::WA_TransparentForMouseEvents); rpl::combine( rpl::duplicate(lockedValue), button->sizeValue(), lockedLabelTop->sizeValue(), lockedLabelBottom->sizeValue() ) | rpl::start_with_next([=]( bool locked, const QSize &b, const QSize &top, const QSize &bottom) { const auto factor = locked ? 1 : -10; const auto sumHeight = top.height() + bottom.height(); lockedLabelTop->moveToLeft( (b.width() - top.width()) / 2, factor * (b.height() - sumHeight) / 2); lockedLabelBottom->moveToLeft( (b.width() - bottom.width()) / 2, factor * ((b.height() - sumHeight) / 2 + top.height())); }, lockedLabelTop->lifetime()); const auto dateUpdateLifetime = lockedLabelBottom->lifetime().make_state(); std::move( dateValue ) | rpl::start_with_next([=](const QDateTime &dt) { dateUpdateLifetime->destroy(); if (dt.isNull()) { return; } constexpr auto kDateUpdateInterval = crl::time(250); const auto was = base::unixtime::serialize(dt); const auto context = Core::MarkedTextContext{ .session = session, .customEmojiRepaint = [=] { lockedLabelBottom->update(); }, }; const auto emoji = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( st::chatSimilarLockedIcon, st::botEarnButtonLockMargins, true)); rpl::single( rpl::empty ) | rpl::then( base::timer_each(kDateUpdateInterval) ) | rpl::start_with_next([=] { const auto secondsDifference = std::max( was - base::unixtime::now() - 1, 0); const auto hours = secondsDifference / 3600; const auto minutes = (secondsDifference % 3600) / 60; const auto seconds = secondsDifference % 60; constexpr auto kZero = QChar('0'); const auto formatted = (hours > 0) ? (u"%1:%2:%3"_q) .arg(hours, 2, 10, kZero) .arg(minutes, 2, 10, kZero) .arg(seconds, 2, 10, kZero) : (u"%1:%2"_q) .arg(minutes, 2, 10, kZero) .arg(seconds, 2, 10, kZero); lockedLabelBottom->setMarkedText( base::duplicate(emoji).append(formatted), context); }, *dateUpdateLifetime); }, lockedLabelBottom->lifetime()); Api::HandleWithdrawalButton( Api::RewardReceiver{ .creditsReceiver = peer, .creditsAmount = [=, show = controller->uiShow()] { const auto amount = input->getLastText().toULongLong(); const auto min = float64(WithdrawalMin(session)); if (amount < min) { auto text = tr::lng_bot_earn_credits_out_minimal( tr::now, lt_link, Ui::Text::Link( tr::lng_bot_earn_credits_out_minimal_link( tr::now, lt_count, min), u"internal:"_q), Ui::Text::RichLangValue); show->showToast(Ui::Toast::Config{ .text = std::move(text), .filter = [=](const auto ...) { input->setText(QString::number(min)); processInputChange(); return true; }, }); return 0ULL; } return amount; }, }, button, controller->uiShow()); Ui::ToggleChildrenVisibility(button, true); Ui::AddSkip(container); Ui::AddSkip(container); const auto arrow = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( st::topicButtonArrow, st::channelEarnLearnArrowMargins, false)); auto about = Ui::CreateLabelWithCustomEmoji( container, tr::lng_bot_earn_learn_credits_out_about( lt_link, tr::lng_channel_earn_about_link( lt_emoji, rpl::single(arrow), Ui::Text::RichLangValue ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link( std::move(text), tr::lng_bot_earn_balance_about_url(tr::now)); }), Ui::Text::RichLangValue), { .session = session }, st::boxDividerLabel); Ui::AddSkip(container); container->add(object_ptr( container, std::move(about), st::defaultBoxDividerLabelPadding, RectPart::Top | RectPart::Bottom)); Ui::AddSkip(container); } } // namespace Settings