Handle t.me/channel?boost links.

This commit is contained in:
John Preston 2023-09-12 21:00:39 +04:00
parent 39f8394f98
commit 1c2951598b
17 changed files with 920 additions and 204 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -2002,6 +2002,37 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_gift_terms" = "You can review the list of features and terms of use for Telegram Premium {link}."; "lng_premium_gift_terms" = "You can review the list of features and terms of use for Telegram Premium {link}.";
"lng_premium_gift_terms_link" = "here"; "lng_premium_gift_terms_link" = "here";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_level#one" = "Level {count}";
"lng_boost_level#other" = "Level {count}";
"lng_boost_channel_title_first" = "Enable stories for channel";
"lng_boost_channel_needs_first#one" = "{channel} needs **{count}** more boost to enable posting stories. Help make it possible!";
"lng_boost_channel_needs_first#other" = "{channel} needs **{count}** more boosts to enable posting stories. Help make it possible!";
"lng_boost_channel_title_more" = "Help upgrade channel";
"lng_boost_channel_needs_more#one" = "{channel} needs **{count}** more boost to be able to {post}.";
"lng_boost_channel_needs_more#other" = "{channel} needs **{count}** more boosts to be able to {post}.";
"lng_boost_channel_you_title" = "You boosted {channel}!";
"lng_boost_channel_you_first#one" = "This channel needs **{count}** more boost\nto enable stories.";
"lng_boost_channel_you_first#other" = "This channel needs **{count}** more boosts\nto enable stories.";
"lng_boost_channel_you_more#one" = "This channel needs **{count}** more boost\nto be able to {post}.";
"lng_boost_channel_you_more#other" = "This channel needs **{count}** more boosts\nto be able to {post}.";
"lng_boost_channel_reached_first" = "This channel reached **Level 1** and can now post stories.";
"lng_boost_channel_reached_more#one" = "This channel reached **Level {count}** and can now {post}.";
"lng_boost_channel_reached_more#other" = "This channel reached **Level {count}** and can now {post}.";
"lng_boost_channel_post_stories#one" = "post **{count} story** per day";
"lng_boost_channel_post_stories#other" = "post **{count} stories** per day";
"lng_boost_error_gifted_title" = "Can't boost with gifted Premium!";
"lng_boost_error_gifted_text" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels.";
"lng_boost_error_already_title" = "Already Boosted!";
"lng_boost_error_already_text" = "You are already boosting this channel.";
"lng_boost_error_premium_title" = "Premium needed!";
"lng_boost_error_premium_text" = "Only **Telegram Premium** subscribers can boost channels. Do you want to subscribe to **Telegram Premium**?";
"lng_boost_error_premium_yes" = "Yes";
"lng_boost_error_flood_title" = "Can't boost too often!";
"lng_boost_error_flood_text" = "You can change the channel you boost only once a day. Next time you can boost is in {left}.";
"lng_boost_now_instead" = "You currently boost {channel}. Do you want to boost {other} instead?";
"lng_boost_now_replace" = "Replace";
"lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts."; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts.";
"lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts."; "lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts.";

View File

@ -410,8 +410,9 @@ void SimpleLimitBox(
Settings::AddSkip(top, st::premiumInfographicPadding.top()); Settings::AddSkip(top, st::premiumInfographicPadding.top());
Ui::Premium::AddBubbleRow( Ui::Premium::AddBubbleRow(
top, top,
st::defaultPremiumBubble,
BoxShowFinishes(box), BoxShowFinishes(box),
descriptor.defaultLimit, 0,
descriptor.current, descriptor.current,
descriptor.premiumLimit, descriptor.premiumLimit,
premiumPossible, premiumPossible,
@ -770,16 +771,18 @@ void FilterLinksLimitBox(
void FiltersLimitBox( void FiltersLimitBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session) { not_null<Main::Session*> session,
std::optional<int> filtersCountOverride) {
const auto premium = session->premium(); const auto premium = session->premium();
const auto premiumPossible = session->premiumPossible(); const auto premiumPossible = session->premiumPossible();
const auto limits = Data::PremiumLimits(session); const auto limits = Data::PremiumLimits(session);
const auto defaultLimit = float64(limits.dialogFiltersDefault()); const auto defaultLimit = float64(limits.dialogFiltersDefault());
const auto premiumLimit = float64(limits.dialogFiltersPremium()); const auto premiumLimit = float64(limits.dialogFiltersPremium());
const auto current = float64(ranges::count_if( const auto cloud = int(ranges::count_if(
session->data().chatsFilters().list(), session->data().chatsFilters().list(),
[](const Data::ChatFilter &f) { return f.id() != FilterId(); })); [](const Data::ChatFilter &f) { return f.id() != FilterId(); }));
const auto current = float64(filtersCountOverride.value_or(cloud));
auto text = rpl::combine( auto text = rpl::combine(
tr::lng_filters_limit1( tr::lng_filters_limit1(
@ -1079,6 +1082,7 @@ void AccountsLimitBox(
Settings::AddSkip(top, st::premiumInfographicPadding.top()); Settings::AddSkip(top, st::premiumInfographicPadding.top());
Ui::Premium::AddBubbleRow( Ui::Premium::AddBubbleRow(
top, top,
st::defaultPremiumBubble,
BoxShowFinishes(box), BoxShowFinishes(box),
0, 0,
current, current,

View File

@ -42,7 +42,8 @@ void FilterLinksLimitBox(
not_null<Main::Session*> session); not_null<Main::Session*> session);
void FiltersLimitBox( void FiltersLimitBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session); not_null<Main::Session*> session,
std::optional<int> filtersCountOverride);
void ShareableFiltersLimitBox( void ShareableFiltersLimitBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session); not_null<Main::Session*> session);

View File

@ -378,6 +378,8 @@ bool ResolveUsernameOrPhone(
startToken = params.value(u"startgroup"_q); startToken = params.value(u"startgroup"_q);
} else if (params.contains(u"startchannel"_q)) { } else if (params.contains(u"startchannel"_q)) {
resolveType = ResolveType::AddToChannel; resolveType = ResolveType::AddToChannel;
} else if (params.contains(u"boost"_q)) {
resolveType = ResolveType::Boost;
} }
auto post = ShowAtUnreadMsgId; auto post = ShowAtUnreadMsgId;
auto adminRights = ChatAdminRights(); auto adminRights = ChatAdminRights();
@ -842,6 +844,32 @@ bool ResolveLoginCode(
return true; return true;
} }
bool ResolveBoost(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto params = url_parse_params(
match->captured(1),
qthelp::UrlParamNameTransform::ToLower);
const auto domainParam = params.value(u"domain"_q);
const auto channelParam = params.value(u"channel"_q);
const auto myContext = context.value<ClickHandlerContext>();
using Navigation = Window::SessionNavigation;
controller->window().activate();
controller->showPeerByLink(Navigation::PeerByLinkInfo{
.usernameOrId = (!domainParam.isEmpty()
? std::variant<QString, ChannelId>(domainParam)
: ChannelId(BareId(channelParam.toULongLong()))),
.resolveType = Window::ResolveType::Boost,
.clickFromMessageId = myContext.itemId,
});
return true;
}
} // namespace } // namespace
const std::vector<LocalUrlHandler> &LocalUrlHandlers() { const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
@ -922,6 +950,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^login/?(\\?code=([0-9]+))(&|$)"_q, u"^login/?(\\?code=([0-9]+))(&|$)"_q,
ResolveLoginCode ResolveLoginCode
}, },
{
u"^boost/?\\?(.+)(#|$)"_q,
ResolveBoost,
},
{ {
u"^([^\\?]+)(\\?|#|$)"_q, u"^([^\\?]+)(\\?|#|$)"_q,
HandleUnknown HandleUnknown
@ -1025,8 +1057,13 @@ QString TryConvertUrlToLocal(QString url) {
"/\\d+/?(\\?|$)|" "/\\d+/?(\\?|$)|"
"/\\d+/\\d+/?(\\?|$)" "/\\d+/\\d+/?(\\?|$)"
")"_q, query, matchOptions)) { ")"_q, query, matchOptions)) {
const auto channel = privateMatch->captured(1);
const auto params = query.mid(privateMatch->captured(0).size()).toString(); const auto params = query.mid(privateMatch->captured(0).size()).toString();
const auto base = u"tg://privatepost?channel="_q + privateMatch->captured(1); if (params.indexOf("boost", 0, Qt::CaseInsensitive) >= 0
&& params.toLower().split('&').contains(u"boost"_q)) {
return u"tg://boost?channel="_q + channel;
}
const auto base = u"tg://privatepost?channel="_q + channel;
auto added = QString(); auto added = QString();
if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, privateMatch->captured(2))) { if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, privateMatch->captured(2))) {
added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2));
@ -1044,7 +1081,12 @@ QString TryConvertUrlToLocal(QString url) {
"/s/\\d+/?(\\?|$)|" "/s/\\d+/?(\\?|$)|"
"/\\d+/\\d+/?(\\?|$)" "/\\d+/\\d+/?(\\?|$)"
")"_q, query, matchOptions)) { ")"_q, query, matchOptions)) {
const auto domain = usernameMatch->captured(1);
const auto params = query.mid(usernameMatch->captured(0).size()).toString(); const auto params = query.mid(usernameMatch->captured(0).size()).toString();
if (params.indexOf("boost", 0, Qt::CaseInsensitive) >= 0
&& params.toLower().split('&').contains(u"boost"_q)) {
return u"tg://boost?domain="_q + domain;
}
const auto base = u"tg://resolve?domain="_q + url_encode(usernameMatch->captured(1)); const auto base = u"tg://resolve?domain="_q + url_encode(usernameMatch->captured(1));
auto added = QString(); auto added = QString();
if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {

View File

@ -362,10 +362,11 @@ void FilterRowButton::paintEvent(QPaintEvent *e) {
const auto removed = ranges::count_if( const auto removed = ranges::count_if(
state->rows, state->rows,
&FilterRow::removed); &FilterRow::removed);
if (state->rows.size() < limit() + removed) { const auto count = int(state->rows.size() - removed);
if (count < limit()) {
return false; return false;
} }
controller->show(Box(FiltersLimitBox, session)); controller->show(Box(FiltersLimitBox, session, count));
return true; return true;
}; };
const auto markForRemovalSure = [=](not_null<FilterRowButton*> button) { const auto markForRemovalSure = [=](not_null<FilterRowButton*> button) {

View File

@ -0,0 +1,251 @@
/*
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/boxes/boost_box.h"
#include "lang/lang_keys.h"
#include "ui/effects/fireworks_animation.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
namespace Ui {
namespace {
void StartFireworks(not_null<QWidget*> parent) {
const auto result = Ui::CreateChild<RpWidget>(parent.get());
result->setAttribute(Qt::WA_TransparentForMouseEvents);
result->setGeometry(parent->rect());
result->show();
auto &lifetime = result->lifetime();
const auto animation = lifetime.make_state<FireworksAnimation>([=] {
result->update();
});
result->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(result);
if (!animation->paint(p, result->rect())) {
crl::on_main(result, [=] { delete result; });
}
}, lifetime);
}
} // namespace
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(bool)>)> boost) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
struct State {
rpl::variable<bool> you = false;
bool submitted = false;
};
const auto state = box->lifetime().make_state<State>();
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
const auto addSkip = [&](int skip) {
box->addRow(object_ptr<Ui::FixedHeightWidget>(box, skip));
};
addSkip(st::boostSkipTop);
const auto levelWidth = [&](int add) {
return st::normalFont->width(
tr::lng_boost_level(tr::now, lt_count, data.boost.level + add));
};
const auto paddings = 2 * st::premiumLineTextSkip;
const auto labelLeftWidth = paddings + levelWidth(0);
const auto labelRightWidth = paddings + levelWidth(1);
const auto ratio = [=](int boosts) {
const auto min = std::min(
data.boost.boosts,
data.boost.thisLevelBoosts);
const auto max = std::max({
data.boost.boosts,
data.boost.nextLevelBoosts,
1,
});
Assert(boosts >= min && boosts <= max);
const auto count = (max - min);
const auto index = (boosts - 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 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;
};
const auto min = std::min(
data.boost.boosts,
data.boost.thisLevelBoosts);
const auto now = data.boost.boosts;
const auto max = (data.boost.nextLevelBoosts > min)
? (data.boost.nextLevelBoosts)
: (data.boost.boosts > 0)
? data.boost.boosts
: 1;
auto bubbleRowState = state->you.value(
) | rpl::map([=](bool mine) {
const auto index = mine ? (now + 1) : now;
return Premium::BubbleRowState{
.counter = index,
.ratio = ratio(index),
.dynamic = true,
};
});
Premium::AddBubbleRow(
box->verticalLayout(),
st::boostBubble,
BoxShowFinishes(box),
rpl::duplicate(bubbleRowState),
max,
true,
nullptr,
&st::premiumIconBoost);
addSkip(st::premiumLineTextSkip);
const auto level = [](int level) {
return tr::lng_boost_level(tr::now, lt_count, level);
};
auto ratioValue = std::move(
bubbleRowState
) | rpl::map([](const Premium::BubbleRowState &state) {
return state.ratio;
});
Premium::AddLimitRow(
box->verticalLayout(),
st::boostLimits,
Premium::LimitRowLabels{
.leftLabel = level(data.boost.level),
.rightLabel = level(data.boost.level + 1),
.dynamic = true,
},
std::move(ratioValue));
const auto name = data.name;
auto title = state->you.value() | rpl::map([=](bool your) {
return your
? tr::lng_boost_channel_you_title(
lt_channel,
rpl::single(data.name))
: !data.boost.level
? tr::lng_boost_channel_title_first()
: tr::lng_boost_channel_title_more();
}) | rpl::flatten_latest();
auto text = state->you.value() | rpl::map([=](bool your) {
const auto bold = Ui::Text::Bold(data.name);
const auto now = data.boost.boosts + (your ? 1 : 0);
const auto left = (data.boost.nextLevelBoosts > now)
? (data.boost.nextLevelBoosts - now)
: 0;
auto post = tr::lng_boost_channel_post_stories(
lt_count,
rpl::single(float64(data.boost.level + 1)),
Ui::Text::RichLangValue);
return your
? ((left > 0)
? (!data.boost.level
? tr::lng_boost_channel_you_first(
lt_count,
rpl::single(float64(left)),
Ui::Text::RichLangValue)
: tr::lng_boost_channel_you_more(
lt_count,
rpl::single(float64(left)),
lt_post,
std::move(post),
Ui::Text::RichLangValue))
: (!data.boost.level
? tr::lng_boost_channel_reached_first(
Ui::Text::RichLangValue)
: tr::lng_boost_channel_reached_more(
lt_count,
rpl::single(float64(data.boost.level + 1)),
lt_post,
std::move(post),
Ui::Text::RichLangValue)))
: !data.boost.level
? tr::lng_boost_channel_needs_first(
lt_count,
rpl::single(float64(left)),
lt_channel,
rpl::single(bold),
Ui::Text::RichLangValue)
: tr::lng_boost_channel_needs_more(
lt_count,
rpl::single(float64(left)),
lt_channel,
rpl::single(bold),
lt_post,
std::move(post),
Ui::Text::RichLangValue);
}) | rpl::flatten_latest();
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(title),
st::boostTitle),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
auto submit = state->you.value(
) | rpl::map([](bool mine) {
return mine ? tr::lng_box_ok() : tr::lng_boost_channel_button();
}) | rpl::flatten_latest();
const auto button = box->addButton(rpl::duplicate(submit), [=] {
if (state->submitted) {
return;
} else if (!state->you.current()) {
state->submitted = true;
boost(crl::guard(box, [=](bool success) {
state->submitted = false;
if (success) {
StartFireworks(box->parentWidget());
state->you = true;
}
}));
} else {
box->closeBox();
}
});
rpl::combine(
std::move(submit),
box->widthValue()
) | rpl::start_with_next([=](const QString &, int width) {
const auto &padding = st::boostBox.buttonPadding;
button->resizeToWidth(width
- padding.left()
- padding.right());
button->moveToLeft(padding.left(), button->y());
}, button->lifetime());
}
} // namespace Ui

View File

@ -0,0 +1,31 @@
/*
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
*/
#pragma once
namespace Ui {
class GenericBox;
struct BoostCounters {
int level = 0;
int boosts = 0;
int thisLevelBoosts = 0;
int nextLevelBoosts = 0; // Zero means no next level is available.
};
struct BoostBoxData {
QString name;
BoostCounters boost;
};
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(bool)>)> boost);
} // namespace Ui

View File

@ -13,6 +13,17 @@ PremiumLimits {
boxLabel: FlatLabel; boxLabel: FlatLabel;
nonPremiumBg: color; nonPremiumBg: color;
nonPremiumFg: color; nonPremiumFg: color;
gradientFromLeft: bool;
}
PremiumBubble {
widthLimit: pixels;
height: pixels;
padding: margins;
skip: pixels;
penWidth: pixels;
textSkip: pixels;
tailSize: size;
font: font;
} }
defaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) { defaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) {
@ -26,6 +37,7 @@ defaultPremiumLimits: PremiumLimits {
boxLabel: defaultPremiumBoxLabel; boxLabel: defaultPremiumBoxLabel;
nonPremiumBg: windowBgOver; nonPremiumBg: windowBgOver;
nonPremiumFg: windowFg; nonPremiumFg: windowFg;
gradientFromLeft: false;
} }
// Preview. // Preview.
@ -74,15 +86,16 @@ premiumVideoWidth: 182px;
// Graphics. // Graphics.
premiumBubblePadding: margins(14px, 0px, 14px, 0px); defaultPremiumBubble: PremiumBubble {
premiumBubblePenWidth: 6; widthLimit: 80px;
premiumBubbleHeight: 40px; height: 40px;
premiumBubbleSkip: 8px; padding: margins(14px, 0px, 14px, 0px);
premiumBubbleWidthLimit: 80px; skip: 8px;
premiumBubbleTextSkip: 3px; penWidth: 6px;
premiumBubbleSlideDuration: 1000; textSkip: 3px;
premiumBubbleTailSize: size(21px, 7px); tailSize: size(21px, 7px);
premiumBubbleFont: font(19px); font: font(19px);
}
premiumLineTextSkip: 11px; premiumLineTextSkip: 11px;
premiumInfographicPadding: margins(0px, 10px, 0px, 15px); premiumInfographicPadding: margins(0px, 10px, 0px, 15px);
@ -93,6 +106,7 @@ premiumIconGroups: icon {{ "limits/groups", settingsIconFg }};
premiumIconLinks: icon {{ "limits/links", settingsIconFg }}; premiumIconLinks: icon {{ "limits/links", settingsIconFg }};
premiumIconPins: icon {{ "limits/pins", settingsIconFg }}; premiumIconPins: icon {{ "limits/pins", settingsIconFg }};
premiumIconAccounts: icon {{ "limits/accounts", settingsIconFg }}; premiumIconAccounts: icon {{ "limits/accounts", settingsIconFg }};
premiumIconBoost: icon {{ "limits/boost", settingsIconFg }};
premiumAccountsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { premiumAccountsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {
imageRadius: 27px; imageRadius: 27px;
@ -176,3 +190,43 @@ premiumGiftTerms: FlatLabel(defaultFlatLabel) {
premiumGiftBox: Box(premiumPreviewBox) { premiumGiftBox: Box(premiumPreviewBox) {
buttonPadding: margins(12px, 12px, 12px, 12px); buttonPadding: margins(12px, 12px, 12px, 12px);
} }
boostSkipTop: 37px;
boostLimits: PremiumLimits(defaultPremiumLimits) {
gradientFromLeft: true;
}
boostBubble: PremiumBubble(defaultPremiumBubble) {
height: 32px;
padding: margins(7px, 0px, 11px, 0px);
skip: 5px;
textSkip: 2px;
tailSize: size(14px, 6px);
font: font(16px);
}
boostTitleSkip: 32px;
boostTitle: FlatLabel(defaultFlatLabel) {
minWidth: 40px;
textFg: windowBoldFg;
align: align(top);
maxHeight: 24px;
style: TextStyle(boxTextStyle) {
font: font(17px semibold);
linkFont: font(17px semibold);
linkFontOver: font(17px semibold);
}
}
boostTextSkip: 5px;
boostText: FlatLabel(defaultFlatLabel) {
minWidth: 40px;
align: align(top);
}
boostBottomSkip: 6px;
boostBox: Box(premiumPreviewDoubledLimitsBox) {
buttonPadding: margins(22px, 22px, 22px, 22px);
buttonHeight: 42px;
button: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
font: font(13px semibold);
}
}

View File

@ -38,6 +38,7 @@ using TextFactory = Fn<QString(int)>;
constexpr auto kBubbleRadiusSubtractor = 2; constexpr auto kBubbleRadiusSubtractor = 2;
constexpr auto kDeflectionSmall = 20.; constexpr auto kDeflectionSmall = 20.;
constexpr auto kDeflection = 30.; constexpr auto kDeflection = 30.;
constexpr auto kSlideDuration = crl::time(1000);
constexpr auto kStepBeforeDeflection = 0.75; constexpr auto kStepBeforeDeflection = 0.75;
constexpr auto kStepAfterDeflection = kStepBeforeDeflection constexpr auto kStepAfterDeflection = kStepBeforeDeflection
@ -185,6 +186,7 @@ public:
using EdgeProgress = float64; using EdgeProgress = float64;
Bubble( Bubble(
const style::PremiumBubble &st,
Fn<void()> updateCallback, Fn<void()> updateCallback,
TextFactory textFactory, TextFactory textFactory,
const style::icon *icon, const style::icon *icon,
@ -206,14 +208,13 @@ public:
private: private:
[[nodiscard]] int filledWidth() const; [[nodiscard]] int filledWidth() const;
const style::PremiumBubble &_st;
const Fn<void()> _updateCallback; const Fn<void()> _updateCallback;
const TextFactory _textFactory; const TextFactory _textFactory;
const style::font &_font;
const style::margins &_padding;
const style::icon *_icon; const style::icon *_icon;
NumbersAnimation _numberAnimation; NumbersAnimation _numberAnimation;
const QSize _tailSize;
const int _height; const int _height;
const int _textTop; const int _textTop;
const bool _premiumPossible; const bool _premiumPossible;
@ -227,19 +228,18 @@ private:
}; };
Bubble::Bubble( Bubble::Bubble(
const style::PremiumBubble &st,
Fn<void()> updateCallback, Fn<void()> updateCallback,
TextFactory textFactory, TextFactory textFactory,
const style::icon *icon, const style::icon *icon,
bool premiumPossible) bool premiumPossible)
: _updateCallback(std::move(updateCallback)) : _st(st)
, _updateCallback(std::move(updateCallback))
, _textFactory(std::move(textFactory)) , _textFactory(std::move(textFactory))
, _font(st::premiumBubbleFont)
, _padding(st::premiumBubblePadding)
, _icon(icon) , _icon(icon)
, _numberAnimation(_font, _updateCallback) , _numberAnimation(_st.font, _updateCallback)
, _tailSize(st::premiumBubbleTailSize) , _height(_st.height + _st.tailSize.height())
, _height(st::premiumBubbleHeight + _tailSize.height()) , _textTop((_height - _st.tailSize.height() - _st.font->height) / 2)
, _textTop((_height - _tailSize.height() - _font->height) / 2)
, _premiumPossible(premiumPossible) { , _premiumPossible(premiumPossible) {
_numberAnimation.setDisabledMonospace(true); _numberAnimation.setDisabledMonospace(true);
_numberAnimation.setWidthChangedCallback([=] { _numberAnimation.setWidthChangedCallback([=] {
@ -258,14 +258,14 @@ int Bubble::height() const {
} }
int Bubble::bubbleRadius() const { int Bubble::bubbleRadius() const {
return (_height - _tailSize.height()) / 2 - kBubbleRadiusSubtractor; return (_height - _st.tailSize.height()) / 2 - kBubbleRadiusSubtractor;
} }
int Bubble::filledWidth() const { int Bubble::filledWidth() const {
return _padding.left() return _st.padding.left()
+ _icon->width() + _icon->width()
+ st::premiumBubbleTextSkip + _st.textSkip
+ _padding.right(); + _st.padding.right();
} }
int Bubble::width() const { int Bubble::width() const {
@ -273,7 +273,7 @@ int Bubble::width() const {
} }
int Bubble::countMaxWidth(int maxCounter) const { int Bubble::countMaxWidth(int maxCounter) const {
auto numbers = Ui::NumbersAnimation(_font, [] {}); auto numbers = Ui::NumbersAnimation(_st.font, [] {});
numbers.setDisabledMonospace(true); numbers.setDisabledMonospace(true);
numbers.setDuration(0); numbers.setDuration(0);
numbers.setText(_textFactory(0), 0); numbers.setText(_textFactory(0), 0);
@ -302,18 +302,18 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) {
return; return;
} }
const auto penWidth = st::premiumBubblePenWidth; const auto penWidth = _st.penWidth;
const auto penWidthHalf = penWidth / 2; const auto penWidthHalf = penWidth / 2;
const auto bubbleRect = r - style::margins( const auto bubbleRect = r - style::margins(
penWidthHalf, penWidthHalf,
penWidthHalf, penWidthHalf,
penWidthHalf, penWidthHalf,
_tailSize.height() + penWidthHalf); _st.tailSize.height() + penWidthHalf);
{ {
const auto radius = bubbleRadius(); const auto radius = bubbleRadius();
auto pathTail = QPainterPath(); auto pathTail = QPainterPath();
const auto tailWHalf = _tailSize.width() / 2.; const auto tailWHalf = _st.tailSize.width() / 2.;
const auto progress = _tailEdge; const auto progress = _tailEdge;
const auto tailTop = bubbleRect.y() + bubbleRect.height(); const auto tailTop = bubbleRect.y() + bubbleRect.height();
@ -326,7 +326,7 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) {
const auto tailCenter = tailLeft + tailWHalf; const auto tailCenter = tailLeft + tailWHalf;
const auto tailRight = [&] { const auto tailRight = [&] {
const auto max = bubbleRect.x() + bubbleRect.width(); const auto max = bubbleRect.x() + bubbleRect.width();
const auto right = tailLeft + _tailSize.width(); const auto right = tailLeft + _st.tailSize.width();
const auto bottomMax = max - radius; const auto bottomMax = max - radius;
return (right > bottomMax) return (right > bottomMax)
? std::max(float64(tailCenter), float64(bottomMax)) ? std::max(float64(tailCenter), float64(bottomMax))
@ -335,7 +335,7 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) {
if (_premiumPossible) { if (_premiumPossible) {
pathTail.moveTo(tailLeftFull, tailTop); pathTail.moveTo(tailLeftFull, tailTop);
pathTail.lineTo(tailLeft, tailTop); pathTail.lineTo(tailLeft, tailTop);
pathTail.lineTo(tailCenter, tailTop + _tailSize.height()); pathTail.lineTo(tailCenter, tailTop + _st.tailSize.height());
pathTail.lineTo(tailRight, tailTop); pathTail.lineTo(tailRight, tailTop);
pathTail.lineTo(tailRight, tailTop - radius); pathTail.lineTo(tailRight, tailTop - radius);
pathTail.moveTo(tailLeftFull, tailTop); pathTail.moveTo(tailLeftFull, tailTop);
@ -365,8 +365,8 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) {
} }
} }
p.setPen(st::activeButtonFg); p.setPen(st::activeButtonFg);
p.setFont(_font); p.setFont(_st.font);
const auto iconLeft = r.x() + _padding.left(); const auto iconLeft = r.x() + _st.padding.left();
_icon->paint( _icon->paint(
p, p,
iconLeft, iconLeft,
@ -374,7 +374,7 @@ void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) {
bubbleRect.width()); bubbleRect.width());
_numberAnimation.paint( _numberAnimation.paint(
p, p,
iconLeft + _icon->width() + st::premiumBubbleTextSkip, iconLeft + _icon->width() + _st.textSkip,
r.y() + _textTop, r.y() + _textTop,
width() / 2); width() / 2);
} }
@ -387,8 +387,9 @@ class BubbleWidget final : public Ui::RpWidget {
public: public:
BubbleWidget( BubbleWidget(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::PremiumBubble &st,
TextFactory textFactory, TextFactory textFactory,
int current, rpl::producer<BubbleRowState> state,
int maxCounter, int maxCounter,
bool premiumPossible, bool premiumPossible,
rpl::producer<> showFinishes, rpl::producer<> showFinishes,
@ -398,7 +399,12 @@ protected:
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
private: private:
const int _currentCounter; void animateTo(BubbleRowState state);
const style::PremiumBubble &_st;
BubbleRowState _animatingFrom;
float64 _animatingFromResultRatio = 0.;
rpl::variable<BubbleRowState> _state;
const int _maxCounter; const int _maxCounter;
Bubble _bubble; Bubble _bubble;
const int _maxBubbleWidth; const int _maxBubbleWidth;
@ -419,28 +425,33 @@ private:
BubbleWidget::BubbleWidget( BubbleWidget::BubbleWidget(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::PremiumBubble &st,
TextFactory textFactory, TextFactory textFactory,
int current, rpl::producer<BubbleRowState> state,
int maxCounter, int maxCounter,
bool premiumPossible, bool premiumPossible,
rpl::producer<> showFinishes, rpl::producer<> showFinishes,
const style::icon *icon) const style::icon *icon)
: RpWidget(parent) : RpWidget(parent)
, _currentCounter(current) , _st(st)
, _state(std::move(state))
, _maxCounter(maxCounter) , _maxCounter(maxCounter)
, _bubble([=] { update(); }, std::move(textFactory), icon, premiumPossible) , _bubble(
_st,
[=] { update(); },
std::move(textFactory),
icon,
premiumPossible)
, _maxBubbleWidth(_bubble.countMaxWidth(_maxCounter)) , _maxBubbleWidth(_bubble.countMaxWidth(_maxCounter))
, _premiumPossible(premiumPossible) , _premiumPossible(premiumPossible)
, _deflection(kDeflection) , _deflection(kDeflection)
, _stepBeforeDeflection(kStepBeforeDeflection) , _stepBeforeDeflection(kStepBeforeDeflection)
, _stepAfterDeflection(kStepAfterDeflection) { , _stepAfterDeflection(kStepAfterDeflection) {
const auto resizeTo = [=](int w, int h) { const auto resizeTo = [=](int w, int h) {
_deflection = (w > st::premiumBubbleWidthLimit) _deflection = (w > _st.widthLimit)
? kDeflectionSmall ? kDeflectionSmall
: kDeflection; : kDeflection;
_spaceForDeflection = QSize( _spaceForDeflection = QSize(_st.skip, _st.skip);
st::premiumBubbleSkip,
st::premiumBubbleSkip);
resize(QSize(w, h) + _spaceForDeflection); resize(QSize(w, h) + _spaceForDeflection);
}; };
@ -450,97 +461,113 @@ BubbleWidget::BubbleWidget(
resizeTo(_bubble.width(), _bubble.height()); resizeTo(_bubble.width(), _bubble.height());
}, lifetime()); }, lifetime());
const auto moveEndPoint = _currentCounter / float64(_maxCounter); std::move(
showFinishes
) | rpl::take(1) | rpl::start_with_next([=] {
_state.value(
) | rpl::start_with_next([=](BubbleRowState state) {
animateTo(state);
}, lifetime());
}, lifetime());
}
void BubbleWidget::animateTo(BubbleRowState state) {
const auto parent = parentWidget();
const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { const auto computeLeft = [=](float64 pointRatio, float64 animProgress) {
const auto &padding = st::boxRowPadding; const auto &padding = st::boxRowPadding;
const auto halfWidth = (_maxBubbleWidth / 2); const auto halfWidth = (_maxBubbleWidth / 2);
const auto left = padding.left(); const auto left = padding.left();
const auto right = padding.right(); const auto right = padding.right();
return ((parent->width() - left - right) const auto available = parent->width() - left - right;
* pointRatio const auto delta = (pointRatio - _animatingFromResultRatio);
* animProgress) const auto center = available
- halfWidth * (_animatingFromResultRatio + delta * animProgress);
+ left; return center - halfWidth + left;
}; };
const auto moveEndPoint = state.ratio;
std::move( const auto computeEdge = [=] {
showFinishes return parent->width()
) | rpl::take(1) | rpl::start_with_next([=] { - st::boxRowPadding.right()
const auto computeEdge = [=] { - _maxBubbleWidth;
return parent->width() };
- st::boxRowPadding.right() struct LeftEdge final {
- _maxBubbleWidth; float64 goodPointRatio = 0.;
}; float64 bubbleLeftEdge = 0.;
struct LeftEdge final { };
float64 goodPointRatio = 0.; const auto leftEdge = [&]() -> LeftEdge {
float64 bubbleLeftEdge = 0.; const auto finish = computeLeft(moveEndPoint, 1.);
}; const auto &padding = st::boxRowPadding;
const auto leftEdge = [&]() -> LeftEdge { if (finish <= padding.left()) {
const auto finish = computeLeft(moveEndPoint, 1.); const auto halfWidth = (_maxBubbleWidth / 2);
const auto &padding = st::boxRowPadding; const auto goodPointRatio = float64(halfWidth)
if (finish <= padding.left()) { / (parent->width() - padding.left() - padding.right());
const auto halfWidth = (_maxBubbleWidth / 2); const auto bubbleLeftEdge = (padding.left() - finish)
const auto goodPointRatio = float64(halfWidth) / (_maxBubbleWidth / 2.);
/ (parent->width() - padding.left() - padding.right()); return { goodPointRatio, bubbleLeftEdge };
const auto bubbleLeftEdge = (padding.left() - finish)
/ (_maxBubbleWidth / 2.);
return { goodPointRatio, bubbleLeftEdge };
}
return {};
}();
const auto checkBubbleRightEdge = [&]() -> Bubble::EdgeProgress {
const auto finish = computeLeft(moveEndPoint, 1.);
const auto edge = computeEdge();
return (finish >= edge)
? (finish - edge) / (_maxBubbleWidth / 2.)
: 0.;
};
const auto bubbleRightEdge = checkBubbleRightEdge();
_ignoreDeflection = bubbleRightEdge || leftEdge.goodPointRatio;
if (_ignoreDeflection) {
_stepBeforeDeflection = 1.;
_stepAfterDeflection = 1.;
} }
const auto resultMoveEndPoint = leftEdge.goodPointRatio return {};
? leftEdge.goodPointRatio }();
: moveEndPoint; const auto checkBubbleRightEdge = [&]() -> Bubble::EdgeProgress {
_bubble.setFlipHorizontal(leftEdge.bubbleLeftEdge); const auto finish = computeLeft(moveEndPoint, 1.);
const auto edge = computeEdge();
return (finish >= edge)
? (finish - edge) / (_maxBubbleWidth / 2.)
: 0.;
};
const auto bubbleRightEdge = checkBubbleRightEdge();
_ignoreDeflection = !_state.current().dynamic
&& (bubbleRightEdge || leftEdge.goodPointRatio);
if (_ignoreDeflection) {
_stepBeforeDeflection = 1.;
_stepAfterDeflection = 1.;
}
const auto resultMoveEndPoint = leftEdge.goodPointRatio
? leftEdge.goodPointRatio
: moveEndPoint;
_bubble.setFlipHorizontal(leftEdge.bubbleLeftEdge);
_appearanceAnimation.start([=](float64 value) { const auto duration = kSlideDuration
const auto moveProgress = std::clamp( * (_ignoreDeflection ? kStepBeforeDeflection : 1.)
(value / _stepBeforeDeflection), * ((_state.current().ratio < 0.001) ? 0.5 : 1.);
0., _appearanceAnimation.start([=](float64 value) {
1.); if (!_appearanceAnimation.animating()) {
const auto counterProgress = std::clamp( _animatingFrom = state;
(value / _stepAfterDeflection), _animatingFromResultRatio = resultMoveEndPoint;
0., }
1.); const auto moveProgress = std::clamp(
moveToLeft( (value / _stepBeforeDeflection),
computeLeft(resultMoveEndPoint, moveProgress) 0.,
- (_maxBubbleWidth / 2.) * bubbleRightEdge, 1.);
0); const auto counterProgress = std::clamp(
(value / _stepAfterDeflection),
0.,
1.);
moveToLeft(
std::max(
int(base::SafeRound(
(computeLeft(resultMoveEndPoint, moveProgress)
- (_maxBubbleWidth / 2.) * bubbleRightEdge))),
0),
0);
const auto counter = int(0 + counterProgress * _currentCounter); const auto now = _animatingFrom.counter
// if (!(counter % 4) || counterProgress > 0.8) { + counterProgress * (state.counter - _animatingFrom.counter);
_bubble.setCounter(counter); _bubble.setCounter(int(base::SafeRound(now)));
// }
const auto edgeProgress = (leftEdge.bubbleLeftEdge const auto edgeProgress = leftEdge.bubbleLeftEdge
? leftEdge.bubbleLeftEdge ? leftEdge.bubbleLeftEdge
: bubbleRightEdge) * value; : (bubbleRightEdge * value);
_bubble.setTailEdge(edgeProgress); _bubble.setTailEdge(edgeProgress);
update(); update();
}, },
0., 0.,
1., 1.,
st::premiumBubbleSlideDuration duration,
* (_ignoreDeflection ? kStepBeforeDeflection : 1.), anim::easeOutCirc);
anim::easeOutCirc);
}, lifetime());
} }
void BubbleWidget::paintEvent(QPaintEvent *e) { void BubbleWidget::paintEvent(QPaintEvent *e) {
if (_bubble.counter() <= 0) { if (_bubble.counter() < 0) {
return; return;
} }
@ -561,10 +588,11 @@ void BubbleWidget::paintEvent(QPaintEvent *e) {
_cachedGradient = std::move(gradient); _cachedGradient = std::move(gradient);
const auto progress = _appearanceAnimation.value(1.); const auto progress = _appearanceAnimation.value(1.);
const auto scaleProgress = std::clamp( const auto finalScale = (_animatingFromResultRatio > 0.)
(progress / _stepBeforeDeflection), || (_state.current().ratio < 0.001);
0., const auto scaleProgress = finalScale
1.); ? 1.
: std::clamp((progress / _stepBeforeDeflection), 0., 1.);
const auto scale = scaleProgress; const auto scale = scaleProgress;
const auto rotationProgress = std::clamp( const auto rotationProgress = std::clamp(
(progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection), (progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection),
@ -608,6 +636,12 @@ public:
QString min, QString min,
float64 ratio); float64 ratio);
Line(
not_null<Ui::RpWidget*> parent,
const style::PremiumLimits &st,
LimitRowLabels labels,
rpl::producer<float64> ratio);
void setColorOverride(QBrush brush); void setColorOverride(QBrush brush);
protected: protected:
@ -618,16 +652,16 @@ private:
const style::PremiumLimits &_st; const style::PremiumLimits &_st;
int _leftWidth = 0;
int _rightWidth = 0;
QPixmap _leftPixmap; QPixmap _leftPixmap;
QPixmap _rightPixmap; QPixmap _rightPixmap;
Ui::Text::String _leftText; float64 _ratio = 0.;
Ui::Text::String _rightText; Ui::Animations::Simple _animation;
Ui::Text::String _rightLabel;
Ui::Text::String _leftLabel; Ui::Text::String _leftLabel;
Ui::Text::String _leftText;
Ui::Text::String _rightLabel;
Ui::Text::String _rightText;
bool _dynamic = false;
std::optional<QBrush> _overrideBrush; std::optional<QBrush> _overrideBrush;
@ -654,24 +688,47 @@ Line::Line(
QString max, QString max,
QString min, QString min,
float64 ratio) float64 ratio)
: Line(parent, st, LimitRowLabels{
.leftLabel = tr::lng_premium_free(tr::now),
.leftCount = min,
.rightLabel = tr::lng_premium(tr::now),
.rightCount = max,
}, rpl::single(ratio)) {
}
Line::Line(
not_null<Ui::RpWidget*> parent,
const style::PremiumLimits &st,
LimitRowLabels labels,
rpl::producer<float64> ratio)
: Ui::RpWidget(parent) : Ui::RpWidget(parent)
, _st(st) , _st(st)
, _leftText(st::semiboldTextStyle, tr::lng_premium_free(tr::now)) , _leftLabel(st::semiboldTextStyle, labels.leftLabel)
, _rightText(st::semiboldTextStyle, tr::lng_premium(tr::now)) , _leftText(st::semiboldTextStyle, labels.leftCount)
, _rightLabel(st::semiboldTextStyle, max) , _rightLabel(st::semiboldTextStyle, labels.rightLabel)
, _leftLabel(st::semiboldTextStyle, min) { , _rightText(st::semiboldTextStyle, labels.rightCount)
, _dynamic(labels.dynamic) {
resize(width(), st::requestsAcceptButton.height); resize(width(), st::requestsAcceptButton.height);
sizeValue( std::move(ratio) | rpl::start_with_next([=](float64 ratio) {
) | rpl::start_with_next([=](const QSize &s) { if (width() > 0) {
if (s.isEmpty()) { const auto from = _animation.value(_ratio);
return; const auto duration = kSlideDuration * kStepBeforeDeflection;
_animation.start([=] {
update();
}, from, ratio, duration, anim::easeOutCirc);
} }
_leftWidth = int(base::SafeRound(s.width() * ratio)); _ratio = ratio;
_rightWidth = (s.width() - _leftWidth); }, lifetime());
recache(s);
sizeValue(
) | rpl::filter([](QSize size) {
return !size.isEmpty();
}) | rpl::start_with_next([=](QSize size) {
recache(size);
update(); update();
}, lifetime()); }, lifetime());
} }
void Line::setColorOverride(QBrush brush) { void Line::setColorOverride(QBrush brush) {
@ -685,52 +742,64 @@ void Line::setColorOverride(QBrush brush) {
void Line::paintEvent(QPaintEvent *event) { void Line::paintEvent(QPaintEvent *event) {
Painter p(this); Painter p(this);
p.drawPixmap(0, 0, _leftPixmap); const auto ratio = _animation.value(_ratio);
p.drawPixmap(_leftWidth, 0, _rightPixmap); const auto left = int(base::SafeRound(ratio * width()));
const auto dpr = int(_leftPixmap.devicePixelRatio());
const auto height = _leftPixmap.height() / dpr;
p.drawPixmap(
QRect(0, 0, left, height),
_leftPixmap,
QRect(0, 0, left * dpr, height * dpr));
p.drawPixmap(
QRect(left, 0, width() - left, height),
_rightPixmap,
QRect(left * dpr, 0, (width() - left) * dpr, height * dpr));
p.setFont(st::normalFont); p.setFont(st::normalFont);
const auto textPadding = st::premiumLineTextSkip; const auto textPadding = st::premiumLineTextSkip;
const auto textTop = (height() - _leftText.minHeight()) / 2; const auto textTop = (height - _leftLabel.minHeight()) / 2;
const auto leftMinWidth = _leftLabel.maxWidth() const auto leftMinWidth = _leftLabel.maxWidth()
+ _leftText.maxWidth() + _leftText.maxWidth()
+ 3 * textPadding; + 3 * textPadding;
if (_leftWidth >= leftMinWidth) { const auto pen = [&](bool gradient) {
p.setPen(_st.nonPremiumFg); return gradient ? st::activeButtonFg : _st.nonPremiumFg;
_leftLabel.drawRight( };
if (!_dynamic && left >= leftMinWidth) {
p.setPen(pen(_st.gradientFromLeft));
_leftLabel.drawLeft(
p, p,
textPadding, textPadding,
textTop, textTop,
_leftWidth - textPadding, left - textPadding,
_leftWidth, left);
_leftText.drawRight(
p,
textPadding,
textTop,
left - textPadding,
left,
style::al_right); style::al_right);
_leftText.drawLeft(
p,
textPadding,
textTop,
_leftWidth - textPadding,
_leftWidth);
} }
const auto rightMinWidth = 2 * _rightLabel.maxWidth() const auto right = width() - left;
const auto rightMinWidth = 2 * _rightText.maxWidth()
+ 3 * textPadding; + 3 * textPadding;
if (_rightWidth >= rightMinWidth) { if (!_dynamic && right >= rightMinWidth) {
p.setPen(st::activeButtonFg); p.setPen(pen(!_st.gradientFromLeft));
_rightLabel.drawRight( _rightLabel.drawLeftElided(
p,
left + textPadding,
textTop,
(right - _rightText.countWidth(right) - textPadding * 2),
right);
_rightText.drawRight(
p, p,
textPadding, textPadding,
textTop, textTop,
_rightWidth - textPadding, right - textPadding,
width(), width(),
style::al_right); style::al_right);
_rightText.drawLeftElided(
p,
_leftWidth + textPadding,
textTop,
(_rightWidth
- _rightLabel.countWidth(_rightWidth)
- textPadding * 2),
_rightWidth);
} }
} }
@ -750,40 +819,46 @@ void Line::recache(const QSize &s) {
result.addRoundedRect(r(width), st::buttonRadius, st::buttonRadius); result.addRoundedRect(r(width), st::buttonRadius, st::buttonRadius);
return result; return result;
}; };
const auto width = s.width();
const auto fill = [&](QPainter &p, QPainterPath path, bool gradient) {
if (!gradient) {
p.fillPath(path, _st.nonPremiumBg);
} else if (_overrideBrush) {
p.fillPath(path, *_overrideBrush);
} else {
p.fillPath(path, QBrush(ComputeGradient(this, 0, width)));
}
};
const auto textPadding = st::premiumLineTextSkip;
const auto textTop = (s.height() - _leftLabel.minHeight()) / 2;
const auto rwidth = _rightLabel.maxWidth();
const auto pen = [&](bool gradient) {
return gradient ? st::activeButtonFg : _st.nonPremiumFg;
};
{ {
auto leftPixmap = pixmap(_leftWidth); auto leftPixmap = pixmap(width);
auto p = QPainter(&leftPixmap); auto p = Painter(&leftPixmap);
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);
auto pathRect = QPainterPath(); fill(p, pathRound(width), _st.gradientFromLeft);
auto halfRect = r(_leftWidth); if (_dynamic) {
halfRect.setLeft(halfRect.center().x()); p.setFont(st::normalFont);
pathRect.addRect(halfRect); p.setPen(pen(_st.gradientFromLeft));
_leftLabel.drawLeft(p, textPadding, textTop, width, width);
p.fillPath(pathRound(_leftWidth) + pathRect, _st.nonPremiumBg); _rightLabel.drawRight(p, textPadding, textTop, rwidth, width);
}
_leftPixmap = std::move(leftPixmap); _leftPixmap = std::move(leftPixmap);
} }
{ {
auto rightPixmap = pixmap(_rightWidth); auto rightPixmap = pixmap(width);
auto p = QPainter(&rightPixmap); auto p = Painter(&rightPixmap);
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);
auto pathRect = QPainterPath(); fill(p, pathRound(width), !_st.gradientFromLeft);
auto halfRect = r(_rightWidth); if (_dynamic) {
halfRect.setRight(halfRect.center().x()); p.setFont(st::normalFont);
pathRect.addRect(halfRect); p.setPen(pen(!_st.gradientFromLeft));
_leftLabel.drawLeft(p, textPadding, textTop, width, width);
if (_overrideBrush) { _rightLabel.drawRight(p, textPadding, textTop, rwidth, width);
p.fillPath(pathRound(_rightWidth) + pathRect, *_overrideBrush);
} else {
auto gradient = ComputeGradient(
this,
_leftPixmap.width() / style::DevicePixelRatio(),
_rightWidth);
p.fillPath(
pathRound(_rightWidth) + pathRect,
QBrush(std::move(gradient)));
} }
_rightPixmap = std::move(rightPixmap); _rightPixmap = std::move(rightPixmap);
} }
} }
@ -792,6 +867,7 @@ void Line::recache(const QSize &s) {
void AddBubbleRow( void AddBubbleRow(
not_null<Ui::VerticalLayout*> parent, not_null<Ui::VerticalLayout*> parent,
const style::PremiumBubble &st,
rpl::producer<> showFinishes, rpl::producer<> showFinishes,
int min, int min,
int current, int current,
@ -799,12 +875,36 @@ void AddBubbleRow(
bool premiumPossible, bool premiumPossible,
std::optional<tr::phrase<lngtag_count>> phrase, std::optional<tr::phrase<lngtag_count>> phrase,
const style::icon *icon) { const style::icon *icon) {
AddBubbleRow(
parent,
st,
std::move(showFinishes),
rpl::single(BubbleRowState{
.counter = current,
.ratio = (current - min) / float64(max - min),
}),
max,
premiumPossible,
ProcessTextFactory(phrase),
icon);
}
void AddBubbleRow(
not_null<Ui::VerticalLayout*> parent,
const style::PremiumBubble &st,
rpl::producer<> showFinishes,
rpl::producer<BubbleRowState> state,
int max,
bool premiumPossible,
Fn<QString(int)> text,
const style::icon *icon) {
const auto container = parent->add( const auto container = parent->add(
object_ptr<Ui::FixedHeightWidget>(parent, 0)); object_ptr<Ui::FixedHeightWidget>(parent, 0));
const auto bubble = Ui::CreateChild<BubbleWidget>( const auto bubble = Ui::CreateChild<BubbleWidget>(
container, container,
ProcessTextFactory(phrase), st,
current, text ? std::move(text) : ProcessTextFactory(std::nullopt),
std::move(state),
max, max,
premiumPossible, premiumPossible,
std::move(showFinishes), std::move(showFinishes),
@ -844,6 +944,16 @@ void AddLimitRow(
ratio); ratio);
} }
void AddLimitRow(
not_null<Ui::VerticalLayout*> parent,
const style::PremiumLimits &st,
LimitRowLabels labels,
rpl::producer<float64> ratio) {
parent->add(
object_ptr<Line>(parent, st, std::move(labels), std::move(ratio)),
st::boxRowPadding);
}
void AddAccountsRow( void AddAccountsRow(
not_null<Ui::VerticalLayout*> parent, not_null<Ui::VerticalLayout*> parent,
AccountsRowArgs &&args) { AccountsRowArgs &&args) {

View File

@ -28,6 +28,7 @@ namespace style {
struct RoundImageCheckbox; struct RoundImageCheckbox;
struct PremiumOption; struct PremiumOption;
struct TextStyle; struct TextStyle;
struct PremiumBubble;
} // namespace style } // namespace style
namespace Ui { namespace Ui {
@ -42,6 +43,7 @@ inline constexpr auto kLimitRowRatio = 0.5;
void AddBubbleRow( void AddBubbleRow(
not_null<Ui::VerticalLayout*> parent, not_null<Ui::VerticalLayout*> parent,
const style::PremiumBubble &st,
rpl::producer<> showFinishes, rpl::producer<> showFinishes,
int min, int min,
int current, int current,
@ -50,6 +52,21 @@ void AddBubbleRow(
std::optional<tr::phrase<lngtag_count>> phrase, std::optional<tr::phrase<lngtag_count>> phrase,
const style::icon *icon); const style::icon *icon);
struct BubbleRowState {
int counter = 0;
float64 ratio = 0.;
bool dynamic = false;
};
void AddBubbleRow(
not_null<Ui::VerticalLayout*> parent,
const style::PremiumBubble &st,
rpl::producer<> showFinishes,
rpl::producer<BubbleRowState> state,
int max,
bool premiumPossible,
Fn<QString(int)> text,
const style::icon *icon);
void AddLimitRow( void AddLimitRow(
not_null<Ui::VerticalLayout*> parent, not_null<Ui::VerticalLayout*> parent,
const style::PremiumLimits &st, const style::PremiumLimits &st,
@ -65,6 +82,19 @@ void AddLimitRow(
int min = 0, int min = 0,
float64 ratio = kLimitRowRatio); float64 ratio = kLimitRowRatio);
struct LimitRowLabels {
QString leftLabel;
QString leftCount;
QString rightLabel;
QString rightCount;
bool dynamic = false;
};
void AddLimitRow(
not_null<Ui::VerticalLayout*> parent,
const style::PremiumLimits &st,
LimitRowLabels labels,
rpl::producer<float64> ratio);
struct AccountsRowArgs final { struct AccountsRowArgs final {
std::shared_ptr<Ui::RadiobuttonGroup> group; std::shared_ptr<Ui::RadiobuttonGroup> group;
const style::RoundImageCheckbox &st; const style::RoundImageCheckbox &st;

View File

@ -316,7 +316,10 @@ base::unique_qptr<Ui::SideBarButton> FiltersMenu::prepareButton(
if (_reordering) { if (_reordering) {
return; return;
} else if (raw->locked()) { } else if (raw->locked()) {
_session->show(Box(FiltersLimitBox, &_session->session())); _session->show(Box(
FiltersLimitBox,
&_session->session(),
std::nullopt));
} else if (id >= 0) { } else if (id >= 0) {
_session->setActiveChatsFilter(id); _session->setActiveChatsFilter(id);
} else { } else {

View File

@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/text/format_values.h" // Ui::FormatPhone. #include "ui/text/format_values.h" // Ui::FormatPhone.
#include "ui/delayed_activation.h" #include "ui/delayed_activation.h"
#include "ui/boxes/boost_box.h"
#include "ui/chat/attach/attach_bot_webview.h" #include "ui/chat/attach/attach_bot_webview.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h" #include "ui/chat/chat_theme.h"
@ -80,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/themes/window_theme.h" #include "window/themes/window_theme.h"
#include "window/window_peer_menu.h" #include "window/window_peer_menu.h"
#include "settings/settings_main.h" #include "settings/settings_main.h"
#include "settings/settings_premium.h"
#include "settings/settings_privacy_security.h" #include "settings/settings_privacy_security.h"
#include "styles/style_window.h" #include "styles/style_window.h"
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
@ -554,6 +556,8 @@ void SessionNavigation::showPeerByLinkResolved(
} else { } else {
showPeerInfo(peer, params); showPeerInfo(peer, params);
} }
} else if (resolveType == ResolveType::Boost && peer->isBroadcast()) {
resolveBoostState(peer->asChannel());
} else { } else {
// Show specific posts only in channels / supergroups. // Show specific posts only in channels / supergroups.
const auto msgId = peer->isChannel() const auto msgId = peer->isChannel()
@ -614,6 +618,145 @@ void SessionNavigation::showPeerByLinkResolved(
} }
} }
void SessionNavigation::resolveBoostState(not_null<ChannelData*> channel) {
if (_boostStateResolving == channel) {
return;
}
_boostStateResolving = channel;
_api.request(MTPstories_GetBoostsStatus(
channel->input
)).done([=](const MTPstories_BoostsStatus &result) {
_boostStateResolving = nullptr;
const auto &data = result.data();
const auto submit = [=](Fn<void(bool)> done) {
applyBoost(channel, done);
};
const auto next = data.vnext_level_boosts().value_or_empty();
uiShow()->show(Box(Ui::BoostBox, Ui::BoostBoxData{
.name = channel->name(),
.boost = {
.level = data.vlevel().v,
.boosts = data.vboosts().v,
.thisLevelBoosts = data.vcurrent_level_boosts().v,
.nextLevelBoosts = next,
},
}, submit));
}).fail([=](const MTP::Error &error) {
_boostStateResolving = nullptr;
showToast(u"Error: "_q + error.type());
}).send();
}
void SessionNavigation::applyBoost(
not_null<ChannelData*> channel,
Fn<void(bool)> done) {
_api.request(MTPstories_CanApplyBoost(
channel->input
)).done([=](const MTPstories_CanApplyBoostResult &result) {
result.match([&](const MTPDstories_canApplyBoostOk &) {
applyBoostChecked(channel, done);
}, [&](const MTPDstories_canApplyBoostReplace &data) {
_session->data().processChats(data.vchats());
const auto peer = _session->data().peer(
peerFromMTP(data.vcurrent_boost()));
replaceBoostConfirm(peer, channel, done);
});
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (type == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
const auto jumpToPremium = [=] {
const auto id = peerToChannel(channel->id).bare;
Settings::ShowPremium(
parentController(),
"channel_boost__" + QString::number(id));
};
uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_boost_error_premium_text(
Ui::Text::RichLangValue),
.confirmed = jumpToPremium,
.confirmText = tr::lng_boost_error_premium_yes(),
.title = tr::lng_boost_error_premium_title(),
}));
} else if (type == u"PREMIUM_GIFTED_NOT_ALLOWED"_q) {
uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_boost_error_gifted_text(
Ui::Text::RichLangValue),
.title = tr::lng_boost_error_gifted_title(),
.inform = true,
}));
} else if (type == u"BOOST_NOT_MODIFIED"_q) {
uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_boost_error_already_text(
Ui::Text::RichLangValue),
.title = tr::lng_boost_error_already_title(),
.inform = true,
}));
} else if (type.startsWith(u"FLOOD_WAIT_"_q)) {
const auto seconds = type.mid(u"FLOOD_WAIT_"_q.size()).toInt();
const auto days = seconds / 86400;
const auto hours = seconds / 3600;
const auto minutes = seconds / 60;
uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_boost_error_flood_text(
lt_left,
rpl::single(Ui::Text::Bold((days > 1)
? tr::lng_days(tr::now, lt_count, days)
: (hours > 1)
? tr::lng_hours(tr::now, lt_count, hours)
: (minutes > 1)
? tr::lng_minutes(tr::now, lt_count, minutes)
: tr::lng_seconds(tr::now, lt_count, seconds))),
Ui::Text::RichLangValue),
.title = tr::lng_boost_error_flood_title(),
.inform = true,
}));
} else {
showToast(u"Error: "_q + type);
}
done(false);
}).handleFloodErrors().send();
}
void SessionNavigation::replaceBoostConfirm(
not_null<PeerData*> from,
not_null<ChannelData*> channel,
Fn<void(bool)> done) {
const auto forwarded = std::make_shared<bool>(false);
const auto confirmed = [=](Fn<void()> close) {
*forwarded = true;
applyBoostChecked(channel, done);
close();
};
const auto box = uiShow()->show(Ui::MakeConfirmBox({
.text = tr::lng_boost_now_instead(
lt_channel,
rpl::single(Ui::Text::Bold(from->name())),
lt_other,
rpl::single(Ui::Text::Bold(channel->name())),
Ui::Text::WithEntities),
.confirmed = confirmed,
.confirmText = tr::lng_boost_now_replace(),
}));
box->boxClosing() | rpl::filter([=] {
return !*forwarded;
}) | rpl::start_with_next([=] {
done(false);
}, box->lifetime());
}
void SessionNavigation::applyBoostChecked(
not_null<ChannelData*> channel,
Fn<void(bool)> done) {
_api.request(MTPstories_ApplyBoost(
channel->input
)).done([=](const MTPBool &result) {
done(true);
}).fail([=](const MTP::Error &error) {
showToast(u"Error: "_q + error.type());
done(false);
}).send();
}
void SessionNavigation::joinVoiceChatFromLink( void SessionNavigation::joinVoiceChatFromLink(
not_null<PeerData*> peer, not_null<PeerData*> peer,
const PeerByLinkInfo &info) { const PeerByLinkInfo &info) {

View File

@ -100,6 +100,7 @@ enum class ResolveType {
AddToChannel, AddToChannel,
ShareGame, ShareGame,
Mention, Mention,
Boost,
}; };
struct PeerThemeOverride { struct PeerThemeOverride {
@ -311,6 +312,16 @@ private:
not_null<PeerData*> peer, not_null<PeerData*> peer,
const PeerByLinkInfo &info); const PeerByLinkInfo &info);
void resolveBoostState(not_null<ChannelData*> channel);
void applyBoost(not_null<ChannelData*> channel, Fn<void(bool)> done);
void replaceBoostConfirm(
not_null<PeerData*> from,
not_null<ChannelData*> channel,
Fn<void(bool)> done);
void applyBoostChecked(
not_null<ChannelData*> channel,
Fn<void(bool)> done);
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
MTP::Sender _api; MTP::Sender _api;
@ -321,6 +332,8 @@ private:
MsgId _showingRepliesRootId = 0; MsgId _showingRepliesRootId = 0;
mtpRequestId _showingRepliesRequestId = 0; mtpRequestId _showingRepliesRequestId = 0;
ChannelData *_boostStateResolving = nullptr;
}; };
class SessionController : public SessionNavigation { class SessionController : public SessionNavigation {

View File

@ -160,6 +160,8 @@ PRIVATE
ui/boxes/auto_delete_settings.cpp ui/boxes/auto_delete_settings.cpp
ui/boxes/auto_delete_settings.h ui/boxes/auto_delete_settings.h
ui/boxes/boost_box.cpp
ui/boxes/boost_box.h
ui/boxes/calendar_box.cpp ui/boxes/calendar_box.cpp
ui/boxes/calendar_box.h ui/boxes/calendar_box.h
ui/boxes/choose_date_time.cpp ui/boxes/choose_date_time.cpp