Files
tdesktop/Telegram/SourceFiles/ui/controls/stars_rating.cpp
2025-12-15 21:40:54 +04:00

555 lines
14 KiB
C++

/*
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<Premium::BubbleText(int)> 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<VerticalLayout*> container,
rpl::producer<Counters> data,
Premium::BubbleType type,
style::margins limitLinePadding,
int starsForScale,
int nextLevelStars,
bool hideCount) {
const auto addSkip = [&](int skip) {
container->add(object_ptr<FixedHeightWidget>(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<GenericBox*> box,
const QString &name,
Counters data,
Data::StarsRatingPending pending) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
struct State {
rpl::variable<Counters> data;
rpl::variable<bool> pending;
rpl::variable<bool> full;
};
const auto state = box->lifetime().make_state<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<FlatLabel>(
box,
std::move(text),
st::boostTextNegative),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),
style::al_top
)->setTryMakeSimilarLines(true);
}
box->addRow(
object_ptr<FlatLabel>(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<FlatLabel>(
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<FlatLabel>(
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<FeatureListEntry>{
{
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<const style::LevelShape*> SelectShape(int level) {
if (level < 0) {
return &st::levelNegative;
}
struct Shape {
int level = 0;
not_null<const style::LevelShape*> shape;
};
const auto list = std::vector<Shape>{
{ 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> show,
const QString &name,
rpl::producer<Counters> value,
Fn<Data::StarsRatingPending()> pending)
: _widget(std::make_unique<AbstractButton>(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<QColor> textColor,
std::optional<QColor> shapeColor) {
_customTextColor = textColor;
_customShapeColor = shapeColor;
_cachedLevel = std::numeric_limits<int>::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<int> StarsRating::widthValue() const {
return _widthValue.value();
}
int StarsRating::width() const {
return _widthValue.current();
}
rpl::lifetime &StarsRating::lifetime() {
return _widget->lifetime();
}
} // namespace Ui