diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cf797b63491e48..ea0aff34e903a6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -225,6 +225,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 @@ -1865,6 +1867,7 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED) install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "telegram.png") install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "telegram.png") install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "telegram.png") + install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "telegram-symbolic.svg") install(FILES "../lib/xdg/org.telegram.desktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.service" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") diff --git a/Telegram/Resources/icons/boosts/boost_unclaimed.png b/Telegram/Resources/icons/boosts/boost_unclaimed.png new file mode 100644 index 00000000000000..fb093176f775d8 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_unclaimed.png differ diff --git a/Telegram/Resources/icons/boosts/boost_unclaimed@2x.png b/Telegram/Resources/icons/boosts/boost_unclaimed@2x.png new file mode 100644 index 00000000000000..30d5bd6bc708a0 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_unclaimed@2x.png differ diff --git a/Telegram/Resources/icons/boosts/boost_unclaimed@3x.png b/Telegram/Resources/icons/boosts/boost_unclaimed@3x.png new file mode 100644 index 00000000000000..f63cca1c90887b Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_unclaimed@3x.png differ diff --git a/Telegram/Resources/icons/boosts/boost_unknown.png b/Telegram/Resources/icons/boosts/boost_unknown.png new file mode 100644 index 00000000000000..78f64285205068 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_unknown.png differ diff --git a/Telegram/Resources/icons/boosts/boost_unknown@2x.png b/Telegram/Resources/icons/boosts/boost_unknown@2x.png new file mode 100644 index 00000000000000..ad22dc971e1d33 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_unknown@2x.png differ diff --git a/Telegram/Resources/icons/boosts/boost_unknown@3x.png b/Telegram/Resources/icons/boosts/boost_unknown@3x.png new file mode 100644 index 00000000000000..bbf4532a27ff43 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_unknown@3x.png differ diff --git a/Telegram/Resources/icons/boosts/mini_gift.png b/Telegram/Resources/icons/boosts/mini_gift.png new file mode 100644 index 00000000000000..1008e51240b088 Binary files /dev/null and b/Telegram/Resources/icons/boosts/mini_gift.png differ diff --git a/Telegram/Resources/icons/boosts/mini_gift@2x.png b/Telegram/Resources/icons/boosts/mini_gift@2x.png new file mode 100644 index 00000000000000..462d3876453503 Binary files /dev/null and b/Telegram/Resources/icons/boosts/mini_gift@2x.png differ diff --git a/Telegram/Resources/icons/boosts/mini_gift@3x.png b/Telegram/Resources/icons/boosts/mini_gift@3x.png new file mode 100644 index 00000000000000..8858b4fff5eadb Binary files /dev/null and b/Telegram/Resources/icons/boosts/mini_gift@3x.png differ diff --git a/Telegram/Resources/icons/boosts/mini_giveaway.png b/Telegram/Resources/icons/boosts/mini_giveaway.png new file mode 100644 index 00000000000000..79ffc0f1d7c46c Binary files /dev/null and b/Telegram/Resources/icons/boosts/mini_giveaway.png differ diff --git a/Telegram/Resources/icons/boosts/mini_giveaway@2x.png b/Telegram/Resources/icons/boosts/mini_giveaway@2x.png new file mode 100644 index 00000000000000..bb5e94a6567162 Binary files /dev/null and b/Telegram/Resources/icons/boosts/mini_giveaway@2x.png differ diff --git a/Telegram/Resources/icons/boosts/mini_giveaway@3x.png b/Telegram/Resources/icons/boosts/mini_giveaway@3x.png new file mode 100644 index 00000000000000..c9f85345256755 Binary files /dev/null and b/Telegram/Resources/icons/boosts/mini_giveaway@3x.png differ diff --git a/Telegram/Resources/icons/chat/link_photo_enlarge.png b/Telegram/Resources/icons/chat/link_photo_enlarge.png new file mode 100644 index 00000000000000..c4ba35289e0e7b Binary files /dev/null and b/Telegram/Resources/icons/chat/link_photo_enlarge.png differ diff --git a/Telegram/Resources/icons/chat/link_photo_enlarge@2x.png b/Telegram/Resources/icons/chat/link_photo_enlarge@2x.png new file mode 100644 index 00000000000000..02e1328ed30d4f Binary files /dev/null and b/Telegram/Resources/icons/chat/link_photo_enlarge@2x.png differ diff --git a/Telegram/Resources/icons/chat/link_photo_enlarge@3x.png b/Telegram/Resources/icons/chat/link_photo_enlarge@3x.png new file mode 100644 index 00000000000000..3dbc8aad427ac0 Binary files /dev/null and b/Telegram/Resources/icons/chat/link_photo_enlarge@3x.png differ diff --git a/Telegram/Resources/icons/menu/forward.png b/Telegram/Resources/icons/menu/forward.png index abdda604583f11..4a50099f4a03d9 100644 Binary files a/Telegram/Resources/icons/menu/forward.png and b/Telegram/Resources/icons/menu/forward.png differ diff --git a/Telegram/Resources/icons/menu/forward@2x.png b/Telegram/Resources/icons/menu/forward@2x.png index 5f51d642da365b..c27765246f6109 100644 Binary files a/Telegram/Resources/icons/menu/forward@2x.png and b/Telegram/Resources/icons/menu/forward@2x.png differ diff --git a/Telegram/Resources/icons/menu/forward@3x.png b/Telegram/Resources/icons/menu/forward@3x.png index d89d37bc6ff81a..557b032eff6eef 100644 Binary files a/Telegram/Resources/icons/menu/forward@3x.png and b/Telegram/Resources/icons/menu/forward@3x.png differ diff --git a/Telegram/Resources/icons/menu/reply.png b/Telegram/Resources/icons/menu/reply.png index cc12969ec27a76..a40619d2e9b16d 100644 Binary files a/Telegram/Resources/icons/menu/reply.png and b/Telegram/Resources/icons/menu/reply.png differ diff --git a/Telegram/Resources/icons/menu/reply@2x.png b/Telegram/Resources/icons/menu/reply@2x.png index e4fe939f8cc990..8ea749d737087a 100644 Binary files a/Telegram/Resources/icons/menu/reply@2x.png and b/Telegram/Resources/icons/menu/reply@2x.png differ diff --git a/Telegram/Resources/icons/menu/reply@3x.png b/Telegram/Resources/icons/menu/reply@3x.png index e5825a5a949ea6..04c83d47f9aa6b 100644 Binary files a/Telegram/Resources/icons/menu/reply@3x.png and b/Telegram/Resources/icons/menu/reply@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 41a86f9ca44ac9..55cd78154d983b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2065,6 +2065,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"; @@ -2086,6 +2087,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!"; @@ -2095,6 +2101,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."; @@ -2132,6 +2151,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."; @@ -2234,6 +2254,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."; @@ -4365,8 +4388,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"; @@ -4383,6 +4406,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..."; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 522be5d7bcc5dd..87d43e60345f0b 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.11.6.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 364b4bad8a56ce..e479291ed0b41d 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -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", "" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "4.11.5.0" + VALUE "FileVersion", "4.11.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.11.5.0" + VALUE "ProductVersion", "4.11.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index bad1463c09953f..711848544384f9 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -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", "" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "4.11.5.0" + VALUE "FileVersion", "4.11.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.11.5.0" + VALUE "ProductVersion", "4.11.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 629abc0d03bac5..58950f6f4b9102 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -390,18 +390,44 @@ rpl::producer PremiumGiftCodeOptions::request() { }; } +rpl::producer 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 &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(); - const auto token = Token{ - users, - _optionsForOnePerson.months[monthsIndex], - }; + const auto token = Token{ users, months }; const auto &store = _stores[token]; return Payments::InvoicePremiumGiftCode{ .randomId = randomId, diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index deb66df8064e1f..1199b48194a409 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -152,9 +152,13 @@ class PremiumGiftCodeOptions final { [[nodiscard]] rpl::producer request(); [[nodiscard]] Data::SubscriptionOptions options(int amount); [[nodiscard]] const std::vector &availablePresets() const; + [[nodiscard]] int monthsFromPreset(int monthsIndex); [[nodiscard]] Payments::InvoicePremiumGiftCode invoice( int users, - int monthsIndex); + int months); + [[nodiscard]] rpl::producer applyPrepaid( + const Payments::InvoicePremiumGiftCode &invoice, + uint64 prepaidId); [[nodiscard]] int giveawayBoostsPerPremium() const; [[nodiscard]] int giveawayCountriesMax() const; diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp index 3069843c179a1e..383d0973cfd9c5 100644 --- a/Telegram/SourceFiles/api/api_statistics.cpp +++ b/Telegram/SourceFiles/api/api_statistics.cpp @@ -518,8 +518,9 @@ rpl::producer 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 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 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(); 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(), }); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 61e937dfe7b11a..989a9867b331ef 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -362,7 +362,7 @@ void EditFilterChatsListController::rowClicked(not_null row) { delegate()->peerListSetRowChecked(row, !row->checked()); updateTitle(); } else { - delegate()->peerListShowBox(_limitBox(count)); + delegate()->peerListUiShow()->showBox(_limitBox(count)); } } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index d731ead51f0a3d..9dcf6fd804e0a6 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -575,19 +575,19 @@ void LinkController::addLinkBlock(not_null 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 &rows) { void LinksController::rowClicked(not_null row) { const auto link = static_cast(row.get())->data(); - delegate()->peerListShowBox( + delegate()->peerListUiShow()->showBox( ShowLinkBox(_window, _currentFilter(), link)); } @@ -881,19 +881,19 @@ base::unique_qptr 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( parent, diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 9a87a58fc1768d..1244964d2e8c31 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -7,9 +7,9 @@ For license and copyright information please follow this link: */ #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 @@ For license and copyright information please follow this link: #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(top); + const auto stars = box->lifetime().make_state(top, true); const auto userpic = Ui::CreateChild( top, @@ -358,6 +360,73 @@ void AddTableRow( st::giveawayGiftCodePeerMargin); } +void AddTable( + not_null container, + not_null controller, + const Api::GiftCode ¤t, + bool skipReason) { + auto table = container->add( + object_ptr( + 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( - 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 box, + not_null 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( + 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(linkLabel); + spoiler->lifetime().make_state([=] { + 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( + box, + tr::lng_gift_link_pending_footer(), + st::giveawayGiftCodeFooter), + st::giveawayGiftCodeFooterMargin); + + const auto close = Ui::CreateChild( + 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 controller, const QString &slug) { diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index a1062dead8f6ee..1891ce5ff43faf 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -50,6 +50,10 @@ void GiftCodeBox( not_null box, not_null controller, const QString &slug); +void GiftCodePendingBox( + not_null box, + not_null controller, + const Api::GiftCode &data); void ResolveGiftCode( not_null controller, const QString &slug); diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index dd165ac6111503..e320b434ff0b1c 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -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 { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index abd4f4b3253a2a..9f12a539e82f6a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -85,16 +85,6 @@ PeerListContentDelegateShow::PeerListContentDelegateShow( : _show(show) { } -void PeerListContentDelegateShow::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { - _show->showBox(std::move(content), options); -} - -void PeerListContentDelegateShow::peerListHideLayer() { - _show->hideLayer(); -} - auto PeerListContentDelegateShow::peerListUiShow() -> std::shared_ptr{ return _show; @@ -324,16 +314,6 @@ void PeerListBox::peerListSetSearchMode(PeerListSearchMode mode) { } } -void PeerListBox::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { - _show->showBox(std::move(content), options); -} - -void PeerListBox::peerListHideLayer() { - _show->hideLayer(); -} - std::shared_ptr 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( diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 0f557da68b431d..e99469b9eabb12 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -141,6 +141,9 @@ class PeerListRow { } 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 @@ class PeerListDelegate { virtual std::optional peerListLastRowMousePosition() = 0; virtual void peerListSortRows(Fn compare) = 0; virtual int peerListPartitionRows(Fn border) = 0; - virtual void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) = 0; - virtual void peerListHideLayer() = 0; virtual std::shared_ptr peerListUiShow() = 0; template @@ -1007,14 +1006,6 @@ class PeerListContentDelegateSimple : public PeerListContentDelegate { object_ptr description) override { description.destroy(); } - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override { - Unexpected("...DelegateSimple::peerListShowBox"); - } - void peerListHideLayer() override { - Unexpected("...DelegateSimple::peerListHideLayer"); - } std::shared_ptr peerListUiShow() override { Unexpected("...DelegateSimple::peerListUiShow"); } @@ -1025,10 +1016,6 @@ class PeerListContentDelegateShow : public PeerListContentDelegateSimple { public: explicit PeerListContentDelegateShow( std::shared_ptr show); - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; private: @@ -1064,10 +1051,6 @@ class PeerListBox bool peerListIsRowChecked(not_null row) override; int peerListSelectedRowsCount() override; void peerListScrollToTop() override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; void setAddedTopScrollSkip(int skip); diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index df1a24fba67559..bad9dbd89000b8 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -685,7 +685,7 @@ void ChooseRecipientBoxController::rowClicked(not_null 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); diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.cpp b/Telegram/SourceFiles/boxes/peer_lists_box.cpp index e445a21e2236fa..80e2a933408ff6 100644 --- a/Telegram/SourceFiles/boxes/peer_lists_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_lists_box.cpp @@ -372,16 +372,6 @@ void PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() { _box->_select->entity()->finishItemsBunch(); } -void PeerListsBox::Delegate::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { - _show->showBox(std::move(content), options); -} - -void PeerListsBox::Delegate::peerListHideLayer() { - _show->hideLayer(); -} - auto PeerListsBox::Delegate::peerListUiShow() -> std::shared_ptr { return _show; diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.h b/Telegram/SourceFiles/boxes/peer_lists_box.h index 94732b3e2440ec..20377548a4a470 100644 --- a/Telegram/SourceFiles/boxes/peer_lists_box.h +++ b/Telegram/SourceFiles/boxes/peer_lists_box.h @@ -54,10 +54,6 @@ class PeerListsBox : public Ui::BoxContent { _box->addSelectItem(row, anim::type::instant); } void peerListFinishSelectedRowsBunch() override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; private: diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index b40e1a0d299247..575522e42aea47 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -366,7 +366,7 @@ bool AddParticipantsBoxController::needsInviteLinkButton() { QPointer AddParticipantsBoxController::showBox( object_ptr 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 AddSpecialBoxController::showBox( object_ptr box) const { const auto weak = Ui::MakeWeak(box.data()); - delegate()->peerListShowBox(std::move(box)); + delegate()->peerListUiShow()->showBox(std::move(box)); return weak; } diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 767b0c91511ede..7e27128c417003 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -433,7 +433,7 @@ void ChoosePeerBoxController::rowClicked(not_null row) { if (const auto user = peer->asUser()) { done(); } else { - delegate()->peerListShowBox( + delegate()->peerListUiShow()->showBox( MakeConfirmBox(_bot, peer, _query, done)); } } diff --git a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp index 48bc383c9e797e..c3e4b3d3449d38 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -168,7 +168,7 @@ void Controller::choose(not_null 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 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), diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 425e21c759529f..1801cc1540ff06 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -1281,7 +1281,7 @@ void ParticipantsBoxController::rebuild() { QPointer ParticipantsBoxController::showBox( object_ptr box) const { const auto weak = Ui::MakeWeak(box.data()); - delegate()->peerListShowBox(std::move(box)); + delegate()->peerListUiShow()->showBox(std::move(box)); return weak; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 1eede85e306a73..1913675afb5f93 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #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(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 62ffe06467fb2f..f4d5adec424768 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -347,21 +347,24 @@ void Controller::addHeaderBlock(not_null 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 = [=] { @@ -1333,7 +1336,7 @@ object_ptr ShowInviteLinkBox( auto data = rpl::single(link) | rpl::then(std::move(updates)); auto initBox = [=, data = rpl::duplicate(data)]( - not_null box) { + not_null box) { rpl::duplicate( data ) | rpl::start_with_next([=](const LinkData &link) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 4d7254388fe102..7ed35cdf89635f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -537,7 +537,7 @@ void LinksController::appendSlice(const InviteLinksSlice &slice) { } void LinksController::rowClicked(not_null row) { - delegate()->peerListShowBox( + delegate()->peerListUiShow()->showBox( ShowInviteLinkBox(_peer, static_cast(row.get())->data())); } @@ -573,25 +573,27 @@ base::unique_qptr 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 row) { - delegate()->peerListShowBox( + delegate()->peerListUiShow()->showBox( Box(ManageInviteLinksBox, _peer, row->peer()->asUser(), 0, 0)); } diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp new file mode 100644 index 00000000000000..0129b85960c9ac --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -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 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 _empty; + Ui::PeerUserpicView _userpic; + crl::time _startPreciseTime = 0; + TimeId _startUnixtime = 0; + bool _waiting = false; + +}; + +class Controller final : public PeerListController { +public: + Controller(not_null to, std::vector from); + + [[nodiscard]] rpl::producer> selectedValue() const { + return _selected.value(); + } + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + bool trackSelectedList() override { + return false; + } + +private: + void updateWaitingState(); + + not_null _to; + std::vector _from; + rpl::variable> _selected; + rpl::variable>> _selectedPeers; + base::Timer _waitingTimer; + bool _hasWaitingRows = false; + +}; + +Row::Row( + not_null 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::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 to, + std::vector 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((QWidget*)nullptr); + above->add( + CreateBoostReplaceUserpics( + above.data(), + _selectedPeers.value(), + _to), + st::boxRowPadding + st::boostReplaceUserpicsPadding); + above->add( + object_ptr( + 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(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(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 row) { + const auto slot = static_cast(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 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 ReassignBoostSingleBox( + not_null to, + TakenBoostSlot from, + Fn slots, int sources)> reassign, + Fn cancel) { + const auto reassigned = std::make_shared(); + const auto slot = from.id; + const auto peer = to->owner().peer(from.peerId); + const auto confirmed = [=](Fn close) { + *reassigned = true; + reassign({ slot }, 1); + close(); + }; + + auto result = Box([=](not_null 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 channel, + const QVector &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 session) { + const auto key = u"boosts_per_sent_gift"_q; + return session->account().appConfig().get(key, 0); +} + +[[nodiscard]] int SourcesCount( + const std::vector &from, + const std::vector &slots) { + auto checked = base::flat_set(); + 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 ReassignBoostsBox( + not_null to, + std::vector from, + Fn slots, int sources)> reassign, + Fn 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(); + auto controller = std::make_unique(to, from); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + raw->selectedValue( + ) | rpl::start_with_next([=](std::vector 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(std::move(controller), std::move(initBox)); +} + +object_ptr CreateBoostReplaceUserpics( + not_null parent, + rpl::producer>> from, + not_null to) { + struct State { + std::vector> from; + std::vector> buttons; + QImage layer; + rpl::variable count = 0; + bool painting = false; + }; + const auto full = st::boostReplaceUserpic.size.height() + + st::boostReplaceIconAdd.y() + + st::lineWidth; + auto result = object_ptr(parent, full); + const auto raw = result.data(); + const auto &st = st::boostReplaceUserpic; + const auto right = CreateChild(raw, to, st); + const auto overlay = CreateChild(raw); + + const auto state = raw->lifetime().make_state(); + std::move( + from + ) | rpl::start_with_next([=]( + const std::vector> &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(raw, peer, st)); + const auto raw = state->buttons.back().get(); + base::install_event_filter(raw, [=](not_null 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; +} diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h new file mode 100644 index 00000000000000..f15cf0b1482000 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -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 free; + std::vector already; + std::vector other; +}; + +[[nodiscard]] ForChannelBoostSlots ParseForChannelBoostSlots( + not_null channel, + const QVector &boosts); + +[[nodiscard]] Ui::BoostCounters ParseBoostCounters( + const MTPpremium_BoostsStatus &status); + +[[nodiscard]] int BoostsForGift(not_null session); + +object_ptr ReassignBoostsBox( + not_null to, + std::vector from, + Fn slots, int sources)> reassign, + Fn cancel); + +[[nodiscard]] object_ptr CreateBoostReplaceUserpics( + not_null parent, + rpl::producer>> from, + not_null to); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 928915afd6220c..9ce74e022b40b4 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -115,10 +115,6 @@ class InactiveDelegate final : public PeerListContentDelegate { void peerListFinishSelectedRowsBunch() override; void peerListSetDescription( object_ptr description) override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; void peerListSetRowChecked( not_null row, @@ -183,14 +179,6 @@ void InactiveDelegate::peerListSetDescription( description.destroy(); } -void InactiveDelegate::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { -} - -void InactiveDelegate::peerListHideLayer() { -} - std::shared_ptr InactiveDelegate::peerListUiShow() { Unexpected("...InactiveDelegate::peerListUiShow"); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index cc3c33ddb4c8a1..586583ad8ee7e5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -1975,14 +1975,6 @@ void Members::peerListSetDescription( description.destroy(); } -void Members::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { -} - -void Members::peerListHideLayer() { -} - std::shared_ptr Members::peerListUiShow() { Unexpected("...Members::peerListUiShow"); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h index f8b4c8c52d78bc..47cb0591438330 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -88,10 +88,6 @@ class Members final void peerListFinishSelectedRowsBunch() override; void peerListSetDescription( object_ptr description) override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; void setupAddMember(not_null call); diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 0835535c308bc3..068200c12ad39f 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -24,7 +24,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_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; diff --git a/Telegram/SourceFiles/data/data_boosts.h b/Telegram/SourceFiles/data/data_boosts.h index a62c70c92ec318..bada02a9137f3c 100644 --- a/Telegram/SourceFiles/data/data_boosts.h +++ b/Telegram/SourceFiles/data/data_boosts.h @@ -10,7 +10,7 @@ For license and copyright information please follow this link: 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 prepaidGiveaway; QString link; }; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index e8eef897847be1..95b042775c8292 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -391,3 +391,7 @@ bool WebPageData::computeDefaultSmallMedia() const { } return false; } + +bool WebPageData::suggestEnlargePhoto() const { + return !siteName.isEmpty() || !title.isEmpty() || !description.empty(); +} diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 9aea4a0f58e15f..04f16a7b667379 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -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; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 835174386637e4..550ba55c78b517 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1864,9 +1864,12 @@ std::unique_ptr HistoryInner::prepareDrag() { session().data().setMimeForwardIds(std::move(forwardIds)); auto result = std::make_unique(); 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( + pressedHandler->property( + kDocumentLinkMediaProperty).toULongLong()); + if (lnkDocument) { + const auto filepath = lnkDocument->filepath(true); if (!filepath.isEmpty()) { QList urls; urls.push_back(QUrl::fromLocalFile(filepath)); @@ -3167,6 +3170,8 @@ void HistoryInner::recountHistoryGeometry() { accumulate_max(oldHistoryPaddingTop, _botAbout->height); } + updateBotInfo(false); + _history->resizeToWidth(_contentWidth); if (_migrated) { _migrated->resizeToWidth(_contentWidth); @@ -3190,7 +3195,6 @@ void HistoryInner::recountHistoryGeometry() { } } - updateBotInfo(false); if (const auto view = _botAbout ? _botAbout->view() : nullptr) { _botAbout->height = view->resizeGetHeight(_contentWidth); _botAbout->top = qMin( diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 18235872cb6d70..1bd5d52c28281e 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3355,6 +3355,7 @@ void HistoryItem::createComponentsHelper( : nullptr; if (!config.reply.externalPeerId && topic + && to && topic->rootId() != to->topicRootId()) { config.reply.externalPeerId = replyTo.messageId.peer; } diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.h b/Telegram/SourceFiles/history/view/history_view_cursor_state.h index 82fcda95479c8f..d5bcaa6f94f5aa 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.h +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.h @@ -22,6 +22,7 @@ enum class CursorState : char { None, Text, Date, + Enlarge, Forwarded, }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 410b1a337675e5..2db6bddb8a4979 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -3639,9 +3639,12 @@ std::unique_ptr ListWidget::prepareDrag() { session().data().setMimeForwardIds(std::move(forwardIds)); auto result = std::make_unique(); 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( + pressedHandler->property( + kDocumentLinkMediaProperty).toULongLong()); + if (lnkDocument) { + const auto filepath = lnkDocument->filepath(true); if (!filepath.isEmpty()) { QList urls; urls.push_back(QUrl::fromLocalFile(filepath)); diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 350ba3668ffa6c..f65613ea3fff5a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -105,6 +105,7 @@ void Giveaway::fillFromData(not_null 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, diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h index 6529ad4971cc9e..250e214c299abd 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h @@ -69,6 +69,9 @@ class Giveaway final : public Media { QRect geometry; ClickHandlerPtr link; mutable std::unique_ptr ripple; + mutable std::array corners; + mutable QColor bg; + uint8 colorIndex = 0; }; void paintBadge(Painter &p, const PaintContext &context) const; @@ -94,10 +97,8 @@ class Giveaway final : public Media { Ui::Text::String _winnersTitle; Ui::Text::String _winners; - mutable QColor _channelBg; mutable QColor _badgeFg; mutable QColor _badgeBorder; - mutable std::array _channelCorners; mutable QImage _badge; mutable QImage _badgeCache; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index e612022391a108..b0df17d486818f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -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); diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index b3d79c6fe2f027..85a2997a8bb9db 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -35,6 +35,7 @@ For license and copyright information please follow this link: #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; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 7213dca2100d46..4b123cc7c1498f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -163,6 +163,7 @@ class Photo final : public File { QPoint photoPosition) const; [[nodiscard]] QSize photoSize() const; + [[nodiscard]] QRect enlargeRect() const; void togglePollingStory(bool enabled) const; @@ -178,6 +179,7 @@ class Photo final : public File { mutable uint32 _imageCacheForum : 1 = 0; mutable uint32 _imageCacheBlurred : 1 = 0; mutable uint32 _pollingStory : 1 = 0; + mutable uint32 _showEnlarge : 1 = 0; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 56299bf451bced..c622585b9a50f5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -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)) { diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp index c1781ea031c483..235e2f1302f2d9 100644 --- a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp @@ -12,6 +12,7 @@ For license and copyright information please follow this link: #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 @@ For license and copyright information please follow this link: #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); }; } -} // namespace +[[nodiscard]] QWidget *FindFirstShadowInBox(not_null box) { + for (const auto &child : box->children()) { + if (child && child->isWidgetType()) { + const auto w = static_cast(child); + if (w->height() == st::lineWidth) { + return w; + } + } + } + return nullptr; +} -void CreateGiveawayBox( +void AddPremiumTopBarWithDefaultTitleBar( not_null box, - not_null controller, - not_null peer) { - box->setWidth(st::boxWideWidth); + rpl::producer<> showFinished, + rpl::producer titleText) { + struct State final { + Ui::Animations::Simple animation; + Ui::Text::String title; - const auto weakWindow = base::make_weak(controller->parentController()); + Ui::RpWidget close; + }; + const auto state = box->lifetime().make_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 bar = box->verticalLayout()->add( - object_ptr( - 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 isCloseBarShown = [=] { return box->scrollTop() > 0; }; + + const auto closeTopBar = box->setPinnedToTopContent( + object_ptr(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); - const auto container = box->verticalLayout(); - const auto &padding = st::giveawayGiftCodeCoverDividerPadding; - Settings::AddSkip(container, padding.top()); - Settings::AddDivider(container); - Settings::AddSkip(container, padding.bottom()); + 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( - container.get(), - st::boxTitleClose); + closeTopBar.get(), + st::startGiveawayBoxTitleClose); close->setClickedCallback([=] { box->closeBox(); }); - box->widthValue( - ) | rpl::start_with_next([=](int) { + 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( + 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( + 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 box, + not_null controller, + not_null peer, + Fn reloadOnDone, + std::optional prepaid) { + box->setWidth(st::boxWideWidth); + + const auto weakWindow = base::make_weak(controller->parentController()); + using GiveawayType = Giveaway::GiveawayTypeRow::Type; using GiveawayGroup = Ui::RadioenumGroup; struct State final { @@ -127,11 +256,25 @@ void CreateGiveawayBox( rpl::variable dateValue; rpl::variable> countriesValue; - bool confirmButtonBusy = false; + rpl::variable confirmButtonBusy = true; }; const auto state = box->lifetime().make_state(peer); const auto typeGroup = std::make_shared(); + 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>( box, @@ -157,7 +300,24 @@ void CreateGiveawayBox( object_ptr(box))); contentWrap->toggle(false, anim::type::instant); - { + if (prepaid) { + contentWrap->entity()->add( + object_ptr( + 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( box, @@ -168,7 +328,7 @@ void CreateGiveawayBox( state->typeValue.force_assign(GiveawayType::Random); }); } - { + if (!prepaid) { const auto row = contentWrap->entity()->add( object_ptr( box, @@ -209,7 +369,8 @@ void CreateGiveawayBox( using Controller = Giveaway::AwardMembersListController; auto listController = std::make_unique( 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(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(sliderContainer, st::settingsScale), + object_ptr(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 c) { + auto terms = object_ptr( + 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([=] { + box->closeBox(); + Settings::ShowPremium(&peer->session(), QString()); + })); + c->add(std::move(terms)); + }; + { const auto dateContainer = randomWrap->entity()->add( object_ptr(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(dateContainer); + terms->add(object_ptr( + 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( + 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(0); const auto listOptions = contentWrap->entity()->add( object_ptr(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( - 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([=] { - box->closeBox(); - Settings::ShowPremium(&peer->session(), QString()); - })); + auto termsContainer = object_ptr(listOptions); + addTerms(termsContainer.data()); listOptions->add(object_ptr( 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( 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(), 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; + } + }; + const auto startPrepaid = [=](Fn 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()); }; - Payments::CheckoutProcess::Start(std::move(invoice), done); + if (prepaid) { + const auto cancel = [=](Fn 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()); } diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.h b/Telegram/SourceFiles/info/boosts/create_giveaway_box.h index 7463732dcb5d12..d1fb6736f149da 100644 --- a/Telegram/SourceFiles/info/boosts/create_giveaway_box.h +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.h @@ -9,6 +9,10 @@ For license and copyright information please follow this link: class PeerData; +namespace Data { +struct BoostPrepaidGiveaway; +} // namespace Data + namespace Info { class Controller; } // namespace Info @@ -20,4 +24,6 @@ class GenericBox; void CreateGiveawayBox( not_null box, not_null controller, - not_null peer); + not_null peer, + Fn reloadOnDone, + std::optional prepaidGiveaway); diff --git a/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp new file mode 100644 index 00000000000000..13e8fb1923be53 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp @@ -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 InfiniteRadialAnimationWidget( + not_null parent, + int size) { + class Widget final : public Ui::RpWidget { + public: + Widget(not_null 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(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 parent, + rpl::producer text, + rpl::producer number, + rpl::producer shown) { + struct State { + QImage badge; + }; + const auto state = parent->lifetime().make_state(); + const auto label = Ui::CreateChild( + parent.get(), + st::startGiveawayButtonLabelSimple); + std::move( + text + ) | rpl::start_with_next([=](const QString &s) { + label->setText(s); + }, label->lifetime()); + const auto count = Ui::CreateChild(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 diff --git a/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h new file mode 100644 index 00000000000000..fe2427e9c773e6 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h @@ -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 InfiniteRadialAnimationWidget( + not_null parent, + int size); + +void AddLabelWithBadgeToButton( + not_null parent, + rpl::producer text, + rpl::producer number, + rpl::producer shown); + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style index db9ad6f042150f..4f04fdabb393dd 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style @@ -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; +} diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp index dd6df700f711e1..975dab5b3e1208 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp @@ -9,9 +9,12 @@ For license and copyright information please follow this link: #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 navigation, - not_null peer) -: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members) { + not_null peer, + std::vector> 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 row) { @@ -148,7 +159,26 @@ MyChannelsListController::MyChannelsListController( std::make_unique(&peer->session())) , _peer(peer) , _show(show) -, _selected(std::move(selected)) { +, _selected(std::move(selected)) +, _otherChannels(std::make_unique>>()) { + { + const auto addList = [&](not_null 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 MyChannelsListController::createSearchRow( @@ -167,6 +197,24 @@ std::unique_ptr 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 row) { const auto channel = row->peer()->asChannel(); const auto checked = !row->checked(); diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h index 8d08a8a159e859..6459d6d8fdeea6 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #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 navigation, - not_null peer); + not_null peer, + std::vector> selected); + + void prepare() override; void setCheckError(Fn callback); @@ -41,6 +45,8 @@ class AwardMembersListController : public ParticipantsBoxController { private: Fn _checkErrorCallback; + std::vector> _selected; + }; class MyChannelsListController : public PeerListController { @@ -55,6 +61,7 @@ class MyChannelsListController : public PeerListController { Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; + void loadMoreRows() override; std::unique_ptr createSearchRow( not_null peer) override; @@ -71,6 +78,8 @@ class MyChannelsListController : public PeerListController { Fn _checkErrorCallback; std::vector> _selected; + std::unique_ptr>> _otherChannels; + int _lastAddedIndex = 0; rpl::lifetime _apiLifetime; diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp index db1fedb08799af..4417bfb25151d7 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #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 parent, Type type, rpl::producer 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 parent, + Type type, + int colorIndex, + rpl::producer title, + rpl::producer 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(); diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h index 0167b338e89fe8..9bb96812b61fba 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h @@ -25,6 +25,8 @@ class GiveawayTypeRow final : public Ui::RippleButton { AllMembers, OnlyNewMembers, + + Prepaid, }; GiveawayTypeRow( @@ -32,6 +34,14 @@ class GiveawayTypeRow final : public Ui::RippleButton { Type type, rpl::producer subtitle); + GiveawayTypeRow( + not_null parent, + Type type, + int colorIndex, + rpl::producer title, + rpl::producer subtitle, + QImage badge); + void addRadio(std::shared_ptr> typeGroup); protected: @@ -47,6 +57,8 @@ class GiveawayTypeRow final : public Ui::RippleButton { Ui::Text::String _status; Ui::Text::String _name; + QImage _badge; + }; } // namespace Giveaway diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp index 494df7a99b0229..34e0d926fffa71 100644 --- a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp +++ b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp @@ -15,6 +15,8 @@ For license and copyright information please follow this link: #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 @@ For license and copyright information please follow this link: #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(content)); - Ui::AddCopyShareLinkButtons(copyShareWrap, copyLink, shareLink); - copyShareWrap->widgetAt(0)->showChildren(); + { + const auto wrap = content->add( + object_ptr( + content, + st::inviteLinkButton.height), + st::inviteLinkButtonsPadding); + const auto copy = CreateChild( + wrap, + tr::lng_group_invite_context_copy(), + st::inviteLinkCopy); + copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + copy->setClickedCallback(copyLink); + const auto share = CreateChild( + 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 content, not_null controller, std::shared_ptr show, - not_null peer) { + not_null peer, + Fn 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( 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(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( 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( + 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>( + object_ptr>( inner, - object_ptr( - inner, - st::defaultTabsSlider))); + object_ptr(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>( inner, @@ -339,10 +457,9 @@ void InnerWidget::fill() { object_ptr>( inner, object_ptr(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) { diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp index fac44f16ad0d25..5e4d8ed322125e 100644 --- a/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp +++ b/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp @@ -71,7 +71,7 @@ not_null Widget::peer() const { } bool Widget::showInternal(not_null memento) { - return false; + return (memento->statisticsPeer() == peer()); } rpl::producer Widget::title() { diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp index fed48653b1f0f8..720f47eba8c36b 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp @@ -281,16 +281,6 @@ void InnerWidget::peerListSetDescription( description.destroy(); } -void InnerWidget::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { - _show->showBox(std::move(content), options); -} - -void InnerWidget::peerListHideLayer() { - _show->hideLayer(); -} - std::shared_ptr InnerWidget::peerListUiShow() { return _show; } diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h index 91d906782196b1..9cfb2f3a824458 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h @@ -64,10 +64,6 @@ class InnerWidget final void peerListFinishSelectedRowsBunch() override; void peerListSetDescription( object_ptr description) override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; object_ptr setupList( diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 85758a9d7d5d6a..48426de06a8025 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -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 memento) { diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp index 5fc2308a5caeb0..b1cefdeff04214 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp +++ b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp @@ -51,10 +51,6 @@ class ListDelegate final : public PeerListContentDelegate { void peerListFinishSelectedRowsBunch() override; void peerListSetDescription( object_ptr description) override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; }; @@ -92,14 +88,6 @@ void ListDelegate::peerListSetDescription( description.destroy(); } -void ListDelegate::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { -} - -void ListDelegate::peerListHideLayer() { -} - std::shared_ptr ListDelegate::peerListUiShow() { Unexpected("...ListDelegate::peerListUiShow"); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_members.cpp b/Telegram/SourceFiles/info/profile/info_profile_members.cpp index 85e6899030da42..0574dd6da7419b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members.cpp @@ -459,16 +459,6 @@ void Members::peerListAddSelectedRowInBunch(not_null row) { void Members::peerListFinishSelectedRowsBunch() { } -void Members::peerListShowBox( - object_ptr content, - Ui::LayerOptions options) { - _show->showBox(std::move(content), options); -} - -void Members::peerListHideLayer() { - _show->hideLayer(); -} - std::shared_ptr Members::peerListUiShow() { return _show; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_members.h b/Telegram/SourceFiles/info/profile/info_profile_members.h index 2789756ce16876..ac86ca81381a0c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members.h +++ b/Telegram/SourceFiles/info/profile/info_profile_members.h @@ -78,10 +78,6 @@ class Members void peerListFinishSelectedRowsBunch() override; void peerListSetDescription( object_ptr description) override; - void peerListShowBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; - void peerListHideLayer() override; std::shared_ptr peerListUiShow() override; //void peerListAppendRow( diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_common.h b/Telegram/SourceFiles/info/statistics/info_statistics_common.h index 30a3e092f69480..cc943ca688cdea 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_common.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_common.h @@ -15,6 +15,7 @@ struct SavedState final { Data::AnyStatistics stats; base::flat_map recentPostPreviews; Data::PublicForwardsSlice publicForwardsFirstSlice; + int recentPostsExpanded = 0; }; } // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index b5b2ae8b094c40..19704c779755b7 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #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(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>( + container, + object_ptr( + 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(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); } } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 6d8c4307edb5d4..498acdbd1f2a6b 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #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; 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 parent) { const auto arrow = Ui::CreateChild(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(); } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp index 1a85458e93d2db..022da23a89ae0e 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp @@ -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 }, diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 9d58ad590701ac..0ad30366919d73 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -130,6 +130,39 @@ not_null SessionFromId(const InvoiceId &id) { return &giveaway.boostPeer->session(); } +MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL( + const InvoicePremiumGiftCode &invoice) { + const auto &giveaway = v::get( + 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 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( - 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 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); } } diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 706f9b81d4a94f..acf3750439e542 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -219,6 +219,9 @@ struct InvoiceId { [[nodiscard]] not_null SessionFromId(const InvoiceId &id); +[[nodiscard]] MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL( + const InvoicePremiumGiftCode &invoice); + class Form final : public base::has_weak_ptr { public: Form(InvoiceId id, bool receipt); diff --git a/Telegram/SourceFiles/platform/win/tray_win.cpp b/Telegram/SourceFiles/platform/win/tray_win.cpp index 2b4a42ae1df1a2..bafce77e3cec79 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.cpp +++ b/Telegram/SourceFiles/platform/win/tray_win.cpp @@ -38,7 +38,7 @@ constexpr auto kTooltipDelay = crl::time(10000); QOperatingSystemVersion::Windows, 10, 0, - 17763); + 18282); static const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion); if (!kSupported) { return std::nullopt; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index cb3eeccd892c58..b18e735f7bdc3f 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -523,7 +523,7 @@ TopBarUser::TopBarUser( , _content(this) , _title(_content, st::settingsPremiumUserTitle) , _about(_content, st::userPremiumCover.about) -, _ministars(_content) +, _ministars(_content, true) , _smallTop({ .widget = object_ptr(this), .text = Ui::Text::String( diff --git a/Telegram/SourceFiles/statistics/statistics.style b/Telegram/SourceFiles/statistics/statistics.style index 036fb16e52c12c..8fce0f2b9e2294 100644 --- a/Telegram/SourceFiles/statistics/statistics.style +++ b/Telegram/SourceFiles/statistics/statistics.style @@ -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 }}; diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index e9ffe759d595be..83bd4fa37843e1 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -8,11 +8,13 @@ For license and copyright information please follow this link: #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 @@ For license and copyright information please follow this link: #include 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 MakeTitle( + not_null box, + rpl::producer title, + rpl::producer repeated) { + auto result = object_ptr(box); + + struct State { + not_null title; + not_null repeated; + }; + const auto notEmpty = [](const QString &text) { + return !text.isEmpty(); + }; + const auto state = box->lifetime().make_state(State{ + .title = Ui::CreateChild( + result.data(), + rpl::duplicate(title), + st::boostTitle), + .repeated = Ui::CreateChild( + 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 parent) { const auto result = Ui::CreateChild(parent.get()); @@ -42,62 +132,69 @@ void StartFireworks(not_null parent) { void BoostBox( not_null box, BoostBoxData data, - Fn)> boost) { + Fn)> 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 you = false; + rpl::variable data; + rpl::variable full; bool submitted = false; }; - const auto state = box->lifetime().make_state(State{ - .you = data.boost.mine, - }); + const auto state = box->lifetime().make_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( - 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( 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 MakeLinkLabel( return result; } +void BoostBoxAlready(not_null 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 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 box) { + InformBox(box, { + .text = tr::lng_boost_error_gifted_text(Text::RichLangValue), + .title = tr::lng_boost_error_gifted_title(), + }); +} + +void PremiumForBoostsBox(not_null box, Fn 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 box, AskBoostBoxData data, @@ -269,19 +465,10 @@ void AskBoostBox( box->setWidth(st::boxWideWidth); box->setStyle(st::boostBox); - struct State { - rpl::variable you = false; - bool submitted = false; - }; - const auto state = box->lifetime().make_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 you, not_null container, - BoostCounters data, + rpl::producer 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(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); } diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index da8d57572e61f1..875a1405e6b5d7 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -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 box, BoostBoxData data, - Fn)> boost); + Fn)> boost); + +void BoostBoxAlready(not_null box); +void GiftForBoostsBox( + not_null box, + QString channel, + int receive, + bool again); +void GiftedNoBoostsBox(not_null box); +void PremiumForBoostsBox(not_null box, Fn buyPremium); struct AskBoostBoxData { QString link; @@ -57,9 +71,8 @@ void AskBoostBox( void FillBoostLimit( rpl::producer<> showFinished, - rpl::producer you, not_null container, - BoostCounters data, + rpl::producer data, style::margins limitLinePadding); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp index 22b2b39875d33e..5af3f3b2816e47 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp @@ -100,11 +100,4 @@ object_ptr MakeConfirmBox(ConfirmBoxArgs &&args) { return Box(ConfirmBox, std::move(args)); } -object_ptr MakeInformBox(v::text::data text) { - return MakeConfirmBox({ - .text = std::move(text), - .inform = true, - }); -} - } // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.h b/Telegram/SourceFiles/ui/boxes/confirm_box.h index b65a04361f00d5..c33192e294bba0 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.h +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.h @@ -40,10 +40,24 @@ struct ConfirmBoxArgs { bool strictCancel = false; }; -void ConfirmBox(not_null box, ConfirmBoxArgs &&args); +void ConfirmBox(not_null box, ConfirmBoxArgs &&args); -[[nodiscard]] object_ptr MakeConfirmBox( - ConfirmBoxArgs &&args); -[[nodiscard]] object_ptr MakeInformBox(v::text::data text); +inline void InformBox(not_null box, ConfirmBoxArgs &&args) { + args.inform = true; + ConfirmBox(box, std::move(args)); +} + +[[nodiscard]] object_ptr MakeConfirmBox(ConfirmBoxArgs &&args); + +[[nodiscard]] inline object_ptr MakeInformBox( + ConfirmBoxArgs &&args) { + args.inform = true; + return MakeConfirmBox(std::move(args)); +} + +[[nodiscard]] inline object_ptr MakeInformBox( + v::text::data text) { + return MakeInformBox({ .text = std::move(text) }); +} } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index d4f189ac0075b4..7567c501a80b39 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -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; diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp index 7bdebaee17568b..d85da4ec4f02cf 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp @@ -505,6 +505,10 @@ ChatStyle::ChatStyle(rpl::producer colorIndices) { &MessageImageStyle::historyVideoMessageMute, st::historyVideoMessageMute, st::historyVideoMessageMuteSelected); + make( + &MessageImageStyle::historyPageEnlarge, + st::historyPageEnlarge, + st::historyPageEnlargeSelected); updateDarkValue(); } diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index 39f5a76e448a2a..afa63a7ae27346 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -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 { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index bad612d203921b..67717c51775061 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -1108,57 +1108,4 @@ not_null CreateUploadSubButton( return upload; } -object_ptr CreateBoostReplaceUserpics( - not_null parent, - not_null from, - not_null to) { - const auto full = st::boostReplaceUserpic.size.height() - + st::boostReplaceIconAdd.y() - + st::lineWidth; - auto result = object_ptr(parent, full); - const auto raw = result.data(); - const auto &st = st::boostReplaceUserpic; - const auto left = CreateChild(raw, from, st); - const auto right = CreateChild(raw, to, st); - const auto overlay = CreateChild(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 diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index db3cdd3fdd3c7f..8d2eb2adf9598d 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -204,9 +204,4 @@ class UserpicButton final : public RippleButton { not_null contact, not_null controller); -[[nodiscard]] object_ptr CreateBoostReplaceUserpics( - not_null parent, - not_null from, - not_null to); - } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 46305a2c7907c2..69c356610ae0fd 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -27,11 +27,13 @@ PremiumBubble { font: font; } PremiumCover { + bg: color; starSize: size; starTopSkip: pixels; titlePadding: margins; titleFont: font; about: FlatLabel; + additionalShadowForDarkThemes: bool; } premiumAboutTextStyle: TextStyle(defaultTextStyle) { @@ -40,6 +42,7 @@ premiumAboutTextStyle: TextStyle(defaultTextStyle) { lineHeight: 18px; } defaultPremiumCover: PremiumCover { + bg: boxBg; starSize: size(84px, 81px); starTopSkip: 37px; titlePadding: margins(0px, 18px, 0px, 11px); @@ -53,6 +56,7 @@ defaultPremiumCover: PremiumCover { textFg: premiumButtonFg; minWidth: 190px; } + additionalShadowForDarkThemes: true; } userPremiumCoverAbout: FlatLabel(boxDividerLabel) { style: premiumAboutTextStyle; @@ -243,17 +247,26 @@ boostTitleSkip: 32px; boostTitle: FlatLabel(defaultFlatLabel) { minWidth: 40px; textFg: windowBoldFg; - align: align(top); maxHeight: 24px; style: TextStyle(boxTextStyle) { font: font(17px semibold); } } +boostTitleBadge: FlatLabel(defaultFlatLabel) { + margin: margins(4px, 2px, 4px, 2px); + style: semiboldTextStyle; + textFg: premiumButtonFg; +} +boostTitleBadgeSkip: 6px; boostTextSkip: 5px; boostText: FlatLabel(defaultFlatLabel) { minWidth: 40px; align: align(top); } +boostReassignText: FlatLabel(defaultFlatLabel) { + minWidth: 40px; + align: align(top); +} boostBottomSkip: 6px; boostBox: Box(premiumPreviewDoubledLimitsBox) { buttonPadding: margins(22px, 22px, 22px, 22px); @@ -267,11 +280,12 @@ boostBox: Box(premiumPreviewDoubledLimitsBox) { boostReplaceUserpicsPadding: margins(0px, 18px, 0px, 20px); boostReplaceUserpicsSkip: 42px; +boostReplaceUserpicsShift: 24px; boostReplaceUserpic: UserpicButton(defaultUserpicButton) { size: size(60px, 60px); photoSize: 60px; } -boostReplaceIcon: icon{{ "stories/boost_mini", windowBg }}; +boostReplaceIcon: icon{{ "stories/boost_mini", premiumButtonFg }}; boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px); diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index c7c8cadbe73c9c..6c134a8b81a5ff 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -195,7 +195,7 @@ class Bubble final { [[nodiscard]] int height() const; [[nodiscard]] int width() const; [[nodiscard]] int bubbleRadius() const; - [[nodiscard]] int countMaxWidth(int maxCounter) const; + [[nodiscard]] int countMaxWidth(int maxPossibleCounter) const; void setCounter(int value); void setTailEdge(EdgeProgress edge); @@ -271,12 +271,12 @@ int Bubble::width() const { return filledWidth() + _numberAnimation.countWidth(); } -int Bubble::countMaxWidth(int maxCounter) const { +int Bubble::countMaxWidth(int maxPossibleCounter) const { auto numbers = Ui::NumbersAnimation(_st.font, [] {}); numbers.setDisabledMonospace(true); numbers.setDuration(0); numbers.setText(_textFactory(0), 0); - numbers.setText(_textFactory(maxCounter), maxCounter); + numbers.setText(_textFactory(maxPossibleCounter), maxPossibleCounter); numbers.finishAnimating(); return filledWidth() + numbers.maxWidth(); } @@ -389,7 +389,6 @@ class BubbleWidget final : public Ui::RpWidget { const style::PremiumBubble &st, TextFactory textFactory, rpl::producer state, - int maxCounter, bool premiumPossible, rpl::producer<> showFinishes, const style::icon *icon, @@ -414,9 +413,8 @@ class BubbleWidget final : public Ui::RpWidget { BubbleRowState _animatingFrom; float64 _animatingFromResultRatio = 0.; rpl::variable _state; - const int _maxCounter; Bubble _bubble; - const int _maxBubbleWidth; + int _maxBubbleWidth = 0; const bool _premiumPossible; const style::margins _outerPadding; @@ -439,7 +437,6 @@ BubbleWidget::BubbleWidget( const style::PremiumBubble &st, TextFactory textFactory, rpl::producer state, - int maxCounter, bool premiumPossible, rpl::producer<> showFinishes, const style::icon *icon, @@ -447,14 +444,12 @@ BubbleWidget::BubbleWidget( : RpWidget(parent) , _st(st) , _state(std::move(state)) -, _maxCounter(maxCounter) , _bubble( _st, [=] { update(); }, std::move(textFactory), icon, premiumPossible) -, _maxBubbleWidth(_bubble.countMaxWidth(_maxCounter)) , _premiumPossible(premiumPossible) , _outerPadding(outerPadding) , _deflection(kDeflection) @@ -485,6 +480,7 @@ BubbleWidget::BubbleWidget( } void BubbleWidget::animateTo(BubbleRowState state) { + _maxBubbleWidth = _bubble.countMaxWidth(state.counter); const auto parent = parentWidget(); const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { const auto halfWidth = (_maxBubbleWidth / 2); @@ -541,6 +537,11 @@ void BubbleWidget::animateTo(BubbleRowState state) { const auto duration = kSlideDuration * (_ignoreDeflection ? kStepBeforeDeflection : 1.) * ((_state.current().ratio < 0.001) ? 0.5 : 1.); + if (state.animateFromZero) { + _animatingFrom.ratio = 0.; + _animatingFrom.counter = 0; + _animatingFromResultRatio = 0.; + } _appearanceAnimation.start([=](float64 value) { if (!_appearanceAnimation.animating()) { _animatingFrom = state; @@ -658,7 +659,7 @@ class Line final : public Ui::RpWidget { not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, - rpl::producer ratio); + rpl::producer state); void setColorOverride(QBrush brush); @@ -675,6 +676,7 @@ class Line final : public Ui::RpWidget { float64 _ratio = 0.; Ui::Animations::Simple _animation; + rpl::event_stream<> _recaches; Ui::Text::String _leftLabel; Ui::Text::String _leftText; Ui::Text::String _rightLabel; @@ -707,44 +709,56 @@ Line::Line( QString min, float64 ratio) : Line(parent, st, LimitRowLabels{ - .leftLabel = tr::lng_premium_free(tr::now), - .leftCount = min, - .rightLabel = tr::lng_premium(tr::now), - .rightCount = max, -}, rpl::single(ratio)) { + .leftLabel = tr::lng_premium_free(), + .leftCount = rpl::single(min), + .rightLabel = tr::lng_premium(), + .rightCount = rpl::single(max), +}, rpl::single(LimitRowState{ ratio })) { } Line::Line( not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, - rpl::producer ratio) + rpl::producer state) : Ui::RpWidget(parent) -, _st(st) -, _leftLabel(st::semiboldTextStyle, labels.leftLabel) -, _leftText(st::semiboldTextStyle, labels.leftCount) -, _rightLabel(st::semiboldTextStyle, labels.rightLabel) -, _rightText(st::semiboldTextStyle, labels.rightCount) -, _dynamic(labels.dynamic) { +, _st(st) { resize(width(), st::requestsAcceptButton.height); - std::move(ratio) | rpl::start_with_next([=](float64 ratio) { + const auto set = [&]( + Ui::Text::String &label, + rpl::producer &text) { + std::move(text) | rpl::start_with_next([=, &label](QString text) { + label = { st::semiboldTextStyle, text }; + _recaches.fire({}); + }, lifetime()); + }; + set(_leftLabel, labels.leftLabel); + set(_leftText, labels.leftCount); + set(_rightLabel, labels.rightLabel); + set(_rightText, labels.rightCount); + + std::move(state) | rpl::start_with_next([=](LimitRowState state) { + _dynamic = state.dynamic; if (width() > 0) { - const auto from = _animation.value(_ratio); + const auto from = state.animateFromZero + ? 0. + : _animation.value(_ratio); const auto duration = kSlideDuration * kStepBeforeDeflection; _animation.start([=] { update(); - }, from, ratio, duration, anim::easeOutCirc); + }, from, state.ratio, duration, anim::easeOutCirc); } - _ratio = ratio; + _ratio = state.ratio; }, lifetime()); rpl::combine( sizeValue(), - parent->widthValue() - ) | rpl::filter([](const QSize &size, int parentWidth) { + parent->widthValue(), + _recaches.events_starting_with({}) + ) | rpl::filter([](const QSize &size, int parentWidth, auto) { return !size.isEmpty() && parentWidth; - }) | rpl::start_with_next([=](const QSize &size, int) { + }) | rpl::start_with_next([=](const QSize &size, auto, auto) { recache(size); update(); }, lifetime()); @@ -906,7 +920,6 @@ void AddBubbleRow( .counter = current, .ratio = (current - min) / float64(max - min), }), - max, premiumPossible, ProcessTextFactory(phrase), icon, @@ -918,7 +931,6 @@ void AddBubbleRow( const style::PremiumBubble &st, rpl::producer<> showFinishes, rpl::producer state, - int max, bool premiumPossible, Fn text, const style::icon *icon, @@ -930,7 +942,6 @@ void AddBubbleRow( st, text ? std::move(text) : ProcessTextFactory(std::nullopt), std::move(state), - max, premiumPossible, std::move(showFinishes), icon, @@ -975,10 +986,10 @@ void AddLimitRow( not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, - rpl::producer ratio, + rpl::producer state, const style::margins &padding) { parent->add( - object_ptr(parent, st, std::move(labels), std::move(ratio)), + object_ptr(parent, st, std::move(labels), std::move(state)), padding); } diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h index 1292a698044299..728dd9d40dce66 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.h +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -55,6 +55,7 @@ void AddBubbleRow( struct BubbleRowState { int counter = 0; float64 ratio = 0.; + bool animateFromZero = false; bool dynamic = false; }; void AddBubbleRow( @@ -62,7 +63,6 @@ void AddBubbleRow( const style::PremiumBubble &st, rpl::producer<> showFinishes, rpl::producer state, - int max, bool premiumPossible, Fn text, const style::icon *icon, @@ -84,17 +84,23 @@ void AddLimitRow( float64 ratio = kLimitRowRatio); struct LimitRowLabels { - QString leftLabel; - QString leftCount; - QString rightLabel; - QString rightCount; + rpl::producer leftLabel; + rpl::producer leftCount; + rpl::producer rightLabel; + rpl::producer rightCount; +}; + +struct LimitRowState { + float64 ratio = 0.; + bool animateFromZero = false; bool dynamic = false; }; + void AddLimitRow( not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, - rpl::producer ratio, + rpl::producer state, const style::margins &padding); struct AccountsRowArgs final { diff --git a/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp b/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp index 5eeee3b89248a1..18c9c9c24f9a80 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp @@ -13,10 +13,16 @@ For license and copyright information please follow this link: namespace Ui { namespace Premium { -ColoredMiniStars::ColoredMiniStars(not_null parent) -: _ministars([=](const QRect &r) { - parent->update(r.translated(_position)); -}, true) { +ColoredMiniStars::ColoredMiniStars( + not_null parent, + bool optimizeUpdate) +: _ministars( + optimizeUpdate + ? Fn([=](const QRect &r) { + parent->update(r.translated(_position)); + }) + : Fn([=](const QRect &) { parent->update(); }), + true) { } void ColoredMiniStars::setSize(const QSize &size) { diff --git a/Telegram/SourceFiles/ui/effects/premium_stars_colored.h b/Telegram/SourceFiles/ui/effects/premium_stars_colored.h index a8bdbd9746bfb6..75660ab9927114 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars_colored.h +++ b/Telegram/SourceFiles/ui/effects/premium_stars_colored.h @@ -16,7 +16,8 @@ namespace Premium { class ColoredMiniStars final { public: - ColoredMiniStars(not_null parent); + // optimizeUpdate may cause paint glitch. + ColoredMiniStars(not_null parent, bool optimizeUpdate); void setSize(const QSize &size); void setPosition(QPoint position); diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 951eb8f219e40e..4094b584d2f5e9 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -120,8 +120,8 @@ void TopBarAbstract::paintEdges(QPainter &p, const QBrush &brush) const { } void TopBarAbstract::paintEdges(QPainter &p) const { - paintEdges(p, st::boxBg); - if (isDark()) { + paintEdges(p, st().bg); + if (isDark() && st().additionalShadowForDarkThemes) { paintEdges(p, st::shadowFg); paintEdges(p, st::shadowFg); } @@ -144,7 +144,7 @@ bool TopBarAbstract::isDark() const { void TopBarAbstract::computeIsDark() { const auto contrast = CountContrast( - st::boxBg->c, + st().bg->c, st::premiumButtonFg->c); _isDark = (contrast > kMinAcceptableContrast); } @@ -155,13 +155,14 @@ TopBar::TopBar( Fn clickContextOther, rpl::producer title, rpl::producer about, - bool light) + bool light, + bool optimizeMinistars) : TopBarAbstract(parent, st) , _light(light) , _titleFont(st.titleFont) , _titlePadding(st.titlePadding) , _about(this, std::move(about), st.about) -, _ministars(this) { +, _ministars(this, optimizeMinistars) { std::move( title ) | rpl::start_with_next([=](QString text) { diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index c0749df00addef..9ccc4fa30a582b 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -72,7 +72,8 @@ class TopBar final : public TopBarAbstract { Fn clickContextOther, rpl::producer title, rpl::producer about, - bool light = false); + bool light = false, + bool optimizeMinistars = true); ~TopBar(); void setPaused(bool paused) override; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index b6822f2d4b24b1..89fac17effb3f0 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "boxes/add_contact_box.h" #include "boxes/peers/add_bot_to_chat_box.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/peers/replace_boost_box.h" #include "boxes/delete_messages_box.h" #include "window/window_adaptive.h" #include "window/window_controller.h" @@ -629,20 +630,13 @@ void SessionNavigation::resolveBoostState(not_null channel) { channel->input )).done([=](const MTPpremium_BoostsStatus &result) { _boostStateResolving = nullptr; - const auto &data = result.data(); - const auto submit = [=](Fn done) { + const auto submit = [=](Fn done) { applyBoost(channel, done); }; - const auto next = data.vnext_level_boosts().value_or_empty(); uiShow()->show(Box(Ui::BoostBox, Ui::BoostBoxData{ .name = channel->name(), - .boost = { - .level = data.vlevel().v, - .boosts = data.vboosts().v, - .thisLevelBoosts = data.vcurrent_level_boosts().v, - .nextLevelBoosts = next, - .mine = data.is_my_boost(), - }, + .boost = ParseBoostCounters(result), + .allowMulti = (BoostsForGift(_session) > 0), }, submit)); }).fail([=](const MTP::Error &error) { _boostStateResolving = nullptr; @@ -652,148 +646,105 @@ void SessionNavigation::resolveBoostState(not_null channel) { void SessionNavigation::applyBoost( not_null channel, - Fn done) { + Fn done) { _api.request(MTPpremium_GetMyBoosts( )).done([=](const MTPpremium_MyBoosts &result) { const auto &data = result.data(); _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); - const auto &list = data.vmy_boosts().v; - if (list.isEmpty()) { - if (!_session->premium()) { - const auto jumpToPremium = [=] { + const auto slots = ParseForChannelBoostSlots( + channel, + data.vmy_boosts().v); + if (!slots.free.empty()) { + applyBoostsChecked(channel, { slots.free.front() }, done); + } else if (slots.other.empty()) { + if (!slots.already.empty()) { + if (const auto receive = BoostsForGift(_session)) { + const auto again = true; + const auto name = channel->name(); + uiShow()->show( + Box(Ui::GiftForBoostsBox, name, receive, again)); + } else { + uiShow()->show(Box(Ui::BoostBoxAlready)); + } + } else if (!_session->premium()) { + uiShow()->show(Box(Ui::PremiumForBoostsBox, [=] { const auto id = peerToChannel(channel->id).bare; Settings::ShowPremium( parentController(), "channel_boost__" + QString::number(id)); - }; - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_premium_text( - Ui::Text::RichLangValue), - .confirmed = jumpToPremium, - .confirmText = tr::lng_boost_error_premium_yes(), - .title = tr::lng_boost_error_premium_title(), })); + } else if (const auto receive = BoostsForGift(_session)) { + const auto again = false; + const auto name = channel->name(); + uiShow()->show( + Box(Ui::GiftForBoostsBox, name, receive, again)); } else { - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_gifted_text( - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_gifted_title(), - .inform = true, - })); - } - done(false); - return; - } - auto slot = int(); - auto different = PeerId(); - auto earliest = TimeId(-1); - const auto now = base::unixtime::now(); - for (const auto &my : list) { - const auto &data = my.data(); - const auto cooldown = data.vcooldown_until_date().value_or(0); - const auto peerId = data.vpeer() - ? peerFromMTP(*data.vpeer()) - : PeerId(); - if (!peerId && cooldown <= now) { - applyBoostChecked(channel, data.vslot().v, done); - return; - } else if (peerId != channel->id - && (earliest < 0 || cooldown < earliest)) { - slot = data.vslot().v; - different = peerId; - earliest = cooldown; - } - } - if (different) { - if (earliest > now) { - const auto seconds = earliest - now; - const auto days = seconds / 86400; - const auto hours = seconds / 3600; - const auto minutes = seconds / 60; - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_flood_text( - lt_left, - rpl::single(Ui::Text::Bold((days > 1) - ? tr::lng_days(tr::now, lt_count, days) - : (hours > 1) - ? tr::lng_hours(tr::now, lt_count, hours) - : (minutes > 1) - ? tr::lng_minutes(tr::now, lt_count, minutes) - : tr::lng_seconds(tr::now, lt_count, seconds))), - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_flood_title(), - .inform = true, - })); - done(false); - } else { - const auto peer = _session->data().peer(different); - replaceBoostConfirm(peer, channel, slot, done); + uiShow()->show(Box(Ui::GiftedNoBoostsBox)); } + done({}); } else { - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_already_text( - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_already_title(), - .inform = true, - })); - done(false); + const auto weak = std::make_shared>(); + const auto reassign = [=](std::vector slots, int sources) { + const auto count = int(slots.size()); + const auto callback = [=](Ui::BoostCounters counters) { + if (const auto strong = weak->data()) { + strong->closeBox(); + } + done(counters); + uiShow()->showToast(tr::lng_boost_reassign_done( + tr::now, + lt_count, + count, + lt_channels, + tr::lng_boost_reassign_channels( + tr::now, + lt_count, + sources))); + }; + applyBoostsChecked( + channel, + slots, + crl::guard(this, callback)); + }; + *weak = uiShow()->show(ReassignBoostsBox( + channel, + slots.other, + reassign, + [=] { done({}); })); } }).fail([=](const MTP::Error &error) { const auto type = error.type(); showToast(u"Error: "_q + type); - done(false); + done({}); }).handleFloodErrors().send(); } -void SessionNavigation::replaceBoostConfirm( - not_null from, +void SessionNavigation::applyBoostsChecked( not_null channel, - int slot, - Fn done) { - const auto forwarded = std::make_shared(false); - const auto confirmed = [=](Fn close) { - *forwarded = true; - applyBoostChecked(channel, slot, done); - close(); - }; - const auto box = uiShow()->show(Box([=](not_null box) { - Ui::ConfirmBox(box, { - .text = tr::lng_boost_now_instead( - lt_channel, - rpl::single(Ui::Text::Bold(from->name())), - lt_other, - rpl::single(Ui::Text::Bold(channel->name())), - Ui::Text::WithEntities), - .confirmed = confirmed, - .confirmText = tr::lng_boost_now_replace(), - .labelPadding = st::boxRowPadding, - }); - box->verticalLayout()->insert( - 0, - Ui::CreateBoostReplaceUserpics(box, from, channel), - st::boxRowPadding + st::boostReplaceUserpicsPadding); + std::vector slots, + Fn done) { + auto mtp = MTP_vector_from_range(ranges::views::all( + slots + ) | ranges::views::transform([](int slot) { + return MTP_int(slot); })); - box->boxClosing() | rpl::filter([=] { - return !*forwarded; - }) | rpl::start_with_next([=] { - done(false); - }, box->lifetime()); -} - -void SessionNavigation::applyBoostChecked( - not_null channel, - int slot, - Fn done) { _api.request(MTPpremium_ApplyBoost( MTP_flags(MTPpremium_ApplyBoost::Flag::f_slots), - MTP_vector({ MTP_int(slot) }), + std::move(mtp), channel->input )).done([=](const MTPpremium_MyBoosts &result) { - done(true); + _api.request(MTPpremium_GetBoostsStatus( + channel->input + )).done([=](const MTPpremium_BoostsStatus &result) { + done(ParseBoostCounters(result)); + }).fail([=](const MTP::Error &error) { + showToast(u"Error: "_q + error.type()); + done({}); + }).send(); }).fail([=](const MTP::Error &error) { showToast(u"Error: "_q + error.type()); - done(false); + done({}); }).send(); } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 2ae8889373ccad..0a5fa2225255e6 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -68,6 +68,7 @@ struct ChatPaintContext; struct ChatThemeBackground; struct ChatThemeBackgroundData; class MessageSendingAnimationController; +struct BoostCounters; } // namespace Ui namespace Data { @@ -314,16 +315,13 @@ class SessionNavigation : public base::has_weak_ptr { const PeerByLinkInfo &info); void resolveBoostState(not_null channel); - void applyBoost(not_null channel, Fn done); - void replaceBoostConfirm( - not_null from, + void applyBoost( not_null channel, - int slot, - Fn done); - void applyBoostChecked( + Fn done); + void applyBoostsChecked( not_null channel, - int slot, - Fn done); + std::vector slots, + Fn done); const not_null _session; diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 8cd67af64e2d5a..a6f0eeef08df0e 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -418,7 +418,7 @@ def runStages(): stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout f603f4f986 + git checkout 58c8cd0c0f """) stage('msys64', """ diff --git a/Telegram/build/version b/Telegram/build/version index 0ba84356e82a67..447ef2247e1f47 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4011005 +AppVersion 4011006 AppVersionStrMajor 4.11 -AppVersionStrSmall 4.11.5 -AppVersionStr 4.11.5 +AppVersionStrSmall 4.11.6 +AppVersionStr 4.11.6 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.11.5 +AppVersionOriginal 4.11.6 diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index b9eab33971a0b6..cdf8cbf278b88e 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -110,6 +110,8 @@ PRIVATE info/userpic/info_userpic_emoji_builder_layer.cpp info/userpic/info_userpic_emoji_builder_layer.h + info/boosts/giveaway/boost_badge.cpp + info/boosts/giveaway/boost_badge.h info/boosts/giveaway/giveaway_type_row.cpp info/boosts/giveaway/giveaway_type_row.h info/boosts/giveaway/select_countries_box.cpp diff --git a/Telegram/lib_ui b/Telegram/lib_ui index efe306db1f8ec8..cf90d4038d3c24 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit efe306db1f8ec8dbb4b93e2513fba118bff34428 +Subproject commit cf90d4038d3c24c5c6b753b6cbaf96388c5d2fd7 diff --git a/changelog.txt b/changelog.txt index c68073a1326c3e..245ef9c1ef87a6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +4.11.6 (09.11.23) + +- Support multiple boosts and reassignment. +- Improve giveaway creation flow. +- Fix crash in topics creation. + 4.11.5 (06.11.23) - Giveaway phrases and sticker fixes.