/* 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/boosts/info_boosts_inner_widget.h" #include "api/api_premium.h" #include "api/api_statistics.h" #include "boxes/gift_premium_box.h" #include "boxes/peers/edit_peer_invite_link.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_user.h" #include "info/boosts/create_giveaway_box.h" #include "info/boosts/giveaway/boost_badge.h" #include "info/boosts/giveaway/giveaway_type_row.h" #include "info/boosts/info_boosts_widget.h" #include "info/info_controller.h" #include "info/profile/info_profile_icon.h" #include "info/statistics/info_statistics_inner_widget.h" // FillLoading. #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" #include "statistics/widgets/chart_header_widget.h" #include "ui/boxes/boost_box.h" #include "ui/controls/invite_link_label.h" #include "ui/effects/ripple_animation.h" #include "ui/empty_userpic.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_giveaway.h" #include "styles/style_info.h" #include "styles/style_premium.h" #include "styles/style_statistics.h" #include namespace Info::Boosts { namespace { void AddHeader( not_null content, tr::phrase<> text) { const auto header = content->add( object_ptr(content), st::statisticsLayerMargins + st::boostsChartHeaderPadding); header->resizeToWidth(header->width()); header->setTitle(text(tr::now)); header->setSubTitle({}); } void FillOverview( not_null content, const Data::BoostStatus &status) { const auto &stats = status.overview; Ui::AddSkip(content, st::boostsLayerOverviewMargins.top()); AddHeader(content, tr::lng_stats_overview_title); Ui::AddSkip(content); const auto diffBetweenHeaders = 0 + st::statisticsOverviewValue.style.font->height - st::statisticsHeaderTitleTextStyle.font->height; const auto container = content->add( object_ptr(content), st::statisticsLayerMargins); const auto addPrimary = [&](float64 v, bool approximately = false) { return Ui::CreateChild( container, (v >= 0) ? (approximately && v ? QChar(0x2248) : QChar()) + Lang::FormatCountToShort(v).string : QString(), st::statisticsOverviewValue); }; const auto addSub = [&]( not_null primary, float64 percentage, tr::phrase<> text) { const auto second = Ui::CreateChild( container, percentage ? u"%1%"_q.arg(std::abs(std::round(percentage * 10.) / 10.)) : QString(), st::statisticsOverviewSecondValue); second->setTextColorOverride(st::windowSubTextFg->c); const auto sub = Ui::CreateChild( container, text(), st::statisticsOverviewSubtext); sub->setTextColorOverride(st::windowSubTextFg->c); primary->geometryValue( ) | rpl::start_with_next([=](const QRect &g) { const auto &padding = st::statisticsOverviewSecondValuePadding; second->moveToLeft( rect::right(g) + padding.left(), g.y() + padding.top()); sub->moveToLeft( g.x(), st::statisticsChartHeaderHeight - st::statisticsOverviewSubtext.style.font->height + g.y() + diffBetweenHeaders); }, primary->lifetime()); }; const auto topLeftLabel = addPrimary(stats.level); const auto topRightLabel = addPrimary(stats.premiumMemberCount, true); const auto bottomLeftLabel = addPrimary(stats.boostCount); const auto bottomRightLabel = addPrimary(std::max( stats.nextLevelBoostCount - stats.boostCount, 0)); addSub( topLeftLabel, 0, tr::lng_boosts_level); addSub( topRightLabel, stats.premiumMemberPercentage, (stats.group ? tr::lng_boosts_premium_members : tr::lng_boosts_premium_audience)); addSub( bottomLeftLabel, 0, tr::lng_boosts_existing); addSub( bottomRightLabel, 0, tr::lng_boosts_next_level); container->showChildren(); container->resize(container->width(), topLeftLabel->height() * 5); container->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { const auto halfWidth = s.width() / 2; { const auto &p = st::boostsOverviewValuePadding; topLeftLabel->moveToLeft(p.left(), p.top()); } topRightLabel->moveToLeft( topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip, topLeftLabel->y()); bottomLeftLabel->moveToLeft( topLeftLabel->x(), topLeftLabel->y() + st::statisticsOverviewMidSkip); bottomRightLabel->moveToLeft( topRightLabel->x(), bottomLeftLabel->y()); }, container->lifetime()); Ui::AddSkip(content, st::boostsLayerOverviewMargins.bottom()); } void FillShareLink( not_null content, std::shared_ptr show, const QString &link, not_null peer) { const auto weak = Ui::MakeWeak(content); const auto copyLink = crl::guard(weak, [=] { QGuiApplication::clipboard()->setText(link); show->showToast(tr::lng_channel_public_link_copied(tr::now)); }); const auto shareLink = crl::guard(weak, [=] { show->showBox(ShareInviteLinkBox(peer, link)); }); const auto label = content->lifetime().make_state( content, rpl::single(link), nullptr); content->add( label->take(), st::boostsLinkFieldPadding); label->clicks( ) | rpl::start_with_next(copyLink, label->lifetime()); { const auto wrap = content->add( object_ptr( content, st::inviteLinkButton.height), st::inviteLinkButtonsPadding); const auto copy = CreateChild( wrap, tr::lng_group_invite_context_copy(), st::inviteLinkCopy); copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); copy->setClickedCallback(copyLink); const auto share = CreateChild( wrap, tr::lng_group_invite_context_share(), st::inviteLinkShare); share->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); share->setClickedCallback(shareLink); wrap->widthValue( ) | rpl::start_with_next([=](int width) { const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2; copy->setFullWidth(buttonWidth); share->setFullWidth(buttonWidth); copy->moveToLeft(0, 0, width); share->moveToRight(0, 0, width); }, wrap->lifetime()); wrap->showChildren(); } Ui::AddSkip(content, st::boostsLinkFieldPadding.bottom()); } void FillGetBoostsButton( not_null content, not_null controller, std::shared_ptr show, not_null peer, Fn reloadOnDone) { if (!Api::PremiumGiftCodeOptions(peer).giveawayGiftsPurchaseAvailable()) { return; } Ui::AddSkip(content); const auto &st = st::getBoostsButton; const auto &icon = st::getBoostsButtonIcon; const auto button = content->add(object_ptr( content.get(), tr::lng_boosts_get_boosts(), st)); button->setClickedCallback([=] { show->showBox(Box( CreateGiveawayBox, controller, peer, reloadOnDone, std::nullopt)); }); Ui::CreateChild( button, icon, QPoint{ st::infoSharedMediaButtonIconPosition.x(), (st.height + rect::m::sum::v(st.padding) - icon.height()) / 2, })->show(); Ui::AddSkip(content); Ui::AddDividerText(content, peer->isMegagroup() ? tr::lng_boosts_get_boosts_subtext_group() : tr::lng_boosts_get_boosts_subtext()); } } // namespace InnerWidget::InnerWidget( QWidget *parent, not_null controller, not_null peer) : VerticalLayout(parent) , _controller(controller) , _peer(peer) , _show(controller->uiShow()) { } void InnerWidget::load() { const auto api = lifetime().make_state(_peer); Info::Statistics::FillLoading( this, _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1), _showFinished.events()); _showFinished.events( ) | rpl::take(1) | rpl::start_with_next([=] { api->request( ) | rpl::start_with_error_done([](const QString &error) { }, [=] { _state = api->boostStatus(); _loaded.fire(true); fill(); }, lifetime()); }, lifetime()); } void InnerWidget::fill() { const auto fakeShowed = lifetime().make_state>(); const auto &status = _state; const auto inner = this; const auto reloadOnDone = crl::guard(this, [=] { while (Ui::VerticalLayout::count()) { delete Ui::VerticalLayout::widgetAt(0); } load(); }); { auto dividerContent = object_ptr(inner); dividerContent->add(object_ptr( dividerContent, st::boostSkipTop)); Ui::FillBoostLimit( fakeShowed->events(), dividerContent.data(), rpl::single(Ui::BoostCounters{ .level = status.overview.level, .boosts = status.overview.boostCount, .thisLevelBoosts = status.overview.currentLevelBoostCount, .nextLevelBoosts = status.overview.nextLevelBoostCount, .mine = status.overview.mine, }), st::statisticsLimitsLinePadding); inner->add(object_ptr( inner, std::move(dividerContent), st::statisticsLimitsDividerPadding)); } FillOverview(inner, status); Ui::AddSkip(inner); Ui::AddDivider(inner); Ui::AddSkip(inner); if (!status.prepaidGiveaway.empty()) { const auto multiplier = Api::PremiumGiftCodeOptions(_peer) .giveawayBoostsPerPremium(); Ui::AddSkip(inner); AddHeader(inner, tr::lng_boosts_prepaid_giveaway_title); Ui::AddSkip(inner); for (const auto &g : status.prepaidGiveaway) { using namespace Giveaway; const auto button = inner->add(object_ptr( inner, GiveawayTypeRow::Type::Prepaid, g.id, tr::lng_boosts_prepaid_giveaway_quantity( lt_count, rpl::single(g.quantity) | tr::to_count()), tr::lng_boosts_prepaid_giveaway_moths( lt_count, rpl::single(g.months) | tr::to_count()), Info::Statistics::CreateBadge( st::statisticsDetailsBottomCaptionStyle, QString::number(g.quantity * multiplier), st::boostsListBadgeHeight, st::boostsListBadgeTextPadding, st::premiumButtonBg2, st::premiumButtonFg, 1., st::boostsListMiniIconPadding, st::boostsListMiniIcon))); button->setClickedCallback([=] { _controller->uiShow()->showBox(Box( CreateGiveawayBox, _controller, _peer, reloadOnDone, g)); }); } Ui::AddSkip(inner); Ui::AddDivider(inner); Ui::AddSkip(inner); } const auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0); const auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0); if (hasBoosts || hasGifts) { auto boostClicked = [=](const Data::Boost &boost) { if (!boost.giftCodeLink.slug.isEmpty()) { ResolveGiftCode(_controller, boost.giftCodeLink.slug); } else if (boost.userId) { const auto user = _peer->owner().user(boost.userId); if (boost.isGift || boost.isGiveaway) { const auto d = Api::GiftCode{ .from = _peer->id, .to = user->id, .date = TimeId(boost.date.toSecsSinceEpoch()), .months = boost.expiresAfterMonths, }; _show->showBox(Box(GiftCodePendingBox, _controller, d)); } else { crl::on_main(this, [=] { _controller->showPeerInfo(user); }); } } else if (!boost.isUnclaimed) { _show->showToast(tr::lng_boosts_list_pending_about(tr::now)); } }; #ifdef _DEBUG const auto hasOneTab = false; #else const auto hasOneTab = (hasBoosts != hasGifts); #endif const auto boostsTabText = tr::lng_boosts_list_title( tr::now, lt_count, status.firstSliceBoosts.multipliedTotal); const auto giftsTabText = tr::lng_boosts_list_tab_gifts( tr::now, lt_count, status.firstSliceGifts.multipliedTotal); if (hasOneTab) { Ui::AddSkip(inner); const auto header = inner->add( object_ptr(inner), st::statisticsLayerMargins + st::boostsChartHeaderPadding); header->resizeToWidth(header->width()); header->setTitle(hasBoosts ? boostsTabText : giftsTabText); header->setSubTitle({}); } class Slider final : public Ui::SettingsSlider { public: using Ui::SettingsSlider::SettingsSlider; void setNaturalWidth(int w) { _naturalWidth = w; } int naturalWidth() const override { return _naturalWidth; } private: int _naturalWidth = 0; }; const auto slider = inner->add( object_ptr>( inner, object_ptr(inner, st::defaultTabsSlider)), st::boxRowPadding); slider->toggle(!hasOneTab, anim::type::instant); slider->entity()->addSection(boostsTabText); slider->entity()->addSection(giftsTabText); { const auto &st = st::defaultTabsSlider; slider->entity()->setNaturalWidth(0 + st.labelStyle.font->width(boostsTabText) + st.labelStyle.font->width(giftsTabText) + rect::m::sum::h(st::boxRowPadding)); } const auto boostsWrap = inner->add( object_ptr>( inner, object_ptr(inner))); const auto giftsWrap = inner->add( object_ptr>( inner, object_ptr(inner))); rpl::single(hasOneTab ? (hasGifts ? 1 : 0) : 0) | rpl::then( slider->entity()->sectionActivated() ) | rpl::start_with_next([=](int index) { boostsWrap->toggle(!index, anim::type::instant); giftsWrap->toggle(index, anim::type::instant); }, inner->lifetime()); Statistics::AddBoostsList( status.firstSliceBoosts, boostsWrap->entity(), boostClicked, _peer, tr::lng_boosts_title()); Statistics::AddBoostsList( status.firstSliceGifts, giftsWrap->entity(), std::move(boostClicked), _peer, tr::lng_boosts_title()); Ui::AddSkip(inner); Ui::AddSkip(inner); Ui::AddDividerText(inner, status.overview.group ? tr::lng_boosts_list_subtext_group() : tr::lng_boosts_list_subtext()); } Ui::AddSkip(inner); Ui::AddSkip(inner); AddHeader(inner, tr::lng_boosts_link_title); Ui::AddSkip(inner, st::boostsLinkSkip); FillShareLink(inner, _show, status.link, _peer); Ui::AddSkip(inner); Ui::AddDividerText(inner, status.overview.group ? tr::lng_boosts_link_subtext_group() : tr::lng_boosts_link_subtext()); FillGetBoostsButton(inner, _controller, _show, _peer, reloadOnDone); resizeToWidth(width()); crl::on_main(this, [=]{ fakeShowed->fire({}); }); } void InnerWidget::saveState(not_null memento) { memento->setState(base::take(_state)); } void InnerWidget::restoreState(not_null memento) { _state = memento->state(); if (!_state.link.isEmpty()) { fill(); } else { load(); } Ui::RpWidget::resizeToWidth(width()); } rpl::producer InnerWidget::scrollToRequests() const { return _scrollToRequests.events(); } auto InnerWidget::showRequests() const -> rpl::producer { return _showRequests.events(); } void InnerWidget::showFinished() { _showFinished.fire({}); } not_null InnerWidget::peer() const { return _peer; } } // namespace Info::Boosts