Add top reactors to paid reaction details.

This commit is contained in:
John Preston 2024-08-06 16:08:55 +02:00
parent 9bb1fa8782
commit afe30da9f4
14 changed files with 301 additions and 58 deletions

View file

@ -2042,6 +2042,11 @@ auto MessageReactions::recent() const
return _recent;
}
auto MessageReactions::topPaid() const -> const std::vector<TopPaid> & {
static const auto kEmpty = std::vector<TopPaid>();
return _paid ? _paid->top : kEmpty;
}
bool MessageReactions::empty() const {
return _list.empty();
}

View file

@ -379,6 +379,7 @@ public:
[[nodiscard]] const std::vector<MessageReaction> &list() const;
[[nodiscard]] auto recent() const
-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;
[[nodiscard]] const std::vector<TopPaid> &topPaid() const;
[[nodiscard]] std::vector<ReactionId> chosen() const;
[[nodiscard]] bool empty() const;

View file

@ -2573,11 +2573,11 @@ const std::vector<Data::MessageReaction> &HistoryItem::reactions() const {
std::vector<Data::MessageReaction> HistoryItem::reactionsWithLocal() const {
auto result = reactions();
const auto i = ranges::find(
result,
Data::ReactionId::Paid(),
&Data::MessageReaction::id);
if (const auto local = _reactions ? _reactions->localPaidCount() : 0) {
const auto i = ranges::find(
result,
Data::ReactionId::Paid(),
&Data::MessageReaction::id);
if (i != end(result)) {
i->my = true;
i->count += local;
@ -2591,10 +2591,16 @@ std::vector<Data::MessageReaction> HistoryItem::reactionsWithLocal() const {
.my = true,
});
}
} else if (i != end(result) && i != begin(result)) {
std::rotate(begin(result), i, i + 1);
}
return result;
}
int HistoryItem::reactionsPaidScheduled() const {
return _reactions ? _reactions->scheduledPaid() : 0;
}
bool HistoryItem::reactionsAreTags() const {
return _flags & MessageFlag::ReactionsAreTags;
}
@ -2609,6 +2615,12 @@ auto HistoryItem::recentReactions() const
return _reactions ? _reactions->recent() : kEmpty;
}
auto HistoryItem::topPaidReactions() const
-> const std::vector<Data::MessageReactionsTopPaid> & {
static const auto kEmpty = std::vector<Data::MessageReactionsTopPaid>();
return _reactions ? _reactions->topPaid() : kEmpty;
}
bool HistoryItem::canViewReactions() const {
return (_flags & MessageFlag::CanViewReactions)
&& _reactions

View file

@ -57,6 +57,7 @@ struct RippleAnimation;
namespace Data {
struct MessagePosition;
struct RecentReaction;
struct MessageReactionsTopPaid;
struct ReactionId;
class Media;
struct MessageReaction;
@ -456,6 +457,9 @@ public:
-> const base::flat_map<
Data::ReactionId,
std::vector<Data::RecentReaction>> &;
[[nodiscard]] auto topPaidReactions() const
-> const std::vector<Data::MessageReactionsTopPaid> &;
[[nodiscard]] int reactionsPaidScheduled() const;
[[nodiscard]] bool canViewReactions() const;
[[nodiscard]] std::vector<Data::ReactionId> chosenReactions() const;
[[nodiscard]] Data::ReactionId lookupUnreadReaction(

View file

@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "base/call_delayed.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/credits.h"
#include "data/components/scheduled_messages.h"
#include "data/components/sponsored_messages.h"
#include "data/notify/data_notify_settings.h"
@ -869,6 +870,11 @@ HistoryWidget::HistoryWidget(
}
if (flags & PeerUpdateFlag::FullInfo) {
fullInfoUpdated();
if (const auto channel = _peer ? _peer->asChannel() : nullptr) {
if (channel->allowedReactions().paidEnabled) {
session().credits().load();
}
}
}
}, lifetime());

View file

@ -55,6 +55,7 @@ struct InlineList::Button {
int textWidth = 0;
int count = 0;
bool chosen = false;
bool paid = false;
bool tag = false;
};
@ -180,7 +181,7 @@ void InlineList::layoutButtons() {
}
InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) {
auto result = Button{ .id = id };
auto result = Button{ .id = id, .paid = id.paid()};
if (const auto customId = id.custom()) {
result.custom = _owner->owner().customEmojiManager().create(
customId,
@ -421,14 +422,18 @@ void InlineList::paint(
} else if (!bubbleReady) {
opacity = bubbleProgress;
}
color = stm->msgFileBg->c;
color = button.paid
? st->creditsBg3()->c
: stm->msgFileBg->c;
} else {
if (!bubbleReady) {
opacity = bubbleProgress;
}
color = (chosen
? st->msgServiceFg()
: st->msgServiceBg())->c;
color = (!chosen
? st->msgServiceBg()
: button.paid
? st->creditsBg2()
: st->msgServiceFg())->c;
}
const auto fill = geometry.marginsAdded({
@ -451,7 +456,7 @@ void InlineList::paint(
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
: st->msgServiceFg())
: !chosen
? stm->msgServiceFg
? (button.paid ? st->creditsFg() : stm->msgServiceFg)
: context.outbg
? (context.selected()
? st->historyFileOutIconFgSelected()

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/send_credits_box.h" // CreditsEmojiSmall.
#include "core/ui_integration.h" // MarkedTextContext.
#include "data/components/credits.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/history_view_element.h"
@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h"
#include "ui/layers/show.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_thumbnails.h"
namespace Payments {
namespace {
@ -167,10 +169,26 @@ void ShowPaidReactionDetails(
};
});
};
auto top = std::vector<Ui::PaidReactionTop>();
const auto &topPaid = item->topPaidReactions();
top.reserve(topPaid.size());
for (const auto &entry : topPaid) {
if (!entry.top) {
continue;
}
top.push_back({
.name = entry.peer->shortName(),
.photo = Ui::MakeUserpicThumbnail(entry.peer),
.count = int(entry.count),
});
}
ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
state->selectBox = show->show(Ui::MakePaidReactionBox({
.min = min,
.max = max,
.chosen = chosen,
.top = std::move(top),
.channel = item->history()->peer->name(),
.submit = std::move(submitText),
.balanceValue = session->credits().balanceValue(),

View file

@ -8,10 +8,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/ui/payments_reaction_box.h"
#include "lang/lang_keys.h"
#include "ui/boxes/boost_box.h" // MakeBoostFeaturesBadge.
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
@ -27,6 +31,8 @@ namespace Settings {
namespace Ui {
namespace {
constexpr auto kMaxTopPaidShown = 3;
void PaidReactionSlider(
not_null<VerticalLayout*> container,
int min,
@ -35,9 +41,9 @@ void PaidReactionSlider(
Fn<void(int)> changed) {
const auto top = st::boxTitleClose.height + st::creditsHistoryRightSkip;
const auto slider = container->add(
object_ptr<MediaSlider>(container, st::settingsScale),
object_ptr<MediaSlider>(container, st::paidReactSlider),
st::boxRowPadding + QMargins(0, top, 0, 0));
slider->resize(slider->width(), st::settingsScale.seekSize.height());
slider->resize(slider->width(), st::paidReactSlider.seekSize.height());
slider->setPseudoDiscrete(
max + 1 - min,
[=](int index) { return min + index; },
@ -46,13 +52,145 @@ void PaidReactionSlider(
changed);
}
[[nodiscard]] QImage GenerateBadgeImage(int count) {
const auto text = Lang::FormatCountDecimal(count);
const auto length = st::chatSimilarBadgeFont->width(text);
const auto contents = length
+ st::chatSimilarLockedIcon.width();
const auto badge = QRect(
st::chatSimilarBadgePadding.left(),
st::chatSimilarBadgePadding.top(),
contents,
st::chatSimilarBadgeFont->height);
const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding);
auto result = QImage(
rect.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
auto q = QPainter(&result);
const auto &font = st::chatSimilarBadgeFont;
const auto textTop = badge.y() + font->ascent;
const auto icon = &st::chatSimilarLockedIcon;
const auto position = st::chatSimilarLockedIconPosition;
auto hq = PainterHighQualityEnabler(q);
q.setBrush(st::creditsBg3);
q.setPen(Qt::NoPen);
const auto radius = rect.height() / 2.;
q.drawRoundedRect(rect, radius, radius);
auto textLeft = 0;
if (icon) {
icon->paint(
q,
badge.x() + position.x(),
badge.y() + position.y(),
rect.width());
textLeft += position.x() + icon->width();
}
q.setFont(font);
q.setPen(st::premiumButtonFg);
q.drawText(textLeft, textTop, text);
q.end();
return result;
}
[[nodiscard]] not_null<Ui::RpWidget*> MakeTopReactor(
not_null<QWidget*> parent,
const PaidReactionTop &data) {
const auto result = Ui::CreateChild<Ui::RpWidget>(parent);
result->show();
struct State {
QImage badge;
Ui::Text::String name;
};
const auto state = result->lifetime().make_state<State>();
state->name.setText(st::defaultTextStyle, data.name);
const auto count = data.count;
const auto photo = data.photo;
photo->subscribeToUpdates([=] {
result->update();
});
style::PaletteChanged(
) | rpl::start_with_next([=] {
state->badge = QImage();
}, result->lifetime());
result->paintRequest() | rpl::start_with_next([=] {
auto p = Painter(result);
const auto left = (result->width() - st::paidReactTopUserpic) / 2;
p.drawImage(left, 0, photo->image(st::paidReactTopUserpic));
if (state->badge.isNull()) {
state->badge = GenerateBadgeImage(count);
}
const auto bwidth = state->badge.width()
/ state->badge.devicePixelRatio();
p.drawImage(
(result->width() - bwidth) / 2,
st::paidReactTopBadgeSkip,
state->badge);
p.setPen(st::windowFg);
const auto skip = st::normalFont->spacew;
const auto nameTop = st::paidReactTopNameSkip;
const auto available = result->width() - skip * 2;
state->name.draw(p, skip, nameTop, available, style::al_top);
}, result->lifetime());
return result;
}
void FillTopReactors(
not_null<VerticalLayout*> container,
std::vector<PaidReactionTop> top) {
container->add(
MakeBoostFeaturesBadge(
container,
tr::lng_paid_react_top_title(),
[](QRect) { return st::creditsBg3->b; }),
st::boxRowPadding + st::paidReactTopTitleMargin);
const auto height = st::paidReactTopNameSkip + st::normalFont->height;
const auto wrap = container->add(
object_ptr<Ui::FixedHeightWidget>(container, height),
st::paidReactTopMargin);
struct State {
std::vector<not_null<Ui::RpWidget*>> widgets;
};
const auto state = wrap->lifetime().make_state<State>();
const auto topCount = std::min(int(top.size()), kMaxTopPaidShown);
for (auto i = 0; i != topCount; ++i) {
state->widgets.push_back(MakeTopReactor(wrap, top[i]));
}
wrap->widthValue() | rpl::start_with_next([=](int width) {
const auto single = width / 4;
if (single <= st::paidReactTopUserpic) {
return;
}
auto left = (width - single * topCount) / 2;
for (const auto widget : state->widgets) {
widget->setGeometry(left, 0, single, height);
left += single;
}
}, wrap->lifetime());
}
} // namespace
void PaidReactionsBox(
not_null<GenericBox*> box,
PaidReactionBoxArgs &&args) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
box->setStyle(st::paidReactBox);
box->setNoContentMargin(true);
struct State {
@ -77,7 +215,7 @@ void PaidReactionsBox(
box,
tr::lng_paid_react_title(),
st::boostCenteredTitle),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
@ -87,7 +225,11 @@ void PaidReactionsBox(
Text::RichLangValue),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));
if (!args.top.empty()) {
FillTopReactors(box->verticalLayout(), std::move(args.top));
}
const auto button = box->addButton(rpl::single(QString()), [=] {
args.send(state->chosen.current());
@ -117,7 +259,7 @@ void PaidReactionsBox(
box->widthValue(
) | rpl::start_with_next([=](int width) {
const auto &padding = st::boostBox.buttonPadding;
const auto &padding = st::paidReactBox.buttonPadding;
button->resizeToWidth(width
- padding.left()
- padding.right());

View file

@ -13,17 +13,26 @@ namespace Ui {
class BoxContent;
class GenericBox;
class DynamicImage;
struct TextWithContext {
TextWithEntities text;
std::any context;
};
struct PaidReactionTop {
QString name;
std::shared_ptr<DynamicImage> photo;
int count = 0;
};
struct PaidReactionBoxArgs {
int min = 0;
int max = 0;
int chosen = 0;
std::vector<PaidReactionTop> top;
QString channel;
Fn<rpl::producer<TextWithContext>(rpl::producer<int> amount)> submit;
rpl::producer<uint64> balanceValue;

View file

@ -111,48 +111,13 @@ namespace {
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text) {
auto result = object_ptr<Ui::FlatLabel>(
parent,
std::move(text),
st::boostLevelBadge);
const auto label = result.data();
label->show();
label->paintRequest() | rpl::start_with_next([=] {
const auto size = label->textMaxWidth();
const auto rect = QRect(
(label->width() - size) / 2,
st::boostLevelBadge.margin.top(),
size,
st::boostLevelBadge.style.font->height
).marginsAdded(st::boostLevelBadge.margin);
auto p = QPainter(label);
auto hq = PainterHighQualityEnabler(p);
return MakeBoostFeaturesBadge(parent, std::move(text), [](QRect rect) {
auto gradient = QLinearGradient(
rect.topLeft(),
rect.topRight());
gradient.setStops(Ui::Premium::GiftGradientStops());
p.setBrush(gradient);
p.setPen(Qt::NoPen);
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
const auto &lineFg = st::windowBgRipple;
const auto line = st::boostLevelBadgeLine;
const auto top = st::boostLevelBadge.margin.top()
+ ((st::boostLevelBadge.style.font->height - line) / 2);
const auto left = 0;
const auto skip = st::boostLevelBadgeSkip;
if (const auto right = rect.x() - skip; right > left) {
p.fillRect(left, top, right - left, line, lineFg);
}
const auto right = label->width();
if (const auto left = rect.x() + rect.width() + skip
; left < right) {
p.fillRect(left, top, right - left, line, lineFg);
}
}, label->lifetime());
return result;
return QBrush(gradient);
});
}
void AddFeaturesList(
@ -885,4 +850,48 @@ void FillBoostLimit(
limitLinePadding);
}
object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text,
Fn<QBrush(QRect)> bg) {
auto result = object_ptr<Ui::FlatLabel>(
parent,
std::move(text),
st::boostLevelBadge);
const auto label = result.data();
label->show();
label->paintRequest() | rpl::start_with_next([=] {
const auto size = label->textMaxWidth();
const auto rect = QRect(
(label->width() - size) / 2,
st::boostLevelBadge.margin.top(),
size,
st::boostLevelBadge.style.font->height
).marginsAdded(st::boostLevelBadge.margin);
auto p = QPainter(label);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(bg(rect));
p.setPen(Qt::NoPen);
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
const auto &lineFg = st::windowBgRipple;
const auto line = st::boostLevelBadgeLine;
const auto top = st::boostLevelBadge.margin.top()
+ ((st::boostLevelBadge.style.font->height - line) / 2);
const auto left = 0;
const auto skip = st::boostLevelBadgeSkip;
if (const auto right = rect.x() - skip; right > left) {
p.fillRect(left, top, right - left, line, lineFg);
}
const auto right = label->width();
if (const auto left = rect.x() + rect.width() + skip
; left < right) {
p.fillRect(left, top, right - left, line, lineFg);
}
}, label->lifetime());
return result;
}
} // namespace Ui

View file

@ -17,6 +17,7 @@ class Show;
class RpWidget;
class GenericBox;
class VerticalLayout;
class FlatLabel;
struct BoostCounters {
int level = 0;
@ -129,4 +130,9 @@ void FillBoostLimit(
rpl::producer<BoostCounters> data,
style::margins limitLinePadding);
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text,
Fn<QBrush(QRect)> bg);
} // namespace Ui

View file

@ -46,7 +46,7 @@ creditsBoxButtonLabel: FlatLabel(defaultFlatLabel) {
}
starIconSmall: icon{{ "payments/small_star", windowFg }};
starIconSmallPadding: margins(0px, -2px, 0px, 0px);
starIconSmallPadding: margins(0px, -3px, 0px, 0px);
creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }};

View file

@ -340,13 +340,13 @@ showOrBox: Box(boostBox) {
boostBoxMaxHeight: 512px;
boostLevelBadge: FlatLabel(defaultFlatLabel) {
margin: margins(12px, 4px, 12px, 4px);
margin: margins(12px, 4px, 12px, 5px);
style: semiboldTextStyle;
textFg: premiumButtonFg;
align: align(top);
}
boostLevelBadgePadding: margins(30px, 12px, 32px, 12px);
boostLevelBadgeSkip: 8px;
boostLevelBadgeSkip: 12px;
boostLevelBadgeLine: 1px;
boostFeatureLabel: FlatLabel(defaultFlatLabel) {
@ -365,3 +365,29 @@ boostFeatureName: icon{{ "settings/premium/features/feature_color_names", window
boostFeatureStories: icon{{ "settings/premium/features/feature_stories", windowBgActive }};
boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }};
boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }};
paidReactTitleSkip: 23px;
paidReactTopTitleMargin: margins(10px, 26px, 10px, 12px);
paidReactTopMargin: margins(0px, 12px, 0px, 11px);
paidReactTopUserpic: 42px;
paidReactTopNameSkip: 47px;
paidReactTopBadgeSkip: 32px;
paidReactSlider: MediaSlider(defaultContinuousSlider) {
activeFg: creditsBg3;
inactiveFg: creditsBg2;
activeFgOver: creditsBg3;
inactiveFgOver: creditsBg2;
activeFgDisabled: creditsBg3;
inactiveFgDisabled: creditsBg2;
width: 6px;
seekSize: size(16px, 16px);
}
paidReactBox: Box(boostBox) {
buttonPadding: margins(22px, 22px, 22px, 22px);
buttonHeight: 42px;
button: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
font: font(13px semibold);
}
}

@ -1 +1 @@
Subproject commit 8db5d1aa533334c75ed2598ecf3607768ae9b418
Subproject commit a5b1266a8c340ed916466a83cbbc5793471c2438