Merge tag 'v4.11.6' into dev

# Conflicts:
#	Telegram/CMakeLists.txt
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/version.h
#	Telegram/lib_ui
This commit is contained in:
ZavaruKitsu 2023-11-10 13:26:08 +03:00
commit a5b446f6df
114 changed files with 2632 additions and 906 deletions

View file

@ -288,6 +288,8 @@ PRIVATE
boxes/peers/peer_short_info_box.h
boxes/peers/prepare_short_info_box.cpp
boxes/peers/prepare_short_info_box.h
boxes/peers/replace_boost_box.cpp
boxes/peers/replace_boost_box.h
boxes/about_box.cpp
boxes/about_box.h
boxes/about_sponsored_box.cpp
@ -1922,6 +1924,7 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED)
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "ayugram.png")
install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "telegram-symbolic.svg")
install(FILES "../lib/xdg/ayugram.desktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ayugram.desktop.service" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ayugram.desktop.metainfo.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo")

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -2030,6 +2030,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_gift_terms_link" = "here";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_again_button" = "Boost Again";
"lng_boost_level#one" = "Level {count}";
"lng_boost_level#other" = "Level {count}";
"lng_boost_channel_title_first" = "Enable stories for channel";
@ -2051,6 +2052,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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_need_more" = "More boosts needed";
"lng_boost_need_more_text#one" = "To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts.";
"lng_boost_need_more_text#other" = "To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts.";
"lng_boost_need_more_again#one" = "To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boost.";
"lng_boost_need_more_again#other" = "To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boosts.";
"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!";
@ -2060,6 +2066,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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_boost_reassign_title" = "Reassign boost";
"lng_boost_reassign_text" = "To boost {channel}, reassign a previous boost or {gift}.";
"lng_boost_reassign_gift#one" = "gift **Telegram Premium** to a friend to get **{count}** additional boost";
"lng_boost_reassign_gift#other" = "gift **Telegram Premium** to a friend to get **{count}** additional boosts";
"lng_boost_remove_title" = "Remove your boost from";
"lng_boost_reassign_button" = "Reassign";
"lng_boost_available_in" = "available in {duration}";
"lng_boost_available_in_toast#one" = "Wait until the boost is available or get **{count}** more boost by gifting a **Telegram Premium** subscription.";
"lng_boost_available_in_toast#other" = "Wait until the boost is available or get **{count}** more boosts by gifting a **Telegram Premium** subscription.";
"lng_boost_reassign_done#one" = "{count} boost is reassigned from {channels}.";
"lng_boost_reassign_done#other" = "{count} boosts are reassigned from {channels}.";
"lng_boost_reassign_channels#one" = "{count} channel";
"lng_boost_reassign_channels#other" = "{count} channels";
"lng_boost_channel_title_color" = "Enable colors";
"lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color.";
@ -2097,6 +2116,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started or to users from specific countries.";
"lng_giveaway_start" = "Start Giveaway";
"lng_giveaway_award" = "Gift Premium";
"lng_giveaway_start_sure" = "Are you sure you want to start this prepaid giveaway now? This action cannot be undone.";
"lng_giveaway_date_title" = "Date when giveaway ends";
"lng_giveaway_date" = "Date and Time";
"lng_giveaway_date_about#one" = "Choose when {count} subscriber of your channel will be randomly selected to receive Telegram Premium.";
@ -2199,6 +2219,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_link_used_about" = "This link was used to activate\na **Telegram Premium** subscription.";
"lng_gift_link_used_footer" = "This link was used on {date}.";
"lng_gift_link_expired" = "Gift code link expired";
"lng_gift_link_pending_about" = "This link allows {user} to activate\na **Telegram Premium** subscription.";
"lng_gift_link_pending_toast" = "Only the recipient can see the link.";
"lng_gift_link_pending_footer" = "This link hasn't been activated yet.";
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts.";
@ -4325,8 +4348,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boosts_existing" = "Existing boosts";
"lng_boosts_premium_audience" = "Premium subscribers";
"lng_boosts_next_level" = "Boosts to level up";
"lng_boosts_list_title#one" = "{count} booster";
"lng_boosts_list_title#other" = "{count} boosters";
"lng_boosts_list_title#one" = "{count} Boost";
"lng_boosts_list_title#other" = "{count} Boosts";
"lng_boosts_list_subtext" = "Your channel is currently boosted by these users.";
"lng_boosts_show_more_boosts#one" = "Show {count} More Boosts";
"lng_boosts_show_more_boosts#other" = "Show {count} More Boosts";
@ -4343,6 +4366,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boosts_list_tab_gifts#one" = "{count} Gifts";
"lng_boosts_list_tab_gifts#other" = "{count} Gifts";
"lng_boosts_prepaid_giveaway_title" = "Prepaid giveaways";
"lng_boosts_prepaid_giveaway_single" = "Prepaid giveaway";
"lng_boosts_prepaid_giveaway_quantity#one" = "{count} Telegram Premium";
"lng_boosts_prepaid_giveaway_quantity#other" = "{count} Telegram Premium";
"lng_boosts_prepaid_giveaway_moths#one" = "{count}-month subscriptions";
"lng_boosts_prepaid_giveaway_moths#other" = "{count}-month subscriptions";
"lng_boosts_prepaid_giveaway_status#one" = "{count} subscription {duration}";
"lng_boosts_prepaid_giveaway_status#other" = "{count} subscriptions {duration}";
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="4.11.5.0" />
Version="4.11.6.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,11,5,0
PRODUCTVERSION 4,11,5,0
FILEVERSION 4,11,6,0
PRODUCTVERSION 4,11,6,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "4.11.5.0"
VALUE "FileVersion", "4.11.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.11.5.0"
VALUE "ProductVersion", "4.11.6.0"
END
END
BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,11,5,0
PRODUCTVERSION 4,11,5,0
FILEVERSION 4,11,6,0
PRODUCTVERSION 4,11,6,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "4.11.5.0"
VALUE "FileVersion", "4.11.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.11.5.0"
VALUE "ProductVersion", "4.11.6.0"
END
END
BLOCK "VarFileInfo"

View file

@ -390,18 +390,44 @@ rpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::request() {
};
}
rpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::applyPrepaid(
const Payments::InvoicePremiumGiftCode &invoice,
uint64 prepaidId) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto channel = _peer->asChannel();
if (!channel) {
return lifetime;
}
_api.request(MTPpayments_LaunchPrepaidGiveaway(
_peer->input,
MTP_long(prepaidId),
Payments::InvoicePremiumGiftCodeGiveawayToTL(invoice)
)).done([=](const MTPUpdates &result) {
_peer->session().api().applyUpdates(result);
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
const std::vector<int> &PremiumGiftCodeOptions::availablePresets() const {
return _availablePresets;
}
[[nodiscard]] int PremiumGiftCodeOptions::monthsFromPreset(int monthsIndex) {
return _optionsForOnePerson.months[monthsIndex];
}
Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
int users,
int monthsIndex) {
int months) {
const auto randomId = base::RandomValue<uint64>();
const auto token = Token{
users,
_optionsForOnePerson.months[monthsIndex],
};
const auto token = Token{ users, months };
const auto &store = _stores[token];
return Payments::InvoicePremiumGiftCode{
.randomId = randomId,

View file

@ -152,9 +152,13 @@ public:
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::SubscriptionOptions options(int amount);
[[nodiscard]] const std::vector<int> &availablePresets() const;
[[nodiscard]] int monthsFromPreset(int monthsIndex);
[[nodiscard]] Payments::InvoicePremiumGiftCode invoice(
int users,
int monthsIndex);
int months);
[[nodiscard]] rpl::producer<rpl::no_value, QString> applyPrepaid(
const Payments::InvoicePremiumGiftCode &invoice,
uint64 prepaidId);
[[nodiscard]] int giveawayBoostsPerPremium() const;
[[nodiscard]] int giveawayCountriesMax() const;

View file

@ -518,8 +518,9 @@ rpl::producer<rpl::no_value, QString> Boosts::request() {
? (100. * premiumMemberCount / participantCount)
: 0;
const auto slots = data.vmy_boost_slots();
_boostStatus.overview = Data::BoostsOverview{
.isBoosted = data.is_my_boost(),
.mine = slots ? int(slots->v.size()) : 0,
.level = std::max(data.vlevel().v, 0),
.boostCount = std::max(
data.vboosts().v,
@ -533,6 +534,20 @@ rpl::producer<rpl::no_value, QString> Boosts::request() {
};
_boostStatus.link = qs(data.vboost_url());
if (data.vprepaid_giveaways()) {
_boostStatus.prepaidGiveaway = ranges::views::all(
data.vprepaid_giveaways()->v
) | ranges::views::transform([](const MTPPrepaidGiveaway &r) {
return Data::BoostPrepaidGiveaway{
.months = r.data().vmonths().v,
.id = r.data().vid().v,
.quantity = r.data().vquantity().v,
.date = QDateTime::fromSecsSinceEpoch(
r.data().vdate().v),
};
}) | ranges::to_vector;
}
using namespace Data;
requestBoosts({ .gifts = false }, [=](BoostsListSlice &&slice) {
_boostStatus.firstSliceBoosts = std::move(slice);
@ -540,7 +555,6 @@ rpl::producer<rpl::no_value, QString> Boosts::request() {
_boostStatus.firstSliceGifts = std::move(s);
consumer.put_done();
});
consumer.put_done();
});
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
@ -574,6 +588,7 @@ void Boosts::requestBoosts(
auto list = std::vector<Data::Boost>();
list.reserve(data.vboosts().v.size());
constexpr auto kMonthsDivider = int(30 * 86400);
for (const auto &boost : data.vboosts().v) {
const auto &data = boost.data();
const auto path = data.vused_gift_slug()
@ -596,7 +611,8 @@ void Boosts::requestBoosts(
? FullMsgId{ _peer->id, data.vgiveaway_msg_id()->v }
: FullMsgId(),
QDateTime::fromSecsSinceEpoch(data.vdate().v),
data.vexpires().v,
QDateTime::fromSecsSinceEpoch(data.vexpires().v),
(data.vexpires().v - data.vdate().v) / kMonthsDivider,
std::move(giftCodeLink),
data.vmultiplier().value_or_empty(),
});

View file

@ -362,7 +362,7 @@ void EditFilterChatsListController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListSetRowChecked(row, !row->checked());
updateTitle();
} else {
delegate()->peerListShowBox(_limitBox(count));
delegate()->peerListUiShow()->showBox(_limitBox(count));
}
}

View file

@ -575,19 +575,19 @@ void LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {
CopyInviteLink(delegate()->peerListUiShow(), link);
});
const auto shareLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
ShareInviteLinkBox(&_window->session(), link));
});
const auto getLinkQr = crl::guard(weak, [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_filters_link_qr_about()));
});
const auto editLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
Box(ChatFilterLinkBox, &_window->session(), _data));
});
const auto deleteLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(DeleteLinkBox(_window, _data));
delegate()->peerListUiShow()->showBox(DeleteLinkBox(_window, _data));
});
const auto createMenu = [=] {
@ -846,7 +846,7 @@ void LinksController::rebuild(const std::vector<InviteLinkData> &rows) {
void LinksController::rowClicked(not_null<PeerListRow*> row) {
const auto link = static_cast<LinkRow*>(row.get())->data();
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
ShowLinkBox(_window, _currentFilter(), link));
}
@ -881,19 +881,19 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
CopyInviteLink(delegate()->peerListUiShow(), link);
};
const auto shareLink = [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
ShareInviteLinkBox(&_window->session(), link));
};
const auto getLinkQr = [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_filters_link_qr_about()));
};
const auto editLink = [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
Box(ChatFilterLinkBox, &_window->session(), data));
};
const auto deleteLink = [=] {
delegate()->peerListShowBox(DeleteLinkBox(_window, data));
delegate()->peerListUiShow()->showBox(DeleteLinkBox(_window, data));
};
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,

View file

@ -7,9 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/gift_premium_box.h"
#include "apiwrap.h"
#include "api/api_premium.h"
#include "api/api_premium_option.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "base/weak_ptr.h"
#include "boxes/peers/prepare_short_info_box.h"
@ -31,7 +31,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/premium_top_bar.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/gradient_round_button.h"
@ -93,7 +95,7 @@ void GiftBox(
+ st::defaultUserpicButton.size.height()));
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
const auto stars = box->lifetime().make_state<ColoredMiniStars>(top);
const auto stars = box->lifetime().make_state<ColoredMiniStars>(top, true);
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
top,
@ -358,6 +360,73 @@ void AddTableRow(
st::giveawayGiftCodePeerMargin);
}
void AddTable(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionNavigation*> controller,
const Api::GiftCode &current,
bool skipReason) {
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
AddTableRow(
table,
tr::lng_gift_link_label_from(),
controller,
current.from);
if (current.to) {
AddTableRow(
table,
tr::lng_gift_link_label_to(),
controller,
current.to);
} else {
AddTableRow(
table,
tr::lng_gift_link_label_to(),
tr::lng_gift_link_label_to_unclaimed(Ui::Text::WithEntities));
}
AddTableRow(
table,
tr::lng_gift_link_label_gift(),
tr::lng_gift_link_gift_premium(
lt_duration,
GiftDurationValue(current.months) | Ui::Text::ToWithEntities(),
Ui::Text::WithEntities));
if (!skipReason) {
const auto reason = AddTableRow(
table,
tr::lng_gift_link_label_reason(),
(current.giveawayId
? ((current.to
? tr::lng_gift_link_reason_giveaway
: tr::lng_gift_link_reason_unclaimed)(
) | Ui::Text::ToLink())
: current.giveaway
? ((current.to
? tr::lng_gift_link_reason_giveaway
: tr::lng_gift_link_reason_unclaimed)(
Ui::Text::WithEntities
) | rpl::type_erased())
: tr::lng_gift_link_reason_chosen(Ui::Text::WithEntities)));
reason->setClickHandlerFilter([=](const auto &...) {
controller->showPeerHistory(
current.from,
Window::SectionShow::Way::Forward,
current.giveawayId);
return false;
});
}
if (current.date) {
AddTableRow(
table,
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(
langDateTime(base::unixtime::parse(current.date)))));
}
}
} // namespace
GiftPremiumValidator::GiftPremiumValidator(
@ -462,65 +531,7 @@ void GiftCodeBox(
MakeLinkCopyIcon(box)),
st::giveawayGiftCodeLinkMargin);
auto table = box->addRow(
object_ptr<Ui::TableLayout>(
box,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
const auto current = state->data.current();
AddTableRow(
table,
tr::lng_gift_link_label_from(),
controller,
current.from);
if (current.to) {
AddTableRow(
table,
tr::lng_gift_link_label_to(),
controller,
current.to);
} else {
AddTableRow(
table,
tr::lng_gift_link_label_to(),
tr::lng_gift_link_label_to_unclaimed(Ui::Text::WithEntities));
}
AddTableRow(
table,
tr::lng_gift_link_label_gift(),
tr::lng_gift_link_gift_premium(
lt_duration,
GiftDurationValue(current.months) | Ui::Text::ToWithEntities(),
Ui::Text::WithEntities));
const auto reason = AddTableRow(
table,
tr::lng_gift_link_label_reason(),
(current.giveawayId
? ((current.to
? tr::lng_gift_link_reason_giveaway
: tr::lng_gift_link_reason_unclaimed)(
) | Ui::Text::ToLink())
: current.giveaway
? ((current.to
? tr::lng_gift_link_reason_giveaway
: tr::lng_gift_link_reason_unclaimed)(
Ui::Text::WithEntities
) | rpl::type_erased())
: tr::lng_gift_link_reason_chosen(Ui::Text::WithEntities)));
reason->setClickHandlerFilter([=](const auto &...) {
controller->showPeerHistory(
current.from,
Window::SectionShow::Way::Forward,
current.giveawayId);
return false;
});
if (current.date) {
AddTableRow(
table,
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(
langDateTime(base::unixtime::parse(current.date)))));
}
AddTable(box->verticalLayout(), controller, state->data.current(), false);
auto shareLink = tr::lng_gift_link_also_send_link(
) | rpl::map([](const QString &text) {
@ -603,6 +614,113 @@ void GiftCodeBox(
}, button->lifetime());
}
void GiftCodePendingBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> controller,
const Api::GiftCode &data) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
{
const auto peerTo = controller->session().data().peer(data.to);
const auto clickContext = [=, weak = base::make_weak(controller)] {
if (const auto strong = weak.get()) {
strong->uiShow()->showBox(
PrepareShortInfoBox(peerTo, strong));
}
return QVariant();
};
const auto &st = st::giveawayGiftCodeCover;
const auto resultToName = st.about.style.font->elided(
peerTo->shortName(),
st.about.minWidth / 2,
Qt::ElideMiddle);
const auto bar = box->setPinnedToTopContent(
object_ptr<Ui::Premium::TopBar>(
box,
st,
clickContext,
tr::lng_gift_link_title(),
tr::lng_gift_link_pending_about(
lt_user,
rpl::single(Ui::Text::Link(resultToName)),
Ui::Text::RichLangValue),
true));
const auto max = st::giveawayGiftCodeTopHeight;
bar->setMaximumHeight(max);
bar->setMinimumHeight(st::infoLayerTopBarHeight);
bar->resize(bar->width(), bar->maximumHeight());
}
{
const auto linkLabel = box->addRow(
Ui::MakeLinkLabel(box, nullptr, nullptr, nullptr, nullptr),
st::giveawayGiftCodeLinkMargin);
const auto spoiler = Ui::CreateChild<Ui::AbstractButton>(linkLabel);
spoiler->lifetime().make_state<Ui::Animations::Basic>([=] {
spoiler->update();
})->start();
linkLabel->sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
spoiler->setGeometry(Rect(s));
}, spoiler->lifetime());
const auto spoilerCached = Ui::SpoilerMessCached(
Ui::DefaultTextSpoilerMask(),
st::giveawayGiftCodeLink.textFg->c);
const auto textHeight = st::giveawayGiftCodeLink.style.font->height;
spoiler->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(spoiler);
const auto rect = spoiler->rect();
const auto r = rect
- QMargins(
st::boxRowPadding.left(),
(rect.height() - textHeight) / 2,
st::boxRowPadding.right(),
(rect.height() - textHeight) / 2);
Ui::FillSpoilerRect(p, r, spoilerCached.frame());
}, spoiler->lifetime());
spoiler->setClickedCallback([show = box->uiShow()] {
show->showToast(tr::lng_gift_link_pending_toast(tr::now));
});
spoiler->show();
}
AddTable(box->verticalLayout(), controller, data, true);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_gift_link_pending_footer(),
st::giveawayGiftCodeFooter),
st::giveawayGiftCodeFooterMargin);
const auto close = Ui::CreateChild<Ui::IconButton>(
box.get(),
st::boxTitleClose);
const auto closeCallback = [=] { box->closeBox(); };
close->setClickedCallback(closeCallback);
box->widthValue(
) | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0);
}, box->lifetime());
const auto button = box->addButton(tr::lng_close(), closeCallback);
const auto buttonPadding = st::giveawayGiftCodeBox.buttonPadding;
const auto buttonWidth = st::boxWideWidth
- buttonPadding.left()
- buttonPadding.right();
button->widthValue() | rpl::filter([=] {
return (button->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=] {
button->resizeToWidth(buttonWidth);
}, button->lifetime());
}
void ResolveGiftCode(
not_null<Window::SessionNavigation*> controller,
const QString &slug) {

View file

@ -50,6 +50,10 @@ void GiftCodeBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> controller,
const QString &slug);
void GiftCodePendingBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> controller,
const Api::GiftCode &data);
void ResolveGiftCode(
not_null<Window::SessionNavigation*> controller,
const QString &slug);

View file

@ -567,12 +567,10 @@ void PasscodeBox::validateEmail(
} else if (error.type() == u"EMAIL_HASH_EXPIRED"_q) {
const auto weak = Ui::MakeWeak(this);
_clearUnconfirmedPassword.fire({});
if (weak) {
auto box = Ui::MakeInformBox({
Lang::Hard::EmailConfirmationExpired()
});
weak->getDelegate()->show(
std::move(box),
if (const auto strong = weak.data()) {
strong->getDelegate()->show(
Ui::MakeInformBox(
Lang::Hard::EmailConfirmationExpired()),
Ui::LayerOption::CloseOther);
}
} else {

View file

@ -85,16 +85,6 @@ PeerListContentDelegateShow::PeerListContentDelegateShow(
: _show(show) {
}
void PeerListContentDelegateShow::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
_show->showBox(std::move(content), options);
}
void PeerListContentDelegateShow::peerListHideLayer() {
_show->hideLayer();
}
auto PeerListContentDelegateShow::peerListUiShow()
-> std::shared_ptr<Main::SessionShow>{
return _show;
@ -324,16 +314,6 @@ void PeerListBox::peerListSetSearchMode(PeerListSearchMode mode) {
}
}
void PeerListBox::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
_show->showBox(std::move(content), options);
}
void PeerListBox::peerListHideLayer() {
_show->hideLayer();
}
std::shared_ptr<Main::SessionShow> PeerListBox::peerListUiShow() {
return _show;
}
@ -1702,9 +1682,19 @@ crl::time PeerListContent::paintRow(
return refreshStatusIn;
}
const auto opacity = row->opacity();
const auto &bg = selected
? _st.item.button.textBgOver
: _st.item.button.textBg;
if (opacity < 1.) {
p.setOpacity(opacity);
}
const auto guard = gsl::finally([&] {
if (opacity < 1.) {
p.setOpacity(1.);
}
});
p.fillRect(0, 0, outerWidth, _rowHeight, bg);
row->paintRipple(p, 0, 0, outerWidth);
row->paintUserpic(

View file

@ -141,6 +141,9 @@ public:
}
virtual void rightActionStopLastRipple() {
}
[[nodiscard]] virtual float64 opacity() {
return 1.;
}
// By default elements code falls back to a simple right action code.
virtual int elementsCount() const;
@ -332,10 +335,6 @@ public:
virtual std::optional<QPoint> peerListLastRowMousePosition() = 0;
virtual void peerListSortRows(Fn<bool(const PeerListRow &a, const PeerListRow &b)> compare) = 0;
virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
virtual void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) = 0;
virtual void peerListHideLayer() = 0;
virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;
template <typename PeerDataRange>
@ -1007,14 +1006,6 @@ public:
object_ptr<Ui::FlatLabel> description) override {
description.destroy();
}
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override {
Unexpected("...DelegateSimple::peerListShowBox");
}
void peerListHideLayer() override {
Unexpected("...DelegateSimple::peerListHideLayer");
}
std::shared_ptr<Main::SessionShow> peerListUiShow() override {
Unexpected("...DelegateSimple::peerListUiShow");
}
@ -1025,10 +1016,6 @@ class PeerListContentDelegateShow : public PeerListContentDelegateSimple {
public:
explicit PeerListContentDelegateShow(
std::shared_ptr<Main::SessionShow> show);
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
private:
@ -1064,10 +1051,6 @@ public:
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
void setAddedTopScrollSkip(int skip);

View file

@ -685,7 +685,7 @@ void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
}, box->lifetime());
});
*weak = owned.data();
delegate()->peerListShowBox(std::move(owned));
delegate()->peerListUiShow()->showBox(std::move(owned));
return;
}
const auto history = peer->owner().history(peer);

View file

@ -372,16 +372,6 @@ void PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() {
_box->_select->entity()->finishItemsBunch();
}
void PeerListsBox::Delegate::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
_show->showBox(std::move(content), options);
}
void PeerListsBox::Delegate::peerListHideLayer() {
_show->hideLayer();
}
auto PeerListsBox::Delegate::peerListUiShow()
-> std::shared_ptr<Main::SessionShow> {
return _show;

View file

@ -54,10 +54,6 @@ private:
_box->addSelectItem(row, anim::type::instant);
}
void peerListFinishSelectedRowsBunch() override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
private:

View file

@ -366,7 +366,7 @@ bool AddParticipantsBoxController::needsInviteLinkButton() {
QPointer<Ui::BoxContent> AddParticipantsBoxController::showBox(
object_ptr<Ui::BoxContent> box) const {
const auto weak = Ui::MakeWeak(box.data());
delegate()->peerListShowBox(std::move(box));
delegate()->peerListUiShow()->showBox(std::move(box));
return weak;
}
@ -668,7 +668,7 @@ void AddSpecialBoxController::migrate(
QPointer<Ui::BoxContent> AddSpecialBoxController::showBox(
object_ptr<Ui::BoxContent> box) const {
const auto weak = Ui::MakeWeak(box.data());
delegate()->peerListShowBox(std::move(box));
delegate()->peerListUiShow()->showBox(std::move(box));
return weak;
}

View file

@ -433,7 +433,7 @@ void ChoosePeerBoxController::rowClicked(not_null<PeerListRow*> row) {
if (const auto user = peer->asUser()) {
done();
} else {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
MakeConfirmBox(_bot, peer, _query, done));
}
}

View file

@ -168,7 +168,7 @@ void Controller::choose(not_null<ChannelData*> chat) {
const auto onstack = _callback;
onstack(chat);
};
delegate()->peerListShowBox(Ui::MakeConfirmBox({
delegate()->peerListUiShow()->showBox(Ui::MakeConfirmBox({
.text = text,
.confirmed = sure,
.confirmText = tr::lng_manage_discussion_group_link(tr::now),
@ -199,7 +199,7 @@ void Controller::choose(not_null<ChatData*> chat) {
};
chat->session().api().migrateChat(chat, crl::guard(this, done));
};
delegate()->peerListShowBox(Ui::MakeConfirmBox({
delegate()->peerListUiShow()->showBox(Ui::MakeConfirmBox({
.text = text,
.confirmed = sure,
.confirmText = tr::lng_manage_discussion_group_link(tr::now),

View file

@ -1281,7 +1281,7 @@ void ParticipantsBoxController::rebuild() {
QPointer<Ui::BoxContent> ParticipantsBoxController::showBox(
object_ptr<Ui::BoxContent> box) const {
const auto weak = Ui::MakeWeak(box.data());
delegate()->peerListShowBox(std::move(box));
delegate()->peerListUiShow()->showBox(std::move(box));
return weak;
}

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/peers/replace_boost_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@ -514,21 +515,17 @@ void Apply(
close();
return;
}
const auto next = data.vnext_level_boosts().value_or_empty();
const auto openStatistics = [=] {
if (const auto controller = show->resolveWindow(
ChatHelpers::WindowUsage::PremiumPromo)) {
controller->showSection(Info::Boosts::Make(peer));
}
};
auto counters = ParseBoostCounters(result);
counters.mine = 0; // Don't show current level as just-reached.
show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
.link = qs(data.vboost_url()),
.boost = {
.level = data.vlevel().v,
.boosts = data.vboosts().v,
.thisLevelBoosts = data.vcurrent_level_boosts().v,
.nextLevelBoosts = next,
},
.boost = counters,
.requiredLevel = required,
}, openStatistics, nullptr));
cancel();

View file

@ -347,21 +347,24 @@ void Controller::addHeaderBlock(not_null<Ui::VerticalLayout*> container) {
const auto copyLink = crl::guard(weak, [=] {
CopyInviteLink(delegate()->peerListUiShow(), link);
});
const auto shareLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(ShareInviteLinkBox(_peer, link));
const auto shareLink = crl::guard(weak, [=, peer = _peer] {
delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(peer, link));
});
const auto getLinkQr = crl::guard(weak, [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_group_invite_qr_about()));
});
const auto revokeLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(RevokeLinkBox(_peer, admin, link));
delegate()->peerListUiShow()->showBox(
RevokeLinkBox(_peer, admin, link));
});
const auto editLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(EditLinkBox(_peer, _data.current()));
delegate()->peerListUiShow()->showBox(
EditLinkBox(_peer, _data.current()));
});
const auto deleteLink = crl::guard(weak, [=] {
delegate()->peerListShowBox(DeleteLinkBox(_peer, admin, link));
delegate()->peerListUiShow()->showBox(
DeleteLinkBox(_peer, admin, link));
});
const auto createMenu = [=] {
@ -1332,7 +1335,7 @@ object_ptr<Ui::BoxContent> ShowInviteLinkBox(
auto data = rpl::single(link) | rpl::then(std::move(updates));
auto initBox = [=, data = rpl::duplicate(data)](
not_null<Ui::BoxContent*> box) {
not_null<Ui::BoxContent*> box) {
rpl::duplicate(
data
) | rpl::start_with_next([=](const LinkData &link) {

View file

@ -537,7 +537,7 @@ void LinksController::appendSlice(const InviteLinksSlice &slice) {
}
void LinksController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
ShowInviteLinkBox(_peer, static_cast<Row*>(row.get())->data()));
}
@ -573,25 +573,27 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
st::popupMenuWithIcons);
if (data.revoked) {
result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
delegate()->peerListShowBox(DeleteLinkBox(_peer, _admin, link));
delegate()->peerListUiShow()->showBox(
DeleteLinkBox(_peer, _admin, link));
}, &st::menuIconDelete);
} else {
result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
CopyInviteLink(delegate()->peerListUiShow(), link);
}, &st::menuIconCopy);
result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
ShareInviteLinkBox(_peer, link));
}, &st::menuIconShare);
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_group_invite_qr_about()));
}, &st::menuIconQrCode);
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
delegate()->peerListShowBox(EditLinkBox(_peer, data));
delegate()->peerListUiShow()->showBox(EditLinkBox(_peer, data));
}, &st::menuIconEdit);
result->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] {
delegate()->peerListShowBox(RevokeLinkBox(_peer, _admin, link));
delegate()->peerListUiShow()->showBox(
RevokeLinkBox(_peer, _admin, link));
}, &st::menuIconRemove);
}
return result;
@ -799,7 +801,7 @@ void AdminsController::loadMoreRows() {
}
void AdminsController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowBox(
delegate()->peerListUiShow()->showBox(
Box(ManageInviteLinksBox, _peer, row->peer()->asUser(), 0, 0));
}

View file

@ -0,0 +1,613 @@
/*
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 "boxes/peers/replace_boost_box.h"
#include "base/event_filter.h"
#include "base/unixtime.h"
#include "boxes/peer_list_box.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "main/session/session_show.h"
#include "ui/boxes/boost_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h"
#include "styles/style_boxes.h"
#include "styles/style_premium.h"
namespace {
constexpr auto kWaitingOpacity = 0.5;
class Row final : public PeerListRow {
public:
Row(
not_null<Main::Session*> session,
TakenBoostSlot slot,
TimeId unixtimeNow,
crl::time preciseNow);
void updateStatus(TimeId unixtimeNow, crl::time preciseNow);
[[nodiscard]] TakenBoostSlot data() const {
return _data;
}
[[nodiscard]] bool waiting() const {
return _waiting;
}
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
float64 opacity() override;
private:
[[nodiscard]]PaintRoundImageCallback peerPaintUserpicCallback();
TakenBoostSlot _data;
PeerData *_peer = nullptr;
std::shared_ptr<Ui::EmptyUserpic> _empty;
Ui::PeerUserpicView _userpic;
crl::time _startPreciseTime = 0;
TimeId _startUnixtime = 0;
bool _waiting = false;
};
class Controller final : public PeerListController {
public:
Controller(not_null<ChannelData*> to, std::vector<TakenBoostSlot> from);
[[nodiscard]] rpl::producer<std::vector<int>> selectedValue() const {
return _selected.value();
}
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
bool trackSelectedList() override {
return false;
}
private:
void updateWaitingState();
not_null<ChannelData*> _to;
std::vector<TakenBoostSlot> _from;
rpl::variable<std::vector<int>> _selected;
rpl::variable<std::vector<not_null<PeerData*>>> _selectedPeers;
base::Timer _waitingTimer;
bool _hasWaitingRows = false;
};
Row::Row(
not_null<Main::Session*> session,
TakenBoostSlot slot,
TimeId unixtimeNow,
crl::time preciseNow)
: PeerListRow(PeerListRowId(slot.id))
, _data(slot)
, _peer(session->data().peerLoaded(_data.peerId))
, _startPreciseTime(preciseNow)
, _startUnixtime(unixtimeNow) {
updateStatus(unixtimeNow, preciseNow);
}
void Row::updateStatus(TimeId unixtimeNow, crl::time preciseNow) {
_waiting = (_data.cooldown > unixtimeNow);
if (_waiting) {
const auto initial = crl::time(_data.cooldown - _startUnixtime);
const auto elapsed = (preciseNow + 500 - _startPreciseTime) / 1000;
const auto seconds = initial
- std::clamp(elapsed, crl::time(), initial);
const auto hours = seconds / 3600;
const auto minutes = seconds / 60;
const auto duration = (hours > 0)
? u"%1:%2:%3"_q.arg(
hours
).arg(minutes % 60, 2, 10, QChar('0')
).arg(seconds % 60, 2, 10, QChar('0'))
: u"%1:%2"_q.arg(
minutes
).arg(seconds % 60, 2, 10, QChar('0'));
setCustomStatus(
tr::lng_boost_available_in(tr::now, lt_duration, duration));
} else {
const auto date = base::unixtime::parse(_data.expires);
setCustomStatus(tr::lng_boosts_list_status(
tr::now,
lt_date,
langDayOfMonth(date.date())));
}
}
QString Row::generateName() {
return _peer ? _peer->name() : u" "_q;
}
QString Row::generateShortName() {
return _peer ? _peer->shortName() : generateName();
}
PaintRoundImageCallback Row::generatePaintUserpicCallback(
bool forceRound) {
if (_peer) {
return (forceRound && _peer->isForum())
? ForceRoundUserpicCallback(_peer)
: peerPaintUserpicCallback();
} else if (!_empty) {
const auto colorIndex = _data.id % Ui::kColorIndexCount;
_empty = std::make_shared<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(colorIndex),
u" "_q);
}
const auto empty = _empty;
return [=](Painter &p, int x, int y, int outerWidth, int size) {
empty->paintCircle(p, x, y, outerWidth, size);
};
}
float64 Row::opacity() {
return _waiting ? kWaitingOpacity : 1.;
}
PaintRoundImageCallback Row::peerPaintUserpicCallback() {
const auto peer = _peer;
if (!_userpic.cloud && peer->hasUserpic()) {
_userpic = peer->createUserpicView();
}
auto userpic = _userpic;
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
};
}
Controller::Controller(
not_null<ChannelData*> to,
std::vector<TakenBoostSlot> from)
: _to(to)
, _from(std::move(from))
, _waitingTimer([=] { updateWaitingState(); }) {
}
Main::Session &Controller::session() const {
return _to->session();
}
void Controller::prepare() {
delegate()->peerListSetTitle(tr::lng_boost_reassign_title());
const auto session = &_to->session();
auto above = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
above->add(
CreateBoostReplaceUserpics(
above.data(),
_selectedPeers.value(),
_to),
st::boxRowPadding + st::boostReplaceUserpicsPadding);
above->add(
object_ptr<Ui::FlatLabel>(
above.data(),
tr::lng_boost_reassign_text(
lt_channel,
rpl::single(Ui::Text::Bold(_to->name())),
lt_gift,
tr::lng_boost_reassign_gift(
lt_count,
rpl::single(1. * BoostsForGift(session)),
Ui::Text::RichLangValue),
Ui::Text::RichLangValue),
st::boostReassignText),
st::boxRowPadding);
delegate()->peerListSetAboveWidget(std::move(above));
const auto now = base::unixtime::now();
const auto precise = crl::now();
ranges::stable_sort(_from, ranges::less(), [&](TakenBoostSlot slot) {
return (slot.cooldown > now) ? slot.cooldown : -slot.cooldown;
});
for (const auto &slot : _from) {
auto row = std::make_unique<Row>(session, slot, now, precise);
if (row->waiting()) {
_hasWaitingRows = true;
}
delegate()->peerListAppendRow(std::move(row));
}
if (_hasWaitingRows) {
_waitingTimer.callEach(1000);
}
delegate()->peerListRefreshRows();
}
void Controller::updateWaitingState() {
_hasWaitingRows = false;
const auto now = base::unixtime::now();
const auto precise = crl::now();
const auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count; ++i) {
const auto bare = delegate()->peerListRowAt(i);
const auto row = static_cast<Row*>(bare.get());
if (row->waiting()) {
row->updateStatus(now, precise);
delegate()->peerListUpdateRow(row);
if (row->waiting()) {
_hasWaitingRows = true;
}
}
}
if (!_hasWaitingRows) {
_waitingTimer.cancel();
}
}
void Controller::rowClicked(not_null<PeerListRow*> row) {
const auto slot = static_cast<Row*>(row.get())->data();
if (slot.cooldown > base::unixtime::now()) {
delegate()->peerListUiShow()->showToast({
.text = tr::lng_boost_available_in_toast(
tr::now,
lt_count,
BoostsForGift(&session()),
Ui::Text::RichLangValue),
.adaptive = true,
});
return;
}
auto now = _selected.current();
const auto id = slot.id;
const auto checked = !row->checked();
delegate()->peerListSetRowChecked(row, checked);
const auto peer = slot.peerId
? _to->owner().peerLoaded(slot.peerId)
: nullptr;
auto peerRemoved = false;
if (checked) {
now.push_back(id);
} else {
now.erase(ranges::remove(now, id), end(now));
peerRemoved = true;
for (const auto left : now) {
const auto i = ranges::find(_from, left, &TakenBoostSlot::id);
Assert(i != end(_from));
if (i->peerId == slot.peerId) {
peerRemoved = false;
break;
}
}
}
_selected = std::move(now);
if (peer) {
auto selectedPeers = _selectedPeers.current();
const auto i = ranges::find(selectedPeers, not_null(peer));
if (peerRemoved) {
Assert(i != end(selectedPeers));
selectedPeers.erase(i);
_selectedPeers = std::move(selectedPeers);
} else if (i == end(selectedPeers) && checked) {
selectedPeers.insert(begin(selectedPeers), peer);
_selectedPeers = std::move(selectedPeers);
}
}
}
object_ptr<Ui::BoxContent> ReassignBoostFloodBox(int seconds) {
const auto days = seconds / 86400;
const auto hours = seconds / 3600;
const auto minutes = seconds / 60;
return Ui::MakeInformBox({
.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(),
});
}
object_ptr<Ui::BoxContent> ReassignBoostSingleBox(
not_null<ChannelData*> to,
TakenBoostSlot from,
Fn<void(std::vector<int> slots, int sources)> reassign,
Fn<void()> cancel) {
const auto reassigned = std::make_shared<bool>();
const auto slot = from.id;
const auto peer = to->owner().peer(from.peerId);
const auto confirmed = [=](Fn<void()> close) {
*reassigned = true;
reassign({ slot }, 1);
close();
};
auto result = Box([=](not_null<Ui::GenericBox*> box) {
Ui::ConfirmBox(box, {
.text = tr::lng_boost_now_instead(
lt_channel,
rpl::single(Ui::Text::Bold(peer->name())),
lt_other,
rpl::single(Ui::Text::Bold(to->name())),
Ui::Text::WithEntities),
.confirmed = confirmed,
.confirmText = tr::lng_boost_now_replace(),
.labelPadding = st::boxRowPadding,
});
box->verticalLayout()->insert(
0,
CreateBoostReplaceUserpics(
box,
rpl::single(std::vector{ peer }),
to),
st::boxRowPadding + st::boostReplaceUserpicsPadding);
});
result->boxClosing() | rpl::filter([=] {
return !*reassigned;
}) | rpl::start_with_next(cancel, result->lifetime());
return result;
}
} // namespace
ForChannelBoostSlots ParseForChannelBoostSlots(
not_null<ChannelData*> channel,
const QVector<MTPMyBoost> &boosts) {
auto result = ForChannelBoostSlots();
const auto now = base::unixtime::now();
for (const auto &my : boosts) {
const auto &data = my.data();
const auto id = data.vslot().v;
const auto cooldown = data.vcooldown_until_date().value_or(0);
const auto peerId = data.vpeer()
? peerFromMTP(*data.vpeer())
: PeerId();
if (!peerId && cooldown <= now) {
result.free.push_back(id);
} else if (peerId == channel->id) {
result.already.push_back(id);
} else {
result.other.push_back({
.id = id,
.expires = data.vexpires().v,
.peerId = peerId,
.cooldown = cooldown,
});
}
}
return result;
}
Ui::BoostCounters ParseBoostCounters(
const MTPpremium_BoostsStatus &status) {
const auto &data = status.data();
const auto slots = data.vmy_boost_slots();
return {
.level = data.vlevel().v,
.boosts = data.vboosts().v,
.thisLevelBoosts = data.vcurrent_level_boosts().v,
.nextLevelBoosts = data.vnext_level_boosts().value_or_empty(),
.mine = slots ? int(slots->v.size()) : 0,
};
}
int BoostsForGift(not_null<Main::Session*> session) {
const auto key = u"boosts_per_sent_gift"_q;
return session->account().appConfig().get<int>(key, 0);
}
[[nodiscard]] int SourcesCount(
const std::vector<TakenBoostSlot> &from,
const std::vector<int> &slots) {
auto checked = base::flat_set<PeerId>();
checked.reserve(slots.size());
for (const auto slot : slots) {
const auto i = ranges::find(from, slot, &TakenBoostSlot::id);
Assert(i != end(from));
checked.emplace(i->peerId);
}
return checked.size();
}
object_ptr<Ui::BoxContent> ReassignBoostsBox(
not_null<ChannelData*> to,
std::vector<TakenBoostSlot> from,
Fn<void(std::vector<int> slots, int sources)> reassign,
Fn<void()> cancel) {
Expects(!from.empty());
const auto now = base::unixtime::now();
if (from.size() == 1 && from.front().cooldown > now) {
cancel();
return ReassignBoostFloodBox(from.front().cooldown - now);
} else if (from.size() == 1 && from.front().peerId) {
return ReassignBoostSingleBox(to, from.front(), reassign, cancel);
}
const auto reassigned = std::make_shared<bool>();
auto controller = std::make_unique<Controller>(to, from);
const auto raw = controller.get();
auto initBox = [=](not_null<Ui::BoxContent*> box) {
raw->selectedValue(
) | rpl::start_with_next([=](std::vector<int> slots) {
box->clearButtons();
if (!slots.empty()) {
const auto sources = SourcesCount(from, slots);
box->addButton(tr::lng_boost_reassign_button(), [=] {
*reassigned = true;
reassign(slots, sources);
});
}
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}, box->lifetime());
box->boxClosing() | rpl::filter([=] {
return !*reassigned;
}) | rpl::start_with_next(cancel, box->lifetime());
};
return Box<PeerListBox>(std::move(controller), std::move(initBox));
}
object_ptr<Ui::RpWidget> CreateBoostReplaceUserpics(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> from,
not_null<PeerData*> to) {
struct State {
std::vector<not_null<PeerData*>> from;
std::vector<std::unique_ptr<Ui::UserpicButton>> buttons;
QImage layer;
rpl::variable<int> count = 0;
bool painting = false;
};
const auto full = st::boostReplaceUserpic.size.height()
+ st::boostReplaceIconAdd.y()
+ st::lineWidth;
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
const auto raw = result.data();
const auto &st = st::boostReplaceUserpic;
const auto right = CreateChild<Ui::UserpicButton>(raw, to, st);
const auto overlay = CreateChild<Ui::RpWidget>(raw);
const auto state = raw->lifetime().make_state<State>();
std::move(
from
) | rpl::start_with_next([=](
const std::vector<not_null<PeerData*>> &list) {
const auto &st = st::boostReplaceUserpic;
auto was = base::take(state->from);
auto buttons = base::take(state->buttons);
state->from.reserve(list.size());
state->buttons.reserve(list.size());
for (const auto &peer : list) {
state->from.push_back(peer);
const auto i = ranges::find(was, peer);
if (i != end(was)) {
const auto index = int(i - begin(was));
Assert(buttons[index] != nullptr);
state->buttons.push_back(std::move(buttons[index]));
} else {
state->buttons.push_back(
std::make_unique<Ui::UserpicButton>(raw, peer, st));
const auto raw = state->buttons.back().get();
base::install_event_filter(raw, [=](not_null<QEvent*> e) {
return (e->type() == QEvent::Paint && !state->painting)
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
});
}
}
state->count.force_assign(int(list.size()));
overlay->update();
}, raw->lifetime());
rpl::combine(
raw->widthValue(),
state->count.value()
) | rpl::start_with_next([=](int width, int count) {
const auto skip = st::boostReplaceUserpicsSkip;
const auto left = width - 2 * right->width() - skip;
const auto shift = std::min(
st::boostReplaceUserpicsShift,
(count > 1 ? (left / (count - 1)) : width));
const auto total = right->width()
+ (count ? (skip + right->width() + (count - 1) * shift) : 0);
auto x = (width - total) / 2;
for (const auto &single : state->buttons) {
single->moveToLeft(x, 0);
x += shift;
}
if (count) {
x += right->width() - shift + skip;
}
right->moveToLeft(x, 0);
overlay->setGeometry(QRect(0, 0, width, raw->height()));
}, raw->lifetime());
overlay->paintRequest(
) | rpl::filter([=] {
return !state->buttons.empty();
}) | rpl::start_with_next([=] {
const auto outerw = overlay->width();
const auto ratio = style::DevicePixelRatio();
if (state->layer.size() != QSize(outerw, full) * ratio) {
state->layer = QImage(
QSize(outerw, full) * ratio,
QImage::Format_ARGB32_Premultiplied);
state->layer.setDevicePixelRatio(ratio);
}
state->layer.fill(Qt::transparent);
auto q = QPainter(&state->layer);
auto hq = PainterHighQualityEnabler(q);
const auto stroke = st::boostReplaceIconOutline;
const auto half = stroke / 2.;
auto pen = st::windowBg->p;
pen.setWidthF(stroke * 2.);
state->painting = true;
for (const auto &button : state->buttons) {
q.setPen(pen);
q.setBrush(Qt::NoBrush);
q.drawEllipse(button->geometry());
const auto position = button->pos();
button->render(&q, position, QRegion(), QWidget::DrawChildren);
}
state->painting = false;
const auto last = state->buttons.back().get();
const auto add = st::boostReplaceIconAdd;
const auto skip = st::boostReplaceIconSkip;
const auto w = st::boostReplaceIcon.width() + 2 * skip;
const auto h = st::boostReplaceIcon.height() + 2 * skip;
const auto x = last->x() + last->width() - w + add.x();
const auto y = last->y() + last->height() - h + add.y();
auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y));
brush.setStops(Ui::Premium::ButtonGradientStops());
q.setBrush(brush);
pen.setWidthF(stroke);
q.setPen(pen);
q.drawEllipse(x - half, y - half, w + stroke, h + stroke);
st::boostReplaceIcon.paint(q, x + skip, y + skip, outerw);
const auto size = st::boostReplaceArrow.size();
st::boostReplaceArrow.paint(
q,
(last->x()
+ last->width()
+ (st::boostReplaceUserpicsSkip - size.width()) / 2),
(last->height() - size.height()) / 2,
outerw);
q.end();
auto p = QPainter(overlay);
p.drawImage(0, 0, state->layer);
}, overlay->lifetime());
return result;
}

View file

@ -0,0 +1,53 @@
/*
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
#include "base/object_ptr.h"
namespace Main {
class Session;
} // namespace Main
namespace Ui {
struct BoostCounters;
class BoxContent;
class RpWidget;
} // namespace Ui
struct TakenBoostSlot {
int id = 0;
TimeId expires = 0;
PeerId peerId = 0;
TimeId cooldown = 0;
};
struct ForChannelBoostSlots {
std::vector<int> free;
std::vector<int> already;
std::vector<TakenBoostSlot> other;
};
[[nodiscard]] ForChannelBoostSlots ParseForChannelBoostSlots(
not_null<ChannelData*> channel,
const QVector<MTPMyBoost> &boosts);
[[nodiscard]] Ui::BoostCounters ParseBoostCounters(
const MTPpremium_BoostsStatus &status);
[[nodiscard]] int BoostsForGift(not_null<Main::Session*> session);
object_ptr<Ui::BoxContent> ReassignBoostsBox(
not_null<ChannelData*> to,
std::vector<TakenBoostSlot> from,
Fn<void(std::vector<int> slots, int sources)> reassign,
Fn<void()> cancel);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateBoostReplaceUserpics(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> from,
not_null<PeerData*> to);

View file

@ -115,10 +115,6 @@ public:
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
void peerListSetRowChecked(
not_null<PeerListRow*> row,
@ -183,14 +179,6 @@ void InactiveDelegate::peerListSetDescription(
description.destroy();
}
void InactiveDelegate::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
}
void InactiveDelegate::peerListHideLayer() {
}
std::shared_ptr<Main::SessionShow> InactiveDelegate::peerListUiShow() {
Unexpected("...InactiveDelegate::peerListUiShow");
}

View file

@ -1975,14 +1975,6 @@ void Members::peerListSetDescription(
description.destroy();
}
void Members::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
}
void Members::peerListHideLayer() {
}
std::shared_ptr<Main::SessionShow> Members::peerListUiShow() {
Unexpected("...Members::peerListUiShow");
}

View file

@ -88,10 +88,6 @@ private:
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
void setupAddMember(not_null<GroupCall*> call);

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 4011005;
constexpr auto AppVersionStr = "4.11.5";
constexpr auto AppVersion = 4011006;
constexpr auto AppVersionStr = "4.11.6";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -10,7 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
struct BoostsOverview final {
bool isBoosted = false;
int mine = 0;
int level = 0;
int boostCount = 0;
int currentLevelBoostCount = 0;
@ -34,7 +34,8 @@ struct Boost final {
UserId userId = UserId(0);
FullMsgId giveawayMessage;
QDateTime date;
crl::time expiresAt = 0;
QDateTime expiresAt;
int expiresAfterMonths = 0;
GiftCodeLink giftCodeLink;
int multiplier = 0;
};
@ -50,10 +51,18 @@ struct BoostsListSlice final {
OffsetToken token;
};
struct BoostPrepaidGiveaway final {
int months = 0;
uint64 id = 0;
int quantity = 0;
QDateTime date;
};
struct BoostStatus final {
BoostsOverview overview;
BoostsListSlice firstSliceBoosts;
BoostsListSlice firstSliceGifts;
std::vector<BoostPrepaidGiveaway> prepaidGiveaway;
QString link;
};

View file

@ -391,3 +391,7 @@ bool WebPageData::computeDefaultSmallMedia() const {
}
return false;
}
bool WebPageData::suggestEnlargePhoto() const {
return !siteName.isEmpty() || !title.isEmpty() || !description.empty();
}

View file

@ -90,6 +90,7 @@ struct WebPageData {
[[nodiscard]] QString displayedSiteName() const;
[[nodiscard]] bool computeDefaultSmallMedia() const;
[[nodiscard]] bool suggestEnlargePhoto() const;
const WebPageId id = 0;
WebPageType type = WebPageType::None;

View file

@ -1861,9 +1861,12 @@ std::unique_ptr<QMimeData> HistoryInner::prepareDrag() {
session().data().setMimeForwardIds(std::move(forwardIds));
auto result = std::make_unique<QMimeData>();
result->setData(u"application/x-td-forward"_q, "1");
if (const auto media = pressedView->media()) {
if (const auto document = media->getDocument()) {
const auto filepath = document->filepath(true);
if (pressedHandler) {
const auto lnkDocument = reinterpret_cast<DocumentData*>(
pressedHandler->property(
kDocumentLinkMediaProperty).toULongLong());
if (lnkDocument) {
const auto filepath = lnkDocument->filepath(true);
if (!filepath.isEmpty()) {
QList<QUrl> urls;
urls.push_back(QUrl::fromLocalFile(filepath));
@ -3098,6 +3101,8 @@ void HistoryInner::recountHistoryGeometry() {
accumulate_max(oldHistoryPaddingTop, _botAbout->height);
}
updateBotInfo(false);
_history->resizeToWidth(_contentWidth);
if (_migrated) {
_migrated->resizeToWidth(_contentWidth);
@ -3121,7 +3126,6 @@ void HistoryInner::recountHistoryGeometry() {
}
}
updateBotInfo(false);
if (const auto view = _botAbout ? _botAbout->view() : nullptr) {
_botAbout->height = view->resizeGetHeight(_contentWidth);
_botAbout->top = qMin(

View file

@ -3407,6 +3407,7 @@ void HistoryItem::createComponentsHelper(
: nullptr;
if (!config.reply.externalPeerId
&& topic
&& to
&& topic->rootId() != to->topicRootId()) {
config.reply.externalPeerId = replyTo.messageId.peer;
}

View file

@ -22,6 +22,7 @@ enum class CursorState : char {
None,
Text,
Date,
Enlarge,
Forwarded,
};

View file

@ -3639,9 +3639,12 @@ std::unique_ptr<QMimeData> ListWidget::prepareDrag() {
session().data().setMimeForwardIds(std::move(forwardIds));
auto result = std::make_unique<QMimeData>();
result->setData(u"application/x-td-forward"_q, "1");
if (const auto media = pressedView->media()) {
if (const auto document = media->getDocument()) {
const auto filepath = document->filepath(true);
if (pressedHandler) {
const auto lnkDocument = reinterpret_cast<DocumentData*>(
pressedHandler->property(
kDocumentLinkMediaProperty).toULongLong());
if (lnkDocument) {
const auto filepath = lnkDocument->filepath(true);
if (!filepath.isEmpty()) {
QList<QUrl> urls;
urls.push_back(QUrl::fromLocalFile(filepath));

View file

@ -105,6 +105,7 @@ void Giveaway::fillFromData(not_null<Data::Giveaway*> giveaway) {
st::msgMinWidth),
.thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel),
.link = channel->openLink(),
.colorIndex = channel->colorIndex(),
});
}
const auto channels = int(_channels.size());
@ -342,18 +343,6 @@ void Giveaway::paintChannels(
const auto st = context.st;
const auto stm = context.messageStyle();
const auto selected = context.selected();
const auto colorIndex = parent()->colorIndex();
const auto cache = context.outbg
? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
: st->coloredReplyCache(selected, colorIndex).get();
if (_channelCorners[0].isNull() || _channelBg != cache->bg) {
_channelBg = cache->bg;
_channelCorners = Images::CornersMask(size / 2);
for (auto &image : _channelCorners) {
style::colorizeImage(image, cache->bg, &image);
}
}
p.setPen(cache->icon);
const auto padding = st::chatGiveawayChannelPadding;
for (const auto &channel : _channels) {
const auto &thumbnail = channel.thumbnail;
@ -364,7 +353,19 @@ void Giveaway::paintChannels(
});
}
Ui::DrawRoundedRect(p, geometry, _channelBg, _channelCorners);
const auto colorIndex = channel.colorIndex;
const auto cache = context.outbg
? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
: st->coloredReplyCache(selected, colorIndex).get();
if (channel.corners[0].isNull() || channel.bg != cache->bg) {
channel.bg = cache->bg;
channel.corners = Images::CornersMask(size / 2);
for (auto &image : channel.corners) {
style::colorizeImage(image, cache->bg, &image);
}
}
p.setPen(cache->icon);
Ui::DrawRoundedRect(p, geometry, channel.bg, channel.corners);
if (channel.ripple) {
channel.ripple->paint(
p,

View file

@ -69,6 +69,9 @@ private:
QRect geometry;
ClickHandlerPtr link;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
mutable std::array<QImage, 4> corners;
mutable QColor bg;
uint8 colorIndex = 0;
};
void paintBadge(Painter &p, const PaintContext &context) const;
@ -94,10 +97,8 @@ private:
Ui::Text::String _winnersTitle;
Ui::Text::String _winners;
mutable QColor _channelBg;
mutable QColor _badgeFg;
mutable QColor _badgeBorder;
mutable std::array<QImage, 4> _channelCorners;
mutable QImage _badge;
mutable QImage _badgeCache;

View file

@ -325,7 +325,7 @@ void UnwrappedMedia::drawSurrounding(
recty += skip;
} else if (via) {
p.setFont(st::msgDateFont);
p.drawTextLeft(rectx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text);
p.drawTextLeft(textx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text);
const auto skip = st::msgServiceNameFont->height
+ (reply ? st::msgReplyPadding.top() : 0);

View file

@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "data/data_auto_download.h"
#include "data/data_web_page.h"
#include "core/application.h"
#include "styles/style_chat.h"
@ -242,6 +243,7 @@ QSize Photo::countCurrentSize(int newWidth) {
maxWidth());
newWidth = qMax(pix.width(), minWidth);
auto newHeight = qMax(pix.height(), st::minPhotoSize);
auto imageHeight = newHeight;
if (_parent->hasBubble() && !_caption.isEmpty()) {
auto captionMaxWidth = st::msgPadding.left()
+ _caption.maxWidth()
@ -252,7 +254,7 @@ QSize Photo::countCurrentSize(int newWidth) {
}
const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
newHeight = adjustHeightForLessCrop(
imageHeight = newHeight = adjustHeightForLessCrop(
dimensions,
{ newWidth, newHeight });
const auto captionw = newWidth
@ -266,6 +268,15 @@ QSize Photo::countCurrentSize(int newWidth) {
newHeight += st::msgPadding.bottom();
}
}
const auto enlargeInner = st::historyPageEnlargeSize;
const auto enlargeOuter = 2 * st::historyPageEnlargeSkip + enlargeInner;
const auto showEnlarge = (_parent->media() != this)
&& _parent->data()->media()
&& _parent->data()->media()->webpage()
&& _parent->data()->media()->webpage()->suggestEnlargePhoto()
&& (newWidth >= enlargeOuter)
&& (imageHeight >= enlargeOuter);
_showEnlarge = showEnlarge ? 1 : 0;
return { newWidth, newHeight };
}
@ -351,15 +362,16 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
fillImageOverlay(p, rthumb, rounding, context);
}
}
if (radial || (!loaded && !_data->loading())) {
const auto radialOpacity = (radial && loaded && !_data->uploading())
? _animation->radial.opacity() :
1.;
const auto innerSize = st::msgFileLayout.thumbSize;
QRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);
const auto showEnlarge = loaded && _showEnlarge;
const auto paintInCenter = (radial || (!loaded && !_data->loading()));
if (paintInCenter || showEnlarge) {
p.setPen(Qt::NoPen);
if (context.selected()) {
p.setBrush(st->msgDateImgBgSelected());
} else if (showEnlarge) {
const auto over = ClickHandler::showAsActive(_openl);
p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
} else if (isThumbAnimation()) {
const auto over = _animation->a_thumbOver.value(1.);
p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));
@ -367,6 +379,13 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
const auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
}
}
if (paintInCenter) {
const auto radialOpacity = (radial && loaded && !_data->uploading())
? _animation->radial.opacity() :
1.;
const auto innerSize = st::msgFileLayout.thumbSize;
QRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);
p.setOpacity(radialOpacity * p.opacity());
@ -386,6 +405,13 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);
}
}
if (showEnlarge) {
auto hq = PainterHighQualityEnabler(p);
const auto rect = enlargeRect();
const auto radius = st::historyPageEnlargeRadius;
p.drawRoundedRect(rect, radius, radius);
sti->historyPageEnlarge.paintInCenter(p, rect);
}
// date
if (!_caption.isEmpty()) {
@ -631,6 +657,18 @@ QSize Photo::photoSize() const {
return QSize(_data->width(), _data->height());
}
QRect Photo::enlargeRect() const {
const auto skip = st::historyPageEnlargeSkip;
const auto enlargeInner = st::historyPageEnlargeSize;
const auto enlargeOuter = 2 * skip + enlargeInner;
return {
width() - enlargeOuter + skip,
skip,
enlargeInner,
enlargeInner,
};
}
TextState Photo::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
@ -673,6 +711,11 @@ TextState Photo::textState(QPoint point, StateRequest request) const {
: _data->loading()
? _cancell
: _savel;
if (_showEnlarge
&& result.link == _openl
&& enlargeRect().contains(point)) {
result.cursor = CursorState::Enlarge;
}
}
if (_caption.isEmpty() && _parent->media() == this) {
auto fullRight = paintx + paintw;

View file

@ -163,6 +163,7 @@ private:
QPoint photoPosition) const;
[[nodiscard]] QSize photoSize() const;
[[nodiscard]] QRect enlargeRect() const;
void togglePollingStory(bool enabled) const;
@ -178,6 +179,7 @@ private:
mutable uint32 _imageCacheForum : 1 = 0;
mutable uint32 _imageCacheBlurred : 1 = 0;
mutable uint32 _pollingStory : 1 = 0;
mutable uint32 _showEnlarge : 1 = 0;
};

View file

@ -783,7 +783,11 @@ TextState WebPage::textState(QPoint point, StateRequest request) const {
auto attachTop = tshift - bubble.top();
if (rtl()) attachLeft = width() - attachLeft - _attach->width();
result = _attach->textState(point - QPoint(attachLeft, attachTop), request);
result.link = replaceAttachLink(result.link);
if (result.cursor == CursorState::Enlarge) {
result.cursor = CursorState::None;
} else {
result.link = replaceAttachLink(result.link);
}
}
}
if (!result.link && outer.contains(point)) {

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "countries/countries_instance.h"
#include "data/data_peer.h"
#include "info/boosts/giveaway/boost_badge.h"
#include "info/boosts/giveaway/giveaway_list_controllers.h"
#include "info/boosts/giveaway/giveaway_type_row.h"
#include "info/boosts/giveaway/select_countries_box.h"
@ -24,9 +25,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_common.h"
#include "settings/settings_premium.h" // Settings::ShowPremium
#include "ui/boxes/choose_date_time.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_top_bar.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
@ -67,47 +71,172 @@ constexpr auto kDoneTooltipDuration = 5 * crl::time(1000);
};
}
[[nodiscard]] QWidget *FindFirstShadowInBox(not_null<Ui::BoxContent*> box) {
for (const auto &child : box->children()) {
if (child && child->isWidgetType()) {
const auto w = static_cast<QWidget*>(child);
if (w->height() == st::lineWidth) {
return w;
}
}
}
return nullptr;
}
void AddPremiumTopBarWithDefaultTitleBar(
not_null<Ui::GenericBox*> box,
rpl::producer<> showFinished,
rpl::producer<QString> titleText) {
struct State final {
Ui::Animations::Simple animation;
Ui::Text::String title;
Ui::RpWidget close;
};
const auto state = box->lifetime().make_state<State>();
box->setNoContentMargin(true);
std::move(
titleText
) | rpl::start_with_next([=](const QString &s) {
state->title.setText(st::startGiveawayBox.title.style, s);
}, box->lifetime());
const auto hPadding = rect::m::sum::h(st::boxRowPadding);
const auto titlePaintContext = Ui::Text::PaintContext{
.position = st::boxTitlePosition,
.outerWidth = (st::boxWideWidth - hPadding),
.availableWidth = (st::boxWideWidth - hPadding),
};
const auto isCloseBarShown = [=] { return box->scrollTop() > 0; };
const auto closeTopBar = box->setPinnedToTopContent(
object_ptr<Ui::RpWidget>(box));
closeTopBar->resize(box->width(), st::boxTitleHeight);
closeTopBar->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = Painter(closeTopBar);
const auto radius = st::boxRadius;
const auto progress = state->animation.value(isCloseBarShown()
? 1.
: 0.);
const auto resultRect = r + QMargins{ 0, 0, 0, radius };
{
auto hq = PainterHighQualityEnabler(p);
if (progress < 1.) {
auto path = QPainterPath();
path.addRect(resultRect);
path.addRect(
st::boxRowPadding.left(),
0,
resultRect.width() - hPadding,
resultRect.height());
p.setClipPath(path);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(st::boxDividerBg);
p.drawRoundedRect(resultRect, radius, radius);
}
if (progress > 0.) {
p.setOpacity(progress);
p.setClipping(false);
p.setPen(Qt::NoPen);
p.setBrush(st::boxBg);
p.drawRoundedRect(resultRect, radius, radius);
p.setPen(st::startGiveawayBox.title.textFg);
p.setBrush(Qt::NoBrush);
state->title.draw(p, titlePaintContext);
}
}
}, closeTopBar->lifetime());
{
const auto close = Ui::CreateChild<Ui::IconButton>(
closeTopBar.get(),
st::startGiveawayBoxTitleClose);
close->setClickedCallback([=] { box->closeBox(); });
closeTopBar->widthValue(
) | rpl::start_with_next([=](int w) {
const auto &pos = st::giveawayGiftCodeCoverClosePosition;
close->moveToRight(pos.x(), pos.y());
}, box->lifetime());
close->show();
}
const auto bar = Ui::CreateChild<Ui::Premium::TopBar>(
box.get(),
st::startGiveawayCover,
nullptr,
tr::lng_giveaway_new_title(),
tr::lng_giveaway_new_about(Ui::Text::RichLangValue),
true,
false);
bar->setAttribute(Qt::WA_TransparentForMouseEvents);
box->addRow(
object_ptr<Ui::BoxContentDivider>(
box.get(),
st::giveawayGiftCodeTopHeight
- st::boxTitleHeight
+ st::boxDividerHeight
+ st::settingsSectionSkip,
st::boxDividerBg,
RectPart::Bottom),
{});
bar->setPaused(true);
bar->setRoundEdges(false);
bar->setMaximumHeight(st::giveawayGiftCodeTopHeight);
bar->setMinimumHeight(st::infoLayerTopBarHeight);
bar->resize(bar->width(), bar->maximumHeight());
box->widthValue(
) | rpl::start_with_next([=](int w) {
bar->resizeToWidth(w - hPadding);
bar->moveToLeft(st::boxRowPadding.left(), bar->y());
}, box->lifetime());
std::move(
showFinished
) | rpl::take(1) | rpl::start_with_next([=] {
closeTopBar->raise();
if (const auto shadow = FindFirstShadowInBox(box)) {
bar->stackUnder(shadow);
}
bar->setPaused(false);
box->scrolls(
) | rpl::map(isCloseBarShown) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool showBar) {
state->animation.stop();
state->animation.start(
[=] { closeTopBar->update(); },
showBar ? 0. : 1.,
showBar ? 1. : 0.,
st::slideWrapDuration);
}, box->lifetime());
box->scrolls(
) | rpl::start_with_next([=] {
bar->moveToLeft(bar->x(), -box->scrollTop());
}, box->lifetime());
}, box->lifetime());
bar->show();
}
} // namespace
void CreateGiveawayBox(
not_null<Ui::GenericBox*> box,
not_null<Info::Controller*> controller,
not_null<PeerData*> peer) {
not_null<PeerData*> peer,
Fn<void()> reloadOnDone,
std::optional<Data::BoostPrepaidGiveaway> prepaid) {
box->setWidth(st::boxWideWidth);
const auto weakWindow = base::make_weak(controller->parentController());
const auto bar = box->verticalLayout()->add(
object_ptr<Ui::Premium::TopBar>(
box,
st::giveawayGiftCodeCover,
nullptr,
tr::lng_giveaway_new_title(),
tr::lng_giveaway_new_about(Ui::Text::RichLangValue),
true));
{
bar->setPaused(true);
bar->setMaximumHeight(st::giveawayGiftCodeTopHeight);
bar->setMinimumHeight(st::infoLayerTopBarHeight);
bar->resize(bar->width(), bar->maximumHeight());
const auto container = box->verticalLayout();
const auto &padding = st::giveawayGiftCodeCoverDividerPadding;
Settings::AddSkip(container, padding.top());
Settings::AddDivider(container);
Settings::AddSkip(container, padding.bottom());
const auto close = Ui::CreateChild<Ui::IconButton>(
container.get(),
st::boxTitleClose);
close->setClickedCallback([=] { box->closeBox(); });
box->widthValue(
) | rpl::start_with_next([=](int) {
const auto &pos = st::giveawayGiftCodeCoverClosePosition;
close->moveToRight(pos.x(), pos.y());
}, box->lifetime());
}
using GiveawayType = Giveaway::GiveawayTypeRow::Type;
using GiveawayGroup = Ui::RadioenumGroup<GiveawayType>;
struct State final {
@ -127,11 +256,25 @@ void CreateGiveawayBox(
rpl::variable<TimeId> dateValue;
rpl::variable<std::vector<QString>> countriesValue;
bool confirmButtonBusy = false;
rpl::variable<bool> confirmButtonBusy = true;
};
const auto state = box->lifetime().make_state<State>(peer);
const auto typeGroup = std::make_shared<GiveawayGroup>();
auto showFinished = Ui::BoxShowFinishes(box);
AddPremiumTopBarWithDefaultTitleBar(
box,
rpl::duplicate(showFinished),
rpl::conditional(
state->typeValue.value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Random),
tr::lng_giveaway_start(),
tr::lng_giveaway_award()));
{
const auto &padding = st::giveawayGiftCodeCoverDividerPadding;
Settings::AddSkip(box->verticalLayout(), padding.bottom());
}
const auto loading = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
@ -157,7 +300,24 @@ void CreateGiveawayBox(
object_ptr<Ui::VerticalLayout>(box)));
contentWrap->toggle(false, anim::type::instant);
{
if (prepaid) {
contentWrap->entity()->add(
object_ptr<Giveaway::GiveawayTypeRow>(
box,
GiveawayType::Prepaid,
prepaid->id,
tr::lng_boosts_prepaid_giveaway_single(),
tr::lng_boosts_prepaid_giveaway_status(
lt_count,
rpl::single(prepaid->quantity) | tr::to_count(),
lt_duration,
tr::lng_premium_gift_duration_months(
lt_count,
rpl::single(prepaid->months) | tr::to_count())),
QImage())
)->setAttribute(Qt::WA_TransparentForMouseEvents);
}
if (!prepaid) {
const auto row = contentWrap->entity()->add(
object_ptr<Giveaway::GiveawayTypeRow>(
box,
@ -168,7 +328,7 @@ void CreateGiveawayBox(
state->typeValue.force_assign(GiveawayType::Random);
});
}
{
if (!prepaid) {
const auto row = contentWrap->entity()->add(
object_ptr<Giveaway::GiveawayTypeRow>(
box,
@ -209,7 +369,8 @@ void CreateGiveawayBox(
using Controller = Giveaway::AwardMembersListController;
auto listController = std::make_unique<Controller>(
controller,
peer);
peer,
state->selectedToAward);
listController->setCheckError(CreateErrorCallback(
state->apiOptions.giveawayAddPeersMax(),
tr::lng_giveaway_maximum_users_error));
@ -237,10 +398,19 @@ void CreateGiveawayBox(
randomWrap->toggle(type == GiveawayType::Random, anim::type::instant);
}, randomWrap->lifetime());
randomWrap->toggleOn(
state->typeValue.value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Random),
anim::type::instant);
const auto sliderContainer = randomWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(randomWrap));
const auto fillSliderContainer = [=] {
const auto availablePresets = state->apiOptions.availablePresets();
if (prepaid) {
state->sliderValue = prepaid->quantity;
return;
}
if (availablePresets.empty()) {
return;
}
@ -274,8 +444,20 @@ void CreateGiveawayBox(
const auto &padding = st::giveawayGiftCodeSliderPadding;
Settings::AddSkip(sliderContainer, padding.top());
class Slider : public Ui::MediaSlider {
public:
using Ui::MediaSlider::MediaSlider;
protected:
void wheelEvent(QWheelEvent *e) override {
e->ignore();
}
};
const auto slider = sliderContainer->add(
object_ptr<Ui::MediaSlider>(sliderContainer, st::settingsScale),
object_ptr<Slider>(sliderContainer, st::settingsScale),
st::boxRowPadding);
Settings::AddSkip(sliderContainer, padding.bottom());
slider->resize(slider->width(), st::settingsScale.seekSize.height());
@ -468,6 +650,24 @@ void CreateGiveawayBox(
Settings::AddSkip(countriesContainer);
}
const auto addTerms = [=](not_null<Ui::VerticalLayout*> c) {
auto terms = object_ptr<Ui::FlatLabel>(
c,
tr::lng_premium_gift_terms(
lt_link,
tr::lng_premium_gift_terms_link(
) | rpl::map([](const QString &t) {
return Ui::Text::Link(t, 1);
}),
Ui::Text::WithEntities),
st::boxDividerLabel);
terms->setLink(1, std::make_shared<LambdaClickHandler>([=] {
box->closeBox();
Settings::ShowPremium(&peer->session(), QString());
}));
c->add(std::move(terms));
};
{
const auto dateContainer = randomWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(randomWrap));
@ -505,18 +705,38 @@ void CreateGiveawayBox(
});
Settings::AddSkip(dateContainer);
Settings::AddDividerText(
dateContainer,
tr::lng_giveaway_date_about(
lt_count,
state->sliderValue.value() | tr::to_count()));
Settings::AddSkip(dateContainer);
if (prepaid) {
auto terms = object_ptr<Ui::VerticalLayout>(dateContainer);
terms->add(object_ptr<Ui::FlatLabel>(
terms,
tr::lng_giveaway_date_about(
lt_count,
state->sliderValue.value() | tr::to_count()),
st::boxDividerLabel));
Settings::AddSkip(terms.data());
Settings::AddSkip(terms.data());
addTerms(terms.data());
dateContainer->add(object_ptr<Ui::DividerLabel>(
dateContainer,
std::move(terms),
st::settingsDividerLabelPadding));
} else {
Settings::AddDividerText(
dateContainer,
tr::lng_giveaway_date_about(
lt_count,
state->sliderValue.value() | tr::to_count()));
Settings::AddSkip(dateContainer);
}
}
const auto durationGroup = std::make_shared<Ui::RadiobuttonGroup>(0);
const auto listOptions = contentWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(box));
const auto rebuildListOptions = [=](int amountUsers) {
if (prepaid) {
return;
}
while (listOptions->count()) {
delete listOptions->widgetAt(0);
}
@ -535,29 +755,16 @@ void CreateGiveawayBox(
Settings::AddSkip(listOptions);
auto terms = object_ptr<Ui::FlatLabel>(
listOptions,
tr::lng_premium_gift_terms(
lt_link,
tr::lng_premium_gift_terms_link(
) | rpl::map([](const QString &t) {
return Ui::Text::Link(t, 1);
}),
Ui::Text::WithEntities),
st::boxDividerLabel);
terms->setLink(1, std::make_shared<LambdaClickHandler>([=] {
box->closeBox();
Settings::ShowPremium(&peer->session(), QString());
}));
auto termsContainer = object_ptr<Ui::VerticalLayout>(listOptions);
addTerms(termsContainer.data());
listOptions->add(object_ptr<Ui::DividerLabel>(
listOptions,
std::move(terms),
std::move(termsContainer),
st::settingsDividerLabelPadding));
box->verticalLayout()->resizeToWidth(box->width());
};
{
if (!prepaid) {
rpl::combine(
state->sliderValue.value(),
state->typeValue.value()
@ -567,27 +774,54 @@ void CreateGiveawayBox(
? state->selectedToAward.size()
: users);
}, box->lifetime());
} else {
typeGroup->setValue(GiveawayType::Random);
}
{
// TODO mini-icon.
const auto &stButton = st::premiumGiftBox;
using namespace Info::Statistics;
const auto &stButton = st::startGiveawayBox;
box->setStyle(stButton);
auto button = object_ptr<Ui::RoundButton>(
box,
state->toAwardAmountChanged.events_starting_with(
rpl::empty_value()
) | rpl::map([=] {
return (typeGroup->value() == GiveawayType::SpecificUsers)
? tr::lng_giveaway_award()
: tr::lng_giveaway_start();
}) | rpl::flatten_latest(),
rpl::never<QString>(),
st::giveawayGiftCodeStartButton);
AddLabelWithBadgeToButton(
button,
rpl::conditional(
state->typeValue.value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Random),
tr::lng_giveaway_start(),
tr::lng_giveaway_award()),
state->sliderValue.value(
) | rpl::map([=](int v) -> int {
return state->apiOptions.giveawayBoostsPerPremium() * v;
}),
state->confirmButtonBusy.value() | rpl::map(!rpl::mappers::_1));
{
const auto loadingAnimation = InfiniteRadialAnimationWidget(
button,
st::giveawayGiftCodeStartButton.height / 2);
button->sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
const auto size = loadingAnimation->size();
loadingAnimation->moveToLeft(
(s.width() - size.width()) / 2,
(s.height() - size.height()) / 2);
}, loadingAnimation->lifetime());
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->resizeToWidth(box->width()
- stButton.buttonPadding.left()
- stButton.buttonPadding.right());
state->typeValue.value(
) | rpl::start_with_next([=, raw = button.data()] {
raw->resizeToWidth(box->width()
- stButton.buttonPadding.left()
- stButton.buttonPadding.right());
}, button->lifetime());
button->setClickedCallback([=] {
if (state->confirmButtonBusy) {
if (state->confirmButtonBusy.current()) {
return;
}
const auto type = typeGroup->value();
@ -600,7 +834,10 @@ void CreateGiveawayBox(
isSpecific
? state->selectedToAward.size()
: state->sliderValue.current(),
durationGroup->value());
prepaid
? prepaid->months
: state->apiOptions.monthsFromPreset(
durationGroup->value()));
if (isSpecific) {
if (state->selectedToAward.empty()) {
return;
@ -633,12 +870,15 @@ void CreateGiveawayBox(
const auto show = box->uiShow();
const auto weak = Ui::MakeWeak(box.get());
const auto done = [=](Payments::CheckoutResult result) {
if (const auto strong = weak.data()) {
state->confirmButtonBusy = false;
strong->window()->setFocus();
strong->closeBox();
const auto isPaid = result == Payments::CheckoutResult::Paid;
if (result == Payments::CheckoutResult::Pending || isPaid) {
if (const auto strong = weak.data()) {
strong->window()->setFocus();
strong->closeBox();
}
}
if (result == Payments::CheckoutResult::Paid) {
if (isPaid) {
reloadOnDone();
const auto filter = [=](const auto &...) {
if (const auto window = weakWindow.get()) {
window->showSection(Info::Boosts::Make(peer));
@ -665,28 +905,71 @@ void CreateGiveawayBox(
.adaptive = true,
.filter = filter,
});
} else {
state->confirmButtonBusy = false;
}
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
const auto startPrepaid = [=](Fn<void()> close) {
if (!weak) {
close();
return;
}
state->apiOptions.applyPrepaid(
invoice,
prepaid->id
) | rpl::start_with_error_done([=](const QString &error) {
if (const auto window = weakWindow.get()) {
window->uiShow()->showToast(error);
close();
done(Payments::CheckoutResult::Cancelled);
}
}, [=] {
close();
done(Payments::CheckoutResult::Paid);
}, box->lifetime());
};
if (prepaid) {
const auto cancel = [=](Fn<void()> close) {
if (weak) {
state->confirmButtonBusy = false;
}
close();
};
show->show(Ui::MakeConfirmBox({
.text = tr::lng_giveaway_start_sure(tr::now),
.confirmed = startPrepaid,
.cancelled = cancel,
}));
} else {
Payments::CheckoutProcess::Start(std::move(invoice), done);
}
});
box->addButton(std::move(button));
}
state->typeValue.force_assign(GiveawayType::Random);
box->setShowFinishedCallback([=] {
std::move(
showFinished
) | rpl::take(1) | rpl::start_with_next([=] {
if (!loading->toggled()) {
return;
}
bar->setPaused(false);
state->lifetimeApi = state->apiOptions.request(
) | rpl::start_with_error_done([=](const QString &error) {
}, [=] {
const auto done = [=] {
state->lifetimeApi.destroy();
loading->toggle(false, anim::type::instant);
state->confirmButtonBusy = false;
fillSliderContainer();
rebuildListOptions(1);
contentWrap->toggle(true, anim::type::instant);
contentWrap->resizeToWidth(box->width());
});
});
};
if (prepaid) {
return done();
}
state->lifetimeApi = state->apiOptions.request(
) | rpl::start_with_error_done([=](const QString &error) {
box->uiShow()->showToast(error);
box->closeBox();
}, done);
}, box->lifetime());
}

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class PeerData;
namespace Data {
struct BoostPrepaidGiveaway;
} // namespace Data
namespace Info {
class Controller;
} // namespace Info
@ -20,4 +24,6 @@ class GenericBox;
void CreateGiveawayBox(
not_null<Ui::GenericBox*> box,
not_null<Info::Controller*> controller,
not_null<PeerData*> peer);
not_null<PeerData*> peer,
Fn<void()> reloadOnDone,
std::optional<Data::BoostPrepaidGiveaway> prepaidGiveaway);

View file

@ -0,0 +1,173 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/boosts/giveaway/boost_badge.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/widgets/labels.h"
#include "styles/style_giveaway.h"
#include "styles/style_statistics.h"
#include "styles/style_widgets.h"
namespace Info::Statistics {
not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
not_null<Ui::RpWidget*> parent,
int size) {
class Widget final : public Ui::RpWidget {
public:
Widget(not_null<Ui::RpWidget*> p, int size)
: Ui::RpWidget(p)
, _animation([=] { update(); }, st::startGiveawayButtonLoading) {
resize(size, size);
shownValue() | rpl::start_with_next([=](bool v) {
return v
? _animation.start()
: _animation.stop(anim::type::instant);
}, lifetime());
}
protected:
void paintEvent(QPaintEvent *e) override {
auto p = QPainter(this);
p.setPen(st::activeButtonFg);
p.setBrush(st::activeButtonFg);
const auto r = rect()
- Margins(st::startGiveawayButtonLoading.thickness);
_animation.draw(p, r.topLeft(), r.size(), width());
}
private:
Ui::InfiniteRadialAnimation _animation;
};
return Ui::CreateChild<Widget>(parent.get(), size);
}
QImage CreateBadge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon) {
auto badgeText = Ui::Text::String(textStyle, text);
const auto badgeTextWidth = badgeText.maxWidth();
const auto badgex = 0;
const auto badgey = 0;
const auto badgeh = 0 + badgeHeight;
const auto badgew = badgeTextWidth
+ rect::m::sum::h(textPadding);
auto result = QImage(
QSize(badgew, badgeh) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = Painter(&result);
p.setPen(Qt::NoPen);
p.setBrush(bg);
const auto r = QRect(badgex, badgey, badgew, badgeh);
{
auto hq = PainterHighQualityEnabler(p);
auto o = ScopedPainterOpacity(p, bgOpacity);
p.drawRoundedRect(r, badgeh / 2, badgeh / 2);
}
p.setPen(fg);
p.setBrush(Qt::NoBrush);
badgeText.drawLeftElided(
p,
r.x() + textPadding.left(),
badgey + textPadding.top(),
badgew,
badgew * 2);
icon.paint(
p,
QPoint(r.x() + iconPadding.left(), r.y() + iconPadding.top()),
badgew * 2);
}
return result;
}
void AddLabelWithBadgeToButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> text,
rpl::producer<int> number,
rpl::producer<bool> shown) {
struct State {
QImage badge;
};
const auto state = parent->lifetime().make_state<State>();
const auto label = Ui::CreateChild<Ui::LabelSimple>(
parent.get(),
st::startGiveawayButtonLabelSimple);
std::move(
text
) | rpl::start_with_next([=](const QString &s) {
label->setText(s);
}, label->lifetime());
const auto count = Ui::CreateChild<Ui::RpWidget>(parent.get());
count->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(count);
p.drawImage(0, 0, state->badge);
}, count->lifetime());
std::move(
number
) | rpl::start_with_next([=](int c) {
state->badge = Info::Statistics::CreateBadge(
st::startGiveawayButtonTextStyle,
QString::number(c),
st::boostsListBadgeHeight,
st::startGiveawayButtonBadgeTextPadding,
st::activeButtonFg,
st::activeButtonBg,
1.,
st::boostsListMiniIconPadding,
st::startGiveawayButtonMiniIcon);
count->resize(state->badge.size() / style::DevicePixelRatio());
count->update();
}, count->lifetime());
std::move(
shown
) | rpl::start_with_next([=](bool shown) {
count->setVisible(shown);
label->setVisible(shown);
}, count->lifetime());
rpl::combine(
parent->sizeValue(),
label->sizeValue(),
count->sizeValue()
) | rpl::start_with_next([=](
const QSize &s,
const QSize &s1,
const QSize &s2) {
const auto sum = st::startGiveawayButtonMiniIconSkip
+ s1.width()
+ s2.width();
const auto contentLeft = (s.width() - sum) / 2;
label->moveToLeft(contentLeft, (s.height() - s1.height()) / 2);
count->moveToLeft(
contentLeft + sum - s2.width(),
(s.height() - s2.height()) / 2 + st::boostsListMiniIconSkip);
}, parent->lifetime());
}
} // namespace Info::Statistics

View file

@ -0,0 +1,41 @@
/*
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 style {
struct TextStyle;
} // namespace style
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Info::Statistics {
[[nodiscard]] QImage CreateBadge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon);
[[nodiscard]] not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
not_null<Ui::RpWidget*> parent,
int size);
void AddLabelWithBadgeToButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> text,
rpl::producer<int> number,
rpl::producer<bool> shown);
} // namespace Info::Statistics

View file

@ -163,3 +163,34 @@ giveawayRefundedLabel: FlatLabel(boxLabel) {
textFg: attentionButtonFg;
}
giveawayRefundedPadding: margins(8px, 10px, 8px, 10px);
startGiveawayBox: Box(premiumGiftBox) {
shadowIgnoreTopSkip: true;
}
startGiveawayScrollArea: ScrollArea(boxScroll) {
deltax: 3px;
deltat: 50px;
}
startGiveawayBoxTitleClose: IconButton(boxTitleClose) {
ripple: universalRippleAnimation;
}
startGiveawayCover: PremiumCover(giveawayGiftCodeCover) {
bg: boxDividerBg;
additionalShadowForDarkThemes: false;
}
startGiveawayButtonLabelSimple: LabelSimple {
font: semiboldFont;
textFg: activeButtonFg;
}
startGiveawayButtonMiniIcon: icon{{ "boosts/boost_mini2", activeButtonBg }};
startGiveawayButtonMiniIconSkip: 5px;
startGiveawayButtonBadgeTextPadding: margins(16px, -1px, 6px, 0px);
startGiveawayButtonTextStyle: TextStyle(defaultTextStyle) {
font: semiboldFont;
}
startGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
color: activeButtonFg;
thickness: 2px;
}

View file

@ -9,9 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_folder.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "dialogs/dialogs_indexed_list.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
@ -107,8 +110,16 @@ void ChannelRow::rightActionStopLastRipple() {
AwardMembersListController::AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer)
: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members) {
not_null<PeerData*> peer,
std::vector<not_null<PeerData*>> selected)
: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members)
, _selected(std::move(selected)) {
}
void AwardMembersListController::prepare() {
ParticipantsBoxController::prepare();
delegate()->peerListAddSelectedPeers(base::take(_selected));
delegate()->peerListRefreshRows();
}
void AwardMembersListController::rowClicked(not_null<PeerListRow*> row) {
@ -148,7 +159,26 @@ MyChannelsListController::MyChannelsListController(
std::make_unique<PeerListGlobalSearchController>(&peer->session()))
, _peer(peer)
, _show(show)
, _selected(std::move(selected)) {
, _selected(std::move(selected))
, _otherChannels(std::make_unique<std::vector<not_null<ChannelData*>>>()) {
{
const auto addList = [&](not_null<Dialogs::IndexedList*> list) {
for (const auto &row : list->all()) {
if (const auto history = row->history()) {
const auto channel = history->peer->asChannel();
if (channel && !channel->isMegagroup()) {
_otherChannels->push_back(channel);
}
}
}
};
auto &data = _peer->owner();
addList(data.chatsList()->indexed());
if (const auto folder = data.folderLoaded(Data::Folder::kId)) {
addList(folder->chatsList()->indexed());
}
addList(data.contactsNoChatsList());
}
}
std::unique_ptr<PeerListRow> MyChannelsListController::createSearchRow(
@ -167,6 +197,24 @@ std::unique_ptr<PeerListRow> MyChannelsListController::createRestoredRow(
return nullptr;
}
void MyChannelsListController::loadMoreRows() {
if (_apiLifetime || !_otherChannels) {
return;
} else if (_lastAddedIndex >= _otherChannels->size()) {
_otherChannels.release();
return;
}
constexpr auto kPerPage = int(40);
const auto till = std::min(
int(_otherChannels->size()),
_lastAddedIndex + kPerPage);
while (_lastAddedIndex < till) {
delegate()->peerListAppendRow(
createRow(_otherChannels->at(_lastAddedIndex++)));
}
delegate()->peerListRefreshRows();
}
void MyChannelsListController::rowClicked(not_null<PeerListRow*> row) {
const auto channel = row->peer()->asChannel();
const auto checked = !row->checked();

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_participants_box.h"
class ChannelData;
class PeerData;
class PeerListRow;
@ -27,7 +28,10 @@ class AwardMembersListController : public ParticipantsBoxController {
public:
AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
not_null<PeerData*> peer,
std::vector<not_null<PeerData*>> selected);
void prepare() override;
void setCheckError(Fn<bool(int)> callback);
@ -41,6 +45,8 @@ public:
private:
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
};
class MyChannelsListController : public PeerListController {
@ -55,6 +61,7 @@ public:
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
@ -71,6 +78,8 @@ private:
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
std::unique_ptr<std::vector<not_null<ChannelData*>>> _otherChannels;
int _lastAddedIndex = 0;
rpl::lifetime _apiLifetime;

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h"
#include "styles/style_boxes.h"
#include "styles/style_giveaway.h"
#include "styles/style_statistics.h"
namespace Giveaway {
@ -24,31 +25,49 @@ GiveawayTypeRow::GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> subtitle)
: GiveawayTypeRow(
parent,
type,
(type == Type::SpecificUsers) ? kColorIndexSpecific : kColorIndexRandom,
(type == Type::SpecificUsers)
? tr::lng_giveaway_award_option()
: (type == Type::Random)
? tr::lng_giveaway_create_option()
: (type == Type::AllMembers)
? tr::lng_giveaway_users_all()
: tr::lng_giveaway_users_new(),
std::move(subtitle),
QImage()) {
}
GiveawayTypeRow::GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
int colorIndex,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
QImage badge)
: RippleButton(parent, st::defaultRippleAnimation)
, _type(type)
, _st((_type == Type::SpecificUsers || _type == Type::Random)
? st::giveawayTypeListItem
: (_type == Type::Prepaid)
? st::boostsListBox.item
: st::giveawayGiftCodeMembersPeerList.item)
, _userpic(
Ui::EmptyUserpic::UserpicColor((_type == Type::SpecificUsers)
? kColorIndexSpecific
: kColorIndexRandom),
Ui::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex(colorIndex)),
QString())
, _name(
_st.nameStyle,
(type == Type::SpecificUsers)
? tr::lng_giveaway_award_option(tr::now)
: (type == Type::Random)
? tr::lng_giveaway_create_option(tr::now)
: (type == Type::AllMembers)
? tr::lng_giveaway_users_all(tr::now)
: tr::lng_giveaway_users_new(tr::now),
Ui::NameTextOptions()) {
, _badge(std::move(badge)) {
std::move(
subtitle
) | rpl::start_with_next([=] (const QString &s) {
_status.setText(st::defaultTextStyle, s, Ui::NameTextOptions());
}, lifetime());
std::move(
title
) | rpl::start_with_next([=] (const QString &s) {
_name.setText(_st.nameStyle, s, Ui::NameTextOptions());
}, lifetime());
}
int GiveawayTypeRow::resizeGetHeight(int) {
@ -62,7 +81,10 @@ void GiveawayTypeRow::paintEvent(QPaintEvent *e) {
const auto skipRight = _st.photoPosition.x();
const auto outerWidth = width();
const auto isSpecific = (_type == Type::SpecificUsers);
const auto hasUserpic = (_type == Type::Random) || isSpecific;
const auto isPrepaid = (_type == Type::Prepaid);
const auto hasUserpic = (_type == Type::Random)
|| isSpecific
|| isPrepaid;
if (paintOver) {
p.fillRect(e->rect(), _st.button.textBgOver);
@ -92,8 +114,19 @@ void GiveawayTypeRow::paintEvent(QPaintEvent *e) {
const auto namey = _st.namePosition.y();
const auto namew = outerWidth - namex - skipRight;
const auto badgew = _badge.width() / style::DevicePixelRatio();
p.setPen(_st.nameFg);
_name.drawLeftElided(p, namex, namey, namew, width());
_name.drawLeftElided(p, namex, namey, namew - badgew, width());
if (!_badge.isNull()) {
p.drawImage(
std::min(
namex + _name.maxWidth() + st::boostsListBadgePadding.left(),
outerWidth - badgew - skipRight),
namey + st::boostsListMiniIconSkip,
_badge);
}
const auto statusx = _st.statusPosition.x();
const auto statusy = _st.statusPosition.y();

View file

@ -25,6 +25,8 @@ public:
AllMembers,
OnlyNewMembers,
Prepaid,
};
GiveawayTypeRow(
@ -32,6 +34,14 @@ public:
Type type,
rpl::producer<QString> subtitle);
GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
int colorIndex,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
QImage badge);
void addRadio(std::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup);
protected:
@ -47,6 +57,8 @@ private:
Ui::Text::String _status;
Ui::Text::String _name;
QImage _badge;
};
} // namespace Giveaway

View file

@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/boosts/create_giveaway_box.h"
#include "info/boosts/giveaway/boost_badge.h"
#include "info/boosts/giveaway/giveaway_type_row.h"
#include "info/boosts/info_boosts_widget.h"
#include "info/info_controller.h"
#include "info/profile/info_profile_icon.h"
@ -24,13 +26,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_common.h"
#include "statistics/widgets/chart_header_widget.h"
#include "ui/boxes/boost_box.h"
#include "ui/controls/invite_link_buttons.h"
#include "ui/controls/invite_link_label.h"
#include "ui/effects/ripple_animation.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_giveaway.h"
#include "styles/style_info.h"
#include "styles/style_statistics.h"
@ -179,10 +184,35 @@ void FillShareLink(
label->clicks(
) | rpl::start_with_next(copyLink, label->lifetime());
const auto copyShareWrap = content->add(
object_ptr<Ui::VerticalLayout>(content));
Ui::AddCopyShareLinkButtons(copyShareWrap, copyLink, shareLink);
copyShareWrap->widgetAt(0)->showChildren();
{
const auto wrap = content->add(
object_ptr<Ui::FixedHeightWidget>(
content,
st::inviteLinkButton.height),
st::inviteLinkButtonsPadding);
const auto copy = CreateChild<Ui::RoundButton>(
wrap,
tr::lng_group_invite_context_copy(),
st::inviteLinkCopy);
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
copy->setClickedCallback(copyLink);
const auto share = CreateChild<Ui::RoundButton>(
wrap,
tr::lng_group_invite_context_share(),
st::inviteLinkShare);
share->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
share->setClickedCallback(shareLink);
wrap->widthValue(
) | rpl::start_with_next([=](int width) {
const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;
copy->setFullWidth(buttonWidth);
share->setFullWidth(buttonWidth);
copy->moveToLeft(0, 0, width);
share->moveToRight(0, 0, width);
}, wrap->lifetime());
wrap->showChildren();
}
::Settings::AddSkip(content, st::boostsLinkFieldPadding.bottom());
}
@ -190,7 +220,8 @@ void FillGetBoostsButton(
not_null<Ui::VerticalLayout*> content,
not_null<Controller*> controller,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer) {
not_null<PeerData*> peer,
Fn<void()> reloadOnDone) {
if (!Api::PremiumGiftCodeOptions(peer).giveawayGiftsPurchaseAvailable()) {
return;
}
@ -203,7 +234,12 @@ void FillGetBoostsButton(
tr::lng_boosts_get_boosts(),
st));
button->setClickedCallback([=] {
show->showBox(Box(CreateGiveawayBox, controller, peer));
show->showBox(Box(
CreateGiveawayBox,
controller,
peer,
reloadOnDone,
std::nullopt));
});
Ui::CreateChild<Info::Profile::FloatingIcon>(
button,
@ -253,21 +289,27 @@ void InnerWidget::fill() {
const auto &status = _state;
const auto inner = this;
const auto reloadOnDone = crl::guard(this, [=] {
while (Ui::VerticalLayout::count()) {
delete Ui::VerticalLayout::widgetAt(0);
}
load();
});
{
auto dividerContent = object_ptr<Ui::VerticalLayout>(inner);
Ui::FillBoostLimit(
fakeShowed->events(),
rpl::single(status.overview.isBoosted),
dividerContent.data(),
Ui::BoostCounters{
rpl::single(Ui::BoostCounters{
.level = status.overview.level,
.boosts = status.overview.boostCount,
.thisLevelBoosts
= status.overview.currentLevelBoostCount,
.nextLevelBoosts
= status.overview.nextLevelBoostCount,
.mine = status.overview.isBoosted,
},
.mine = status.overview.mine,
}),
st::statisticsLimitsLinePadding);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
@ -281,6 +323,49 @@ void InnerWidget::fill() {
::Settings::AddDivider(inner);
::Settings::AddSkip(inner);
if (!status.prepaidGiveaway.empty()) {
const auto multiplier = Api::PremiumGiftCodeOptions(_peer)
.giveawayBoostsPerPremium();
::Settings::AddSkip(inner);
AddHeader(inner, tr::lng_boosts_prepaid_giveaway_title);
::Settings::AddSkip(inner);
for (const auto &g : status.prepaidGiveaway) {
using namespace Giveaway;
const auto button = inner->add(object_ptr<GiveawayTypeRow>(
inner,
GiveawayTypeRow::Type::Prepaid,
g.id,
tr::lng_boosts_prepaid_giveaway_quantity(
lt_count,
rpl::single(g.quantity) | tr::to_count()),
tr::lng_boosts_prepaid_giveaway_moths(
lt_count,
rpl::single(g.months) | tr::to_count()),
Info::Statistics::CreateBadge(
st::statisticsDetailsBottomCaptionStyle,
QString::number(g.quantity * multiplier),
st::boostsListBadgeHeight,
st::boostsListBadgeTextPadding,
st::premiumButtonBg2,
st::premiumButtonFg,
1.,
st::boostsListMiniIconPadding,
st::boostsListMiniIcon)));
button->setClickedCallback([=] {
_controller->uiShow()->showBox(Box(
CreateGiveawayBox,
_controller,
_peer,
reloadOnDone,
g));
});
}
::Settings::AddSkip(inner);
::Settings::AddDivider(inner);
::Settings::AddSkip(inner);
}
const auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0);
const auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0);
if (hasBoosts || hasGifts) {
@ -289,9 +374,19 @@ void InnerWidget::fill() {
ResolveGiftCode(_controller, boost.giftCodeLink.slug);
} else if (boost.userId) {
const auto user = _peer->owner().user(boost.userId);
crl::on_main(this, [=] {
_controller->showPeerInfo(user);
});
if (boost.isGift || boost.isGiveaway) {
const auto d = Api::GiftCode{
.from = _peer->id,
.to = user->id,
.date = TimeId(boost.date.toSecsSinceEpoch()),
.months = boost.expiresAfterMonths,
};
_show->showBox(Box(GiftCodePendingBox, _controller, d));
} else {
crl::on_main(this, [=] {
_controller->showPeerInfo(user);
});
}
} else if (!boost.isUnclaimed) {
_show->showToast(tr::lng_boosts_list_pending_about(tr::now));
}
@ -321,16 +416,39 @@ void InnerWidget::fill() {
header->setSubTitle({});
}
class Slider final : public Ui::SettingsSlider {
public:
using Ui::SettingsSlider::SettingsSlider;
void setNaturalWidth(int w) {
_naturalWidth = w;
}
int naturalWidth() const override {
return _naturalWidth;
}
private:
int _naturalWidth = 0;
};
const auto slider = inner->add(
object_ptr<Ui::SlideWrap<Ui::SettingsSlider>>(
object_ptr<Ui::SlideWrap<Slider>>(
inner,
object_ptr<Ui::SettingsSlider>(
inner,
st::defaultTabsSlider)));
object_ptr<Slider>(inner, st::defaultTabsSlider)),
st::boxRowPadding);
slider->toggle(!hasOneTab, anim::type::instant);
slider->entity()->addSection(boostsTabText);
slider->entity()->addSection(giftsTabText);
{
const auto &st = st::defaultTabsSlider;
slider->entity()->setNaturalWidth(0
+ st.labelStyle.font->width(boostsTabText)
+ st.labelStyle.font->width(giftsTabText)
+ rect::m::sum::h(st::boxRowPadding));
}
const auto boostsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
@ -339,10 +457,9 @@ void InnerWidget::fill() {
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
boostsWrap->toggle(hasOneTab ? true : hasBoosts, anim::type::instant);
giftsWrap->toggle(hasOneTab ? false : hasGifts, anim::type::instant);
slider->entity()->sectionActivated(
rpl::single(hasOneTab ? (hasGifts ? 1 : 0) : 0) | rpl::then(
slider->entity()->sectionActivated()
) | rpl::start_with_next([=](int index) {
boostsWrap->toggle(!index, anim::type::instant);
giftsWrap->toggle(index, anim::type::instant);
@ -366,6 +483,7 @@ void InnerWidget::fill() {
::Settings::AddDividerText(inner, tr::lng_boosts_list_subtext());
}
::Settings::AddSkip(inner);
::Settings::AddSkip(inner);
AddHeader(inner, tr::lng_boosts_link_title);
::Settings::AddSkip(inner, st::boostsLinkSkip);
@ -373,10 +491,10 @@ void InnerWidget::fill() {
::Settings::AddSkip(inner);
::Settings::AddDividerText(inner, tr::lng_boosts_link_subtext());
FillGetBoostsButton(inner, _controller, _show, _peer);
FillGetBoostsButton(inner, _controller, _show, _peer, reloadOnDone);
resizeToWidth(width());
crl::on_main([=]{ fakeShowed->fire({}); });
crl::on_main(this, [=]{ fakeShowed->fire({}); });
}
void InnerWidget::saveState(not_null<Memento*> memento) {

View file

@ -71,7 +71,7 @@ not_null<PeerData*> Widget::peer() const {
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return false;
return (memento->statisticsPeer() == peer());
}
rpl::producer<QString> Widget::title() {

View file

@ -281,16 +281,6 @@ void InnerWidget::peerListSetDescription(
description.destroy();
}
void InnerWidget::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
_show->showBox(std::move(content), options);
}
void InnerWidget::peerListHideLayer() {
_show->hideLayer();
}
std::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() {
return _show;
}

View file

@ -64,10 +64,6 @@ private:
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
object_ptr<ListWidget> setupList(

View file

@ -284,7 +284,8 @@ bool Controller::validateMementoPeer(
return memento->peer() == peer()
&& memento->migratedPeerId() == migratedPeerId()
&& memento->settingsSelf() == settingsSelf()
&& memento->storiesPeer() == storiesPeer();
&& memento->storiesPeer() == storiesPeer()
&& memento->statisticsPeer() == statisticsPeer();
}
void Controller::setSection(not_null<ContentMemento*> memento) {

View file

@ -51,10 +51,6 @@ public:
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
};
@ -92,14 +88,6 @@ void ListDelegate::peerListSetDescription(
description.destroy();
}
void ListDelegate::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
}
void ListDelegate::peerListHideLayer() {
}
std::shared_ptr<Main::SessionShow> ListDelegate::peerListUiShow() {
Unexpected("...ListDelegate::peerListUiShow");
}

View file

@ -459,16 +459,6 @@ void Members::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
void Members::peerListFinishSelectedRowsBunch() {
}
void Members::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
_show->showBox(std::move(content), options);
}
void Members::peerListHideLayer() {
_show->hideLayer();
}
std::shared_ptr<Main::SessionShow> Members::peerListUiShow() {
return _show;
}

View file

@ -78,10 +78,6 @@ private:
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
//void peerListAppendRow(

View file

@ -15,6 +15,7 @@ struct SavedState final {
Data::AnyStatistics stats;
base::flat_map<MsgId, QImage> recentPostPreviews;
Data::PublicForwardsSlice publicForwardsFirstSlice;
int recentPostsExpanded = 0;
};
} // namespace Info::Statistics

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_statistics.h"
#include "apiwrap.h"
#include "base/call_delayed.h"
#include "base/event_filter.h"
#include "data/data_peer.h"
#include "data/data_session.h"
@ -697,25 +698,53 @@ void InnerWidget::fillRecentPosts() {
}
};
auto foundLoaded = false;
for (const auto &recent : stats.recentMessageInteractions) {
const auto messageWrap = content->add(
object_ptr<Ui::VerticalLayout>(content));
const auto msgId = recent.messageId;
if (const auto item = _peer->owner().message(_peer, msgId)) {
addMessage(messageWrap, item, recent);
foundLoaded = true;
continue;
const auto buttonWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
object_ptr<Ui::SettingsButton>(
container,
tr::lng_stories_show_more())));
constexpr auto kPerPage = int(10);
const auto max = stats.recentMessageInteractions.size();
if (_state.recentPostsExpanded) {
_state.recentPostsExpanded = std::max(
_state.recentPostsExpanded - kPerPage,
0);
}
const auto showMore = [=] {
const auto from = _state.recentPostsExpanded;
_state.recentPostsExpanded = std::min(
int(max),
_state.recentPostsExpanded + kPerPage);
if (_state.recentPostsExpanded == max) {
buttonWrap->toggle(false, anim::type::instant);
}
const auto callback = crl::guard(content, [=] {
for (auto i = from; i < _state.recentPostsExpanded; i++) {
const auto &recent = stats.recentMessageInteractions[i];
const auto messageWrap = content->add(
object_ptr<Ui::VerticalLayout>(content));
const auto msgId = recent.messageId;
if (const auto item = _peer->owner().message(_peer, msgId)) {
addMessage(messageWrap, item, recent);
content->resizeToWidth(content->width());
continue;
}
});
_peer->session().api().requestMessageData(_peer, msgId, callback);
}
if (!foundLoaded) {
const auto callback = crl::guard(content, [=] {
if (const auto item = _peer->owner().message(_peer, msgId)) {
addMessage(messageWrap, item, recent);
content->resizeToWidth(content->width());
}
});
_peer->session().api().requestMessageData(_peer, msgId, callback);
}
container->resizeToWidth(container->width());
};
const auto delay = st::defaultRippleAnimation.hideDuration;
buttonWrap->entity()->setClickedCallback([=] {
base::call_delayed(delay, crl::guard(container, showMore));
});
showMore();
if (_messagePreviews.empty()) {
wrap->toggle(false, anim::type::instant);
}
}

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history_item.h"
#include "info/boosts/giveaway/boost_badge.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
@ -35,58 +36,6 @@ using BoostCallback = Fn<void(const Data::Boost &)>;
constexpr auto kColorIndexUnclaimed = int(3);
constexpr auto kColorIndexPending = int(4);
[[nodiscard]] QImage Badge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon) {
auto badgeText = Ui::Text::String(textStyle, text);
const auto badgeTextWidth = badgeText.maxWidth();
const auto badgex = 0;
const auto badgey = 0;
const auto badgeh = 0 + badgeHeight;
const auto badgew = badgeTextWidth
+ rect::m::sum::h(textPadding);
auto result = QImage(
QSize(badgew, badgeh) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = Painter(&result);
p.setPen(Qt::NoPen);
p.setBrush(bg);
const auto r = QRect(badgex, badgey, badgew, badgeh);
{
auto hq = PainterHighQualityEnabler(p);
auto o = ScopedPainterOpacity(p, bgOpacity);
p.drawRoundedRect(r, badgeh / 2, badgeh / 2);
}
p.setPen(fg);
p.setBrush(Qt::NoBrush);
badgeText.drawLeftElided(
p,
r.x() + textPadding.left(),
badgey + textPadding.top(),
badgew,
badgew * 2);
icon.paint(
p,
QPoint(r.x() + iconPadding.left(), r.y() + iconPadding.top()),
badgew * 2);
}
return result;
}
void AddArrow(not_null<Ui::RpWidget*> parent) {
const auto arrow = Ui::CreateChild<Ui::RpWidget>(parent.get());
arrow->paintRequest(
@ -442,19 +391,16 @@ BoostRow::BoostRow(const Data::Boost &boost)
void BoostRow::init() {
invalidateBadges();
constexpr auto kMonthsDivider = int(30 * 86400);
const auto months = (_boost.expiresAt - _boost.date.toSecsSinceEpoch())
/ kMonthsDivider;
auto status = !PeerListRow::special()
? tr::lng_boosts_list_status(
tr::now,
lt_date,
langDateTime(_boost.date))
: tr::lng_months_tiny(tr::now, lt_count, months)
langDayOfMonth(_boost.expiresAt.date()))
: tr::lng_months_tiny(tr::now, lt_count, _boost.expiresAfterMonths)
+ ' '
+ QChar(0x2022)
+ ' '
+ langDateTime(_boost.date);
+ langDayOfMonth(_boost.date.date());
PeerListRow::setCustomStatus(std::move(status));
}
@ -476,12 +422,17 @@ PaintRoundImageCallback BoostRow::generatePaintUserpicCallback(bool force) {
}
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
_userpic.paintCircle(p, x, y, outerWidth, size);
(_boost.isUnclaimed
? st::boostsListUnclaimedIcon
: st::boostsListUnknownIcon).paintInCenter(
p,
{ x, y, size, size });
};
}
void BoostRow::invalidateBadges() {
_badge = _boost.multiplier
? Badge(
? CreateBadge(
st::statisticsDetailsBottomCaptionStyle,
QString::number(_boost.multiplier),
st::boostsListBadgeHeight,
@ -501,7 +452,7 @@ void BoostRow::invalidateBadges() {
? st::boostsListGiveawayMiniIcon
: st::boostsListGiftMiniIcon;
_rightBadge = (_boost.isGift || _boost.isGiveaway)
? Badge(
? CreateBadge(
st::boostsListRightBadgeTextStyle,
_boost.isGiveaway
? tr::lng_gift_link_reason_giveaway(tr::now)
@ -745,7 +696,6 @@ void AddMembersList(
container,
tr::lng_stories_show_more())),
{ 0, -st::settingsButton.padding.top(), 0, 0 });
const auto button = wrap->entity();
const auto showMore = [=] {
state->limit = std::min(int(max), state->limit + kPerPage);
@ -755,7 +705,7 @@ void AddMembersList(
}
container->resizeToWidth(container->width());
};
button->setClickedCallback(showMore);
wrap->entity()->setClickedCallback(showMore);
showMore();
}

View file

@ -215,6 +215,7 @@ void MessagePreview::paintEvent(QPaintEvent *e) {
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = crl::now(),
.elisionHeight = st::statisticsDetailsPopupHeaderStyle.font->height,
.elisionLines = 1,
});
_views.draw(p, {
.position = { width() - _viewsWidth, topTextTop },

View file

@ -130,6 +130,39 @@ not_null<Main::Session*> SessionFromId(const InvoiceId &id) {
return &giveaway.boostPeer->session();
}
MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL(
const InvoicePremiumGiftCode &invoice) {
const auto &giveaway = v::get<InvoicePremiumGiftCodeGiveaway>(
invoice.purpose);
using Flag = MTPDinputStorePaymentPremiumGiveaway::Flag;
return MTP_inputStorePaymentPremiumGiveaway(
MTP_flags(Flag()
| (giveaway.onlyNewSubscribers
? Flag::f_only_new_subscribers
: Flag())
| (giveaway.additionalChannels.empty()
? Flag()
: Flag::f_additional_peers)
| (giveaway.countries.empty()
? Flag()
: Flag::f_countries_iso2)),
giveaway.boostPeer->input,
MTP_vector_from_range(ranges::views::all(
giveaway.additionalChannels
) | ranges::views::transform([](not_null<ChannelData*> c) {
return MTPInputPeer(c->input);
})),
MTP_vector_from_range(ranges::views::all(
giveaway.countries
) | ranges::views::transform([](QString value) {
return MTP_string(value);
})),
MTP_long(invoice.randomId),
MTP_int(giveaway.untilDate),
MTP_string(invoice.currency),
MTP_long(invoice.amount));
}
Form::Form(InvoiceId id, bool receipt)
: _id(id)
, _session(SessionFromId(id))
@ -305,36 +338,8 @@ MTPInputInvoice Form::inputInvoice() const {
MTP_long(giftCode.amount)),
option);
} else {
const auto &giveaway = v::get<InvoicePremiumGiftCodeGiveaway>(
giftCode.purpose);
using Flag = MTPDinputStorePaymentPremiumGiveaway::Flag;
return MTP_inputInvoicePremiumGiftCode(
MTP_inputStorePaymentPremiumGiveaway(
MTP_flags(Flag()
| (giveaway.onlyNewSubscribers
? Flag::f_only_new_subscribers
: Flag())
| (giveaway.additionalChannels.empty()
? Flag()
: Flag::f_additional_peers)
| (giveaway.countries.empty()
? Flag()
: Flag::f_countries_iso2)),
giveaway.boostPeer->input,
MTP_vector_from_range(ranges::views::all(
giveaway.additionalChannels
) | ranges::views::transform([](not_null<ChannelData*> c) {
return MTPInputPeer(c->input);
})),
MTP_vector_from_range(ranges::views::all(
giveaway.countries
) | ranges::views::transform([](QString value) {
return MTP_string(value);
})),
MTP_long(giftCode.randomId),
MTP_int(giveaway.untilDate),
MTP_string(giftCode.currency),
MTP_long(giftCode.amount)),
InvoicePremiumGiftCodeGiveawayToTL(giftCode),
option);
}
}

View file

@ -219,6 +219,9 @@ struct InvoiceId {
[[nodiscard]] not_null<Main::Session*> SessionFromId(const InvoiceId &id);
[[nodiscard]] MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL(
const InvoicePremiumGiftCode &invoice);
class Form final : public base::has_weak_ptr {
public:
Form(InvoiceId id, bool receipt);

View file

@ -43,7 +43,7 @@ constexpr auto kTooltipDelay = crl::time(10000);
QOperatingSystemVersion::Windows,
10,
0,
17763);
18282);
static const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);
if (!kSupported) {
return std::nullopt;

View file

@ -527,7 +527,7 @@ TopBarUser::TopBarUser(
, _content(this)
, _title(_content, st::settingsPremiumUserTitle)
, _about(_content, st::userPremiumCover.about)
, _ministars(_content)
, _ministars(_content, true)
, _smallTop({
.widget = object_ptr<Ui::RpWidget>(this),
.text = Ui::Text::String(

View file

@ -159,9 +159,11 @@ boostsListBadgeHeight: 16px;
boostsListRightBadgeTextStyle: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
boostsListRightBadgeTextPadding: margins(16px, 1px, 6px, 0px);
boostsListRightBadgePadding: margins(4px, 5px, 8px, 0px);
boostsListRightBadgeTextPadding: margins(22px, 1px, 8px, 0px);
boostsListRightBadgePadding: margins(4px, 5px, 12px, 0px);
boostsListRightBadgeHeight: 20px;
boostsListGiftMiniIconPadding: margins(1px, 2px, 0px, 0px);
boostsListGiftMiniIcon: icon{{ "boosts/boost_mini2", historyPeer8UserpicBg2 }};
boostsListGiveawayMiniIcon: icon{{ "boosts/boost_mini2", historyPeer4UserpicBg2 }};
boostsListGiftMiniIconPadding: margins(4px, 2px, 0px, 0px);
boostsListGiftMiniIcon: icon{{ "boosts/mini_gift", historyPeer8UserpicBg2 }};
boostsListGiveawayMiniIcon: icon{{ "boosts/mini_giveaway", historyPeer4UserpicBg2 }};
boostsListUnclaimedIcon: icon{{ "boosts/boost_unknown", premiumButtonFg }};
boostsListUnknownIcon: icon{{ "boosts/boost_unclaimed", premiumButtonFg }};

View file

@ -8,11 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/boost_box.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.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 "ui/painter.h"
#include "styles/style_giveaway.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
@ -20,6 +22,94 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
[[nodiscard]] BoostCounters AdjustByReached(BoostCounters data) {
const auto exact = (data.boosts == data.thisLevelBoosts);
const auto reached = !data.nextLevelBoosts || (exact && data.mine > 0);
if (reached) {
if (data.nextLevelBoosts) {
--data.level;
}
data.boosts = data.nextLevelBoosts = std::max({
data.boosts,
data.thisLevelBoosts,
1
});
data.thisLevelBoosts = 0;
} else {
data.boosts = std::max(data.thisLevelBoosts, data.boosts);
data.nextLevelBoosts = std::max(
data.nextLevelBoosts,
data.boosts + 1);
}
return data;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title,
rpl::producer<QString> repeated) {
auto result = object_ptr<Ui::RpWidget>(box);
struct State {
not_null<Ui::FlatLabel*> title;
not_null<Ui::FlatLabel*> repeated;
};
const auto notEmpty = [](const QString &text) {
return !text.isEmpty();
};
const auto state = box->lifetime().make_state<State>(State{
.title = Ui::CreateChild<Ui::FlatLabel>(
result.data(),
rpl::duplicate(title),
st::boostTitle),
.repeated = Ui::CreateChild<Ui::FlatLabel>(
result.data(),
rpl::duplicate(repeated) | rpl::filter(notEmpty),
st::boostTitleBadge),
});
state->title->show();
state->repeated->showOn(std::move(repeated) | rpl::map(notEmpty));
result->resize(result->width(), st::boostTitle.style.font->height);
rpl::combine(
result->widthValue(),
rpl::duplicate(title),
state->repeated->shownValue(),
state->repeated->widthValue()
) | rpl::start_with_next([=](int outer, auto&&, bool shown, int badge) {
const auto repeated = shown ? badge : 0;
const auto skip = st::boostTitleBadgeSkip;
const auto available = outer - repeated - skip;
const auto use = std::min(state->title->textMaxWidth(), available);
state->title->resizeToWidth(use);
const auto left = (outer - use - skip - repeated) / 2;
state->title->moveToLeft(left, 0);
const auto mleft = st::boostTitleBadge.margin.left();
const auto mtop = st::boostTitleBadge.margin.top();
state->repeated->moveToLeft(left + use + skip + mleft, mtop);
}, result->lifetime());
const auto badge = state->repeated;
badge->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(badge);
auto hq = PainterHighQualityEnabler(p);
const auto radius = std::min(badge->width(), badge->height()) / 2;
p.setPen(Qt::NoPen);
auto brush = QLinearGradient(
QPointF(badge->width(), badge->height()),
QPointF());
brush.setStops(Ui::Premium::ButtonGradientStops());
p.setBrush(brush);
p.drawRoundedRect(badge->rect(), radius, radius);
}, badge->lifetime());
return result;
}
} // namespace
void StartFireworks(not_null<QWidget*> parent) {
const auto result = Ui::CreateChild<RpWidget>(parent.get());
@ -42,62 +132,69 @@ void StartFireworks(not_null<QWidget*> parent) {
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(bool)>)> boost) {
Fn<void(Fn<void(BoostCounters)>)> boost) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
const auto full = !data.boost.nextLevelBoosts;
//AssertIsDebug();
//data.boost = {
// .level = 2,
// .boosts = 3,
// .thisLevelBoosts = 2,
// .nextLevelBoosts = 5,
// .mine = 2,
//};
struct State {
rpl::variable<bool> you = false;
rpl::variable<BoostCounters> data;
rpl::variable<bool> full;
bool submitted = false;
};
const auto state = box->lifetime().make_state<State>(State{
.you = data.boost.mine,
});
const auto state = box->lifetime().make_state<State>();
state->data = std::move(data.boost);
FillBoostLimit(
BoxShowFinishes(box),
state->you.value(),
box->verticalLayout(),
data.boost,
state->data.value(),
st::boxRowPadding);
{
const auto &d = data.boost;
if (!d.nextLevelBoosts
|| ((d.thisLevelBoosts == d.boosts) && d.mine)) {
--data.boost.level;
}
}
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
const auto name = data.name;
auto title = state->you.value() | rpl::map([=](bool your) {
return your
auto title = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (counters.mine > 0)
? tr::lng_boost_channel_you_title(
lt_channel,
rpl::single(data.name))
: full
rpl::single(name))
: !counters.nextLevelBoosts
? tr::lng_boost_channel_title_max()
: !data.boost.level
: !counters.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)
auto repeated = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (counters.mine > 1) ? u"x%1"_q.arg(counters.mine) : u""_q;
});
auto text = state->data.value(
) | rpl::map([=](BoostCounters counters) {
const auto bold = Ui::Text::Bold(name);
const auto now = counters.boosts;
const auto full = !counters.nextLevelBoosts;
const auto left = (counters.nextLevelBoosts > now)
? (counters.nextLevelBoosts - now)
: 0;
auto post = tr::lng_boost_channel_post_stories(
lt_count,
rpl::single(float64(data.boost.level + 1)),
rpl::single(float64(counters.level + (left ? 1 : 0))),
Ui::Text::RichLangValue);
return (your || full)
? ((!full && left > 0)
? (!data.boost.level
return (counters.mine || full)
? (left
? (!counters.level
? tr::lng_boost_channel_you_first(
lt_count,
rpl::single(float64(left)),
@ -108,16 +205,16 @@ void BoostBox(
lt_post,
std::move(post),
Ui::Text::RichLangValue))
: (!data.boost.level
: (!counters.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)),
rpl::single(float64(counters.level)),
lt_post,
std::move(post),
Ui::Text::RichLangValue)))
: !data.boost.level
: !counters.level
? tr::lng_boost_channel_needs_first(
lt_count,
rpl::single(float64(left)),
@ -133,12 +230,11 @@ void BoostBox(
std::move(post),
Ui::Text::RichLangValue);
}) | rpl::flatten_latest();
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(title),
st::boostTitle),
MakeTitle(box, std::move(title), std::move(repeated)),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
@ -147,28 +243,85 @@ void BoostBox(
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
auto submit = full
? (tr::lng_box_ok() | rpl::type_erased())
: state->you.value(
) | rpl::map([](bool mine) {
return mine ? tr::lng_box_ok() : tr::lng_boost_channel_button();
}) | rpl::flatten_latest();
const auto allowMulti = data.allowMulti;
auto submit = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (!counters.nextLevelBoosts || (counters.mine && !allowMulti))
? tr::lng_box_ok()
: (counters.mine > 0)
? tr::lng_boost_again_button()
: tr::lng_boost_channel_button();
}) | rpl::flatten_latest();
const auto button = box->addButton(rpl::duplicate(submit), [=] {
if (state->submitted) {
return;
} else if (!full && !state->you.current()) {
} else if (state->data.current().nextLevelBoosts > 0
&& (allowMulti || !state->data.current().mine)) {
state->submitted = true;
boost(crl::guard(box, [=](bool success) {
const auto was = state->data.current().mine;
//AssertIsDebug();
//state->submitted = false;
//if (state->data.current().level == 5
// && state->data.current().boosts == 11) {
// state->data = BoostCounters{
// .level = 5,
// .boosts = 14,
// .thisLevelBoosts = 9,
// .nextLevelBoosts = 15,
// .mine = 14,
// };
//} else if (state->data.current().level == 5) {
// state->data = BoostCounters{
// .level = 7,
// .boosts = 16,
// .thisLevelBoosts = 15,
// .nextLevelBoosts = 19,
// .mine = 16,
// };
//} else if (state->data.current().level == 4) {
// state->data = BoostCounters{
// .level = 5,
// .boosts = 11,
// .thisLevelBoosts = 9,
// .nextLevelBoosts = 15,
// .mine = 9,
// };
//} else if (state->data.current().level == 3) {
// state->data = BoostCounters{
// .level = 4,
// .boosts = 7,
// .thisLevelBoosts = 7,
// .nextLevelBoosts = 9,
// .mine = 5,
// };
//} else {
// state->data = BoostCounters{
// .level = 3,
// .boosts = 5,
// .thisLevelBoosts = 5,
// .nextLevelBoosts = 7,
// .mine = 3,
// };
//}
//return;
boost(crl::guard(box, [=](BoostCounters result) {
state->submitted = false;
if (success) {
StartFireworks(box->parentWidget());
state->you = true;
if (result.thisLevelBoosts || result.nextLevelBoosts) {
if (result.mine > was) {
StartFireworks(box->parentWidget());
}
state->data = result;
}
}));
} else {
box->closeBox();
}
});
rpl::combine(
std::move(submit),
box->widthValue()
@ -261,6 +414,49 @@ object_ptr<Ui::RpWidget> MakeLinkLabel(
return result;
}
void BoostBoxAlready(not_null<GenericBox*> box) {
ConfirmBox(box, {
.text = tr::lng_boost_error_already_text(Text::RichLangValue),
.title = tr::lng_boost_error_already_title(),
.inform = true,
});
}
void GiftForBoostsBox(
not_null<GenericBox*> box,
QString channel,
int receive,
bool again) {
ConfirmBox(box, {
.text = (again
? tr::lng_boost_need_more_again
: tr::lng_boost_need_more_text)(
lt_count,
rpl::single(receive) | tr::to_count(),
lt_channel,
rpl::single(TextWithEntities{ channel }),
Text::RichLangValue),
.title = tr::lng_boost_need_more(),
.inform = true,
});
}
void GiftedNoBoostsBox(not_null<GenericBox*> box) {
InformBox(box, {
.text = tr::lng_boost_error_gifted_text(Text::RichLangValue),
.title = tr::lng_boost_error_gifted_title(),
});
}
void PremiumForBoostsBox(not_null<GenericBox*> box, Fn<void()> buyPremium) {
ConfirmBox(box, {
.text = tr::lng_boost_error_premium_text(Text::RichLangValue),
.confirmed = buyPremium,
.confirmText = tr::lng_boost_error_premium_yes(),
.title = tr::lng_boost_error_premium_title(),
});
}
void AskBoostBox(
not_null<GenericBox*> box,
AskBoostBoxData data,
@ -269,19 +465,10 @@ void AskBoostBox(
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>(State{
.you = data.boost.mine,
});
FillBoostLimit(
BoxShowFinishes(box),
state->you.value(),
box->verticalLayout(),
data.boost,
rpl::single(data.boost),
st::boxRowPadding);
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
@ -338,56 +525,22 @@ void AskBoostBox(
void FillBoostLimit(
rpl::producer<> showFinished,
rpl::producer<bool> you,
not_null<VerticalLayout*> container,
BoostCounters data,
rpl::producer<BoostCounters> data,
style::margins limitLinePadding) {
const auto full = !data.nextLevelBoosts;
if (data.mine && data.boosts > 0) {
--data.boosts;
}
if (full) {
data.nextLevelBoosts = data.boosts
+ (data.mine ? 1 : 0);
data.thisLevelBoosts = 0;
if (data.level > 0) {
--data.level;
}
} else if (data.mine
&& data.level > 0
&& data.boosts < data.thisLevelBoosts) {
--data.level;
data.nextLevelBoosts = data.thisLevelBoosts;
data.thisLevelBoosts = 0;
}
const auto addSkip = [&](int skip) {
container->add(object_ptr<Ui::FixedHeightWidget>(container, skip));
};
addSkip(st::boostSkipTop);
const auto levelWidth = [&](int add) {
return st::normalFont->width(
tr::lng_boost_level(tr::now, lt_count, data.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.boosts,
data.thisLevelBoosts);
const auto max = std::max({
data.boosts,
data.nextLevelBoosts,
1,
});
Assert(boosts >= min && boosts <= max);
const auto ratio = [=](BoostCounters counters) {
const auto min = counters.thisLevelBoosts;
const auto max = counters.nextLevelBoosts;
Assert(counters.boosts >= min && counters.boosts <= max);
const auto count = (max - min);
const auto index = (boosts - min);
const auto index = (counters.boosts - min);
if (!index) {
return 0.;
} else if (index == count) {
@ -399,26 +552,33 @@ void FillBoostLimit(
- 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,
counters.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;
};
const auto min = std::min(data.boosts, data.thisLevelBoosts);
const auto now = data.boosts;
const auto max = (data.nextLevelBoosts > min)
? (data.nextLevelBoosts)
: (data.boosts > 0)
? data.boosts
: 1;
auto bubbleRowState = (
std::move(you)
) | rpl::map([=](bool mine) {
const auto index = mine ? (now + 1) : now;
auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);
auto bubbleRowState = rpl::duplicate(
adjustedData
) | rpl::combine_previous(
BoostCounters()
) | rpl::map([=](BoostCounters previous, BoostCounters counters) {
return Premium::BubbleRowState{
.counter = index,
.ratio = ratio(index),
.counter = counters.boosts,
.ratio = ratio(counters),
.animateFromZero = (counters.level != previous.level),
.dynamic = true,
};
});
@ -427,7 +587,6 @@ void FillBoostLimit(
st::boostBubble,
std::move(showFinished),
rpl::duplicate(bubbleRowState),
max,
true,
nullptr,
&st::premiumIconBoost,
@ -437,20 +596,33 @@ void FillBoostLimit(
const auto level = [](int level) {
return tr::lng_boost_level(tr::now, lt_count, level);
};
auto ratioValue = std::move(
auto limitState = std::move(
bubbleRowState
) | rpl::map([](const Premium::BubbleRowState &state) {
return state.ratio;
return Premium::LimitRowState{
.ratio = state.ratio,
.animateFromZero = state.animateFromZero,
.dynamic = state.dynamic
};
});
auto left = rpl::duplicate(
adjustedData
) | rpl::map([=](BoostCounters counters) {
return level(counters.level);
});
auto right = rpl::duplicate(
adjustedData
) | rpl::map([=](BoostCounters counters) {
return level(counters.level + 1);
});
Premium::AddLimitRow(
container,
st::boostLimits,
Premium::LimitRowLabels{
.leftLabel = level(data.level),
.rightLabel = level(data.level + 1),
.dynamic = true,
.leftLabel = std::move(left),
.rightLabel = std::move(right),
},
std::move(ratioValue),
std::move(limitState),
limitLinePadding);
}

View file

@ -23,18 +23,32 @@ struct BoostCounters {
int boosts = 0;
int thisLevelBoosts = 0;
int nextLevelBoosts = 0; // Zero means no next level is available.
bool mine = false;
int mine = 0;
friend inline constexpr bool operator==(
BoostCounters,
BoostCounters) = default;
};
struct BoostBoxData {
QString name;
BoostCounters boost;
bool allowMulti = false;
};
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(bool)>)> boost);
Fn<void(Fn<void(BoostCounters)>)> boost);
void BoostBoxAlready(not_null<GenericBox*> box);
void GiftForBoostsBox(
not_null<GenericBox*> box,
QString channel,
int receive,
bool again);
void GiftedNoBoostsBox(not_null<GenericBox*> box);
void PremiumForBoostsBox(not_null<GenericBox*> box, Fn<void()> buyPremium);
struct AskBoostBoxData {
QString link;
@ -57,9 +71,8 @@ void AskBoostBox(
void FillBoostLimit(
rpl::producer<> showFinished,
rpl::producer<bool> you,
not_null<VerticalLayout*> container,
BoostCounters data,
rpl::producer<BoostCounters> data,
style::margins limitLinePadding);
} // namespace Ui

View file

@ -100,11 +100,4 @@ object_ptr<Ui::GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args) {
return Box(ConfirmBox, std::move(args));
}
object_ptr<Ui::GenericBox> MakeInformBox(v::text::data text) {
return MakeConfirmBox({
.text = std::move(text),
.inform = true,
});
}
} // namespace Ui

View file

@ -40,10 +40,24 @@ struct ConfirmBoxArgs {
bool strictCancel = false;
};
void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args);
void ConfirmBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args);
[[nodiscard]] object_ptr<Ui::GenericBox> MakeConfirmBox(
ConfirmBoxArgs &&args);
[[nodiscard]] object_ptr<Ui::GenericBox> MakeInformBox(v::text::data text);
inline void InformBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args) {
args.inform = true;
ConfirmBox(box, std::move(args));
}
[[nodiscard]] object_ptr<GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args);
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
ConfirmBoxArgs &&args) {
args.inform = true;
return MakeConfirmBox(std::move(args));
}
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
v::text::data text) {
return MakeInformBox({ .text = std::move(text) });
}
} // namespace Ui

View file

@ -640,6 +640,12 @@ historyPageButtonLine: 1px;
historyPageButtonHeight: 36px;
historyPageButtonPadding: margins(13px, 8px, 13px, 8px);
historyPageEnlarge: icon{{ "chat/link_photo_enlarge", historyFileThumbRadialFg }};
historyPageEnlargeSelected: icon{{ "chat/link_photo_enlarge", historyFileThumbRadialFgSelected }};
historyPageEnlargeSize: 36px;
historyPageEnlargeSkip: 4px;
historyPageEnlargeRadius: 8px;
historyCommentsButtonHeight: 40px;
historyCommentsSkipLeft: 9px;
historyCommentsSkipText: 10px;

View file

@ -505,6 +505,10 @@ ChatStyle::ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices) {
&MessageImageStyle::historyVideoMessageMute,
st::historyVideoMessageMute,
st::historyVideoMessageMuteSelected);
make(
&MessageImageStyle::historyPageEnlarge,
st::historyPageEnlarge,
st::historyPageEnlargeSelected);
updateDarkValue();
}

View file

@ -117,6 +117,7 @@ struct MessageImageStyle {
style::icon historyVideoDownload = { Qt::Uninitialized };
style::icon historyVideoCancel = { Qt::Uninitialized };
style::icon historyVideoMessageMute = { Qt::Uninitialized };
style::icon historyPageEnlarge = { Qt::Uninitialized };
};
struct ReactionPaintInfo {

View file

@ -1108,57 +1108,4 @@ not_null<Ui::UserpicButton*> CreateUploadSubButton(
return upload;
}
object_ptr<Ui::RpWidget> CreateBoostReplaceUserpics(
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> from,
not_null<PeerData*> to) {
const auto full = st::boostReplaceUserpic.size.height()
+ st::boostReplaceIconAdd.y()
+ st::lineWidth;
auto result = object_ptr<FixedHeightWidget>(parent, full);
const auto raw = result.data();
const auto &st = st::boostReplaceUserpic;
const auto left = CreateChild<UserpicButton>(raw, from, st);
const auto right = CreateChild<UserpicButton>(raw, to, st);
const auto overlay = CreateChild<RpWidget>(raw);
raw->widthValue(
) | rpl::start_with_next([=](int width) {
const auto skip = st::boostReplaceUserpicsSkip;
const auto total = left->width() + skip + right->width();
left->moveToLeft((width - total) / 2, 0);
right->moveToLeft(left->x() + left->width() + skip, 0);
overlay->setGeometry(QRect(0, 0, width, raw->height()));
}, raw->lifetime());
overlay->paintRequest(
) | rpl::start_with_next([=] {
const auto outerw = overlay->width();
const auto add = st::boostReplaceIconAdd;
const auto skip = st::boostReplaceIconSkip;
const auto w = st::boostReplaceIcon.width() + 2 * skip;
const auto h = st::boostReplaceIcon.height() + 2 * skip;
const auto x = left->x() + left->width() - w + add.x();
const auto y = left->y() + left->height() - h + add.y();
const auto stroke = st::boostReplaceIconOutline;
const auto half = stroke / 2.;
auto p = QPainter(overlay);
auto hq = PainterHighQualityEnabler(p);
auto pen = st::windowBg->p;
pen.setWidthF(stroke);
p.setPen(pen);
auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y));
brush.setStops(Premium::ButtonGradientStops());
p.setBrush(brush);
p.drawEllipse(x - half, y - half, w + stroke, h + stroke);
st::boostReplaceIcon.paint(p, x + skip, y + skip, outerw);
const auto size = st::boostReplaceArrow.size();
st::boostReplaceArrow.paint(
p,
(outerw - size.width()) / 2,
(left->height() - size.height()) / 2,
outerw);
}, overlay->lifetime());
return result;
}
} // namespace Ui

View file

@ -204,9 +204,4 @@ private:
not_null<UserData*> contact,
not_null<Window::SessionController*> controller);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateBoostReplaceUserpics(
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> from,
not_null<PeerData*> to);
} // namespace Ui

Some files were not shown because too many files have changed in this diff Show more