/* 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 "ui/controls/stars_rating.h" #include "base/unixtime.h" #include "lang/lang_keys.h" #include "ui/controls/feature_list.h" #include "ui/effects/premium_bubble.h" #include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" #include "ui/layers/show.h" #include "ui/text/custom_emoji_helper.h" #include "ui/text/custom_emoji_text_badge.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/rp_widget.h" #include "ui/ui_utility.h" #include "styles/style_chat.h" // textMoreIconEmoji #include "styles/style_info.h" #include "styles/style_info_levels.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" #include "styles/style_media_view.h" #include "styles/style_menu_icons.h" namespace Ui { namespace { constexpr auto kAutoCollapseTimeout = 4 * crl::time(1000); using Counters = Data::StarsRating; [[nodiscard]] Counters AdjustByReached(Counters data) { if (data.stars < 0) { return data; } const auto reached = !data.nextLevelStars; if (reached) { --data.level; data.stars = data.nextLevelStars = std::max({ data.stars, data.thisLevelStars, 1 }); data.thisLevelStars = 0; } else { data.stars = std::max(data.thisLevelStars, data.stars); data.nextLevelStars = std::max( data.nextLevelStars, data.stars + 1); } return data; } [[nodiscard]] Fn BubbleTextFactory( int countForScale, int nextLevelCounter) { return [=](int count) { const auto counter = [&](int count) { return (countForScale < 10'000) ? Lang::FormatCountDecimal(count) : (countForScale < 10'000'000) ? (Lang::FormatCountDecimal((count / 100) / 10.) + 'K') : (Lang::FormatCountDecimal((count / 100'000) / 10.) + 'M'); }; return Premium::BubbleText{ .counter = counter(count), .additional = (nextLevelCounter ? (u"/"_q + counter(nextLevelCounter)) : QString()), }; }; } void FillRatingLimit( rpl::producer<> showFinished, not_null container, rpl::producer data, Premium::BubbleType type, style::margins limitLinePadding, int starsForScale, int nextLevelStars, bool hideCount) { const auto addSkip = [&](int skip) { container->add(object_ptr(container, skip)); }; const auto negative = (type == Premium::BubbleType::NegativeRating); const auto ratio = [=](Counters rating) { if (negative) { return 0.5; } const auto min = rating.thisLevelStars; const auto max = rating.nextLevelStars; Assert(rating.stars >= min && rating.stars <= max); const auto count = (max - min); const auto index = (rating.stars - min); if (!index) { return 0.; } else if (index == count) { return 1.; } else if (count == 2) { return 0.5; } const auto available = st::boxWideWidth - st::boxPadding.left() - st::boxPadding.right(); const auto average = available / float64(count); const auto levelWidth = [&](int add) { return st::normalFont->width( tr::lng_boost_level( tr::now, lt_count, rating.level + add)); }; const auto paddings = 2 * st::premiumLineTextSkip; const auto labelLeftWidth = paddings + levelWidth(0); const auto labelRightWidth = paddings + levelWidth(1); const auto first = std::max(average, labelLeftWidth * 1.); const auto last = std::max(average, labelRightWidth * 1.); const auto other = (available - first - last) / (count - 2); return (first + (index - 1) * other) / available; }; auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached); auto bubbleRowState = rpl::duplicate( adjustedData ) | rpl::combine_previous( Counters() ) | rpl::map([=](Counters previous, Counters counters) { return Premium::BubbleRowState{ .counter = counters.stars, .ratio = ratio(counters), .animateFromZero = (counters.level != previous.level), .dynamic = true, }; }); Premium::AddBubbleRow( container, (hideCount ? st::iconOnlyPremiumBubble : st::starRatingBubble), std::move(showFinished), rpl::duplicate(bubbleRowState), type, (hideCount ? [](int) { return Premium::BubbleText(); } : BubbleTextFactory(starsForScale, nextLevelStars)), negative ? &st::levelNegativeBubble : &st::infoStarsCrown, limitLinePadding); addSkip(st::premiumLineTextSkip); const auto level = [](int level) { return tr::lng_boost_level(tr::now, lt_count, level); }; auto limitState = std::move( bubbleRowState ) | rpl::map([negative](const Premium::BubbleRowState &state) { return Premium::LimitRowState{ .ratio = negative ? 0.5 : state.ratio, .animateFromZero = !negative && state.animateFromZero, .dynamic = state.dynamic }; }); auto left = rpl::duplicate( adjustedData ) | rpl::map([=](Counters counters) { return (counters.level < 0) ? QString() : level(counters.level); }); auto right = rpl::duplicate( adjustedData ) | rpl::map([=](Counters counters) { return (counters.level < 0) ? tr::lng_stars_rating_negative_label(tr::now) : level(counters.level + 1); }); Premium::AddLimitRow( container, (negative ? st::negativeStarsLimits : st::boostLimits), Premium::LimitRowLabels{ .leftLabel = std::move(left), .rightLabel = std::move(right), .activeLineBg = [=] { return negative ? st::attentionButtonFg->b : st::windowBgActive->b; }, }, std::move(limitState), limitLinePadding); } void AboutRatingBox( not_null box, const QString &name, Counters data, Data::StarsRatingPending pending) { box->setWidth(st::boxWideWidth); box->setStyle(st::boostBox); struct State { rpl::variable data; rpl::variable pending; rpl::variable full; }; const auto state = box->lifetime().make_state(); state->data = data; FillRatingLimit( BoxShowFinishes(box), box->verticalLayout(), state->data.value(), (data.level < 0 ? Premium::BubbleType::NegativeRating : Premium::BubbleType::StarRating), st::boxRowPadding, data.stars, data.nextLevelStars, (data.level < 0 && !data.stars)); box->setMaxHeight(st::boostBoxMaxHeight); auto title = rpl::conditional( state->pending.value(), tr::lng_stars_rating_future(), tr::lng_stars_rating_title()); auto text = !name.isEmpty() ? tr::lng_stars_rating_about( lt_name, rpl::single(TextWithEntities{ name }), tr::rich) | rpl::type_erased : tr::lng_stars_rating_about_your( tr::rich) | rpl::type_erased; if (data.level < 0) { auto text = (data.stars < 0) ? tr::lng_stars_rating_negative_your( lt_count_decimal, rpl::single(-data.stars * 1.), tr::rich) : tr::lng_stars_rating_negative( lt_name, rpl::single(TextWithEntities{ name }), tr::rich); box->addRow( object_ptr( box, std::move(text), st::boostTextNegative), (st::boxRowPadding + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), style::al_top )->setTryMakeSimilarLines(true); } box->addRow( object_ptr(box, std::move(title), st::infoStarsTitle), st::boxRowPadding + QMargins(0, st::boostTitleSkip / 2, 0, 0), style::al_top); if (pending) { const auto now = base::unixtime::now(); const auto days = std::max((pending.date - now + 43200) / 86400, 1); auto text = state->pending.value( ) | rpl::map([=](bool value) { return tr::lng_stars_rating_pending( tr::now, lt_count_decimal, pending.value.stars - data.stars, lt_when, TextWithEntities{ tr::lng_stars_rating_updates(tr::now, lt_count, days), }, lt_link, Text::Link((value ? tr::lng_stars_rating_pending_back : tr::lng_stars_rating_pending_preview)( tr::now, lt_arrow, Text::IconEmoji(&st::textMoreIconEmoji), tr::marked)), tr::rich); }); const auto aboutPending = box->addRow( object_ptr( box, std::move(text), st::boostTextPending), (st::boxRowPadding + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), style::al_top); aboutPending->setTryMakeSimilarLines(true); aboutPending->setClickHandlerFilter([=](const auto &...) { state->pending = !state->pending.current(); state->data = state->pending.current() ? pending.value : data; box->verticalLayout()->resizeToWidth(box->width()); return false; }); } box->addRow( object_ptr( box, std::move(text), st::boostText), (st::boxRowPadding + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), style::al_top )->setTryMakeSimilarLines(true); auto helper = Text::CustomEmojiHelper(); const auto makeBadge = [&]( const QString &text, const style::RoundButton &st) { return helper.paletteDependent( Text::CustomEmojiTextBadge(text, st)); }; const auto makeActive = [&](const QString &text) { return makeBadge(text, st::customEmojiTextBadge); }; const auto makeInactive = [&](const QString &text) { return makeBadge(text, st::infoRatingDeductedBadge); }; const auto features = std::vector{ { st::menuIconRatingGifts, tr::lng_stars_title_gifts_telegram(tr::now), tr::lng_stars_about_gifts_telegram( tr::now, lt_emoji, makeActive(tr::lng_stars_rating_added(tr::now)), tr::rich), }, { st::menuIconRatingUsers, tr::lng_stars_title_gifts_users(tr::now), tr::lng_stars_about_gifts_users( tr::now, lt_emoji, makeActive(tr::lng_stars_rating_added(tr::now)), tr::rich), }, { st::menuIconRatingRefund, tr::lng_stars_title_refunds(tr::now), tr::lng_stars_about_refunds( tr::now, lt_emoji, makeInactive(tr::lng_stars_rating_deducted(tr::now)), tr::rich), }, }; const auto context = helper.context(); for (const auto &feature : features) { box->addRow(MakeFeatureListEntry(box, feature, context)); } box->addButton(rpl::single(QString()), [=] { box->closeBox(); })->setText(rpl::single(Text::IconEmoji( &st::infoStarsUnderstood ).append(' ').append(tr::lng_stars_rating_understood(tr::now)))); } [[nodiscard]] not_null SelectShape(int level) { if (level < 0) { return &st::levelNegative; } struct Shape { int level = 0; not_null shape; }; const auto list = std::vector{ { 1, &st::level1 }, { 2, &st::level2 }, { 3, &st::level3 }, { 4, &st::level4 }, { 5, &st::level5 }, { 6, &st::level6 }, { 7, &st::level7 }, { 8, &st::level8 }, { 9, &st::level9 }, { 10, &st::level10 }, { 20, &st::level20 }, { 30, &st::level30 }, { 40, &st::level40 }, { 50, &st::level50 }, { 60, &st::level60 }, { 70, &st::level70 }, { 80, &st::level80 }, { 90, &st::level90 }, }; const auto i = ranges::lower_bound( list, level + 1, ranges::less(), &Shape::level); return (i != begin(list)) ? (i - 1)->shape : list.front().shape; } } // namespace StarsRating::StarsRating( QWidget *parent, std::shared_ptr show, const QString &name, rpl::producer value, Fn pending) : _widget(std::make_unique(parent)) , _show(std::move(show)) , _name(name) , _value(std::move(value)) , _pending(std::move(pending)) { init(); } StarsRating::~StarsRating() = default; void StarsRating::init() { _widget->setPointerCursor(true); _widget->paintRequest() | rpl::on_next([=] { auto p = QPainter(_widget.get()); paint(p); }, lifetime()); _widget->setClickedCallback([=] { if (!_value.current()) { return; } _show->show(Box(AboutRatingBox, _name, _value.current(), _pending ? _pending() : Data::StarsRatingPending())); }); _widget->resize(_widget->width(), st::level1.icon.height()); _value.value() | rpl::on_next([=](Counters rating) { updateData(rating); }, lifetime()); } void StarsRating::updateData(Data::StarsRating rating) { if (!rating) { _shape = nullptr; _widthValue = 0; _currentLevel = 0; } else { _shape = SelectShape(rating.level); _collapsedText = (rating.level < 0 ? QString() : Lang::FormatCountDecimal(rating.level)); const auto &margin = st::levelMargin; _widthValue = _shape->icon.width() + margin.right() - margin.left(); _currentLevel = rating.level; } updateWidth(); } void StarsRating::updateWidth() { if (const auto widthToRight = _widthValue.current()) { const auto &margin = st::levelMargin; _widget->resize(margin.left() + widthToRight, _widget->height()); _widget->update(); } else { _widget->resize(0, _widget->height()); } } void StarsRating::raise() { _widget->raise(); } void StarsRating::moveTo(int x, int y) { _widget->move(x - st::levelMargin.left(), y - st::levelMargin.top()); } void StarsRating::setOpacity(float64 opacity) { _opacity = opacity; _widget->update(); } void StarsRating::setCustomColors( std::optional textColor, std::optional shapeColor) { _customTextColor = textColor; _customShapeColor = shapeColor; _cachedLevel = std::numeric_limits::min(); _widget->update(); } void StarsRating::paint(QPainter &p) { p.setOpacity(_opacity); if (!_shape) { return; } if (_cachedLevel != _currentLevel) { const auto ratio = style::DevicePixelRatio(); const auto size = _widget->size() * ratio; _cache = QImage(size, QImage::Format_ARGB32_Premultiplied); _cache.setDevicePixelRatio(ratio); _cache.fill(Qt::transparent); auto q = QPainter(&_cache); if (_customShapeColor) { _shape->icon.paint(q, 0, 0, _widget->width(), *_customShapeColor); } else { _shape->icon.paint(q, 0, 0, _widget->width()); } if (!_collapsedText.isEmpty()) { q.setPen(_customTextColor ? *_customTextColor : st::levelTextFg->c); q.setFont(st::levelStyle.font); q.drawText( Rect(_shape->icon.size()), Qt::AlignCenter, _collapsedText); } _cachedLevel = _currentLevel; } p.setOpacity(_opacity); p.drawImage(0, 0, _cache); } rpl::producer StarsRating::widthValue() const { return _widthValue.value(); } int StarsRating::width() const { return _widthValue.current(); } rpl::lifetime &StarsRating::lifetime() { return _widget->lifetime(); } } // namespace Ui