diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ad5ccedc5..947fc9178 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -30,7 +30,7 @@ jobs: curl -sSL https://install.python-poetry.org | python3 - - name: Free up some disk space. - uses: jlumbroso/free-disk-space@76866dbe54312617f00798d1762df7f43def6e5c + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 - name: Docker image build. run: | diff --git a/.github/workflows/master_updater.yml b/.github/workflows/master_updater.yml index dc77d50d0..284bb844e 100644 --- a/.github/workflows/master_updater.yml +++ b/.github/workflows/master_updater.yml @@ -11,7 +11,9 @@ jobs: SKIP: "0" to_branch: "master" steps: - - uses: actions/checkout@v3.1.0 + - uses: actions/checkout@v4.1.0 + with: + fetch-depth: 0 if: env.SKIP == '0' - name: Push the code to the master branch. if: env.SKIP == '0' diff --git a/.gitmodules b/.gitmodules index 47d501392..7972f7ae3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -100,3 +100,6 @@ [submodule "Telegram/ThirdParty/wayland"] path = Telegram/ThirdParty/wayland url = https://github.com/gitlab-freedesktop-mirrors/wayland.git +[submodule "Telegram/ThirdParty/libprisma"] + path = Telegram/ThirdParty/libprisma + url = https://github.com/desktop-app/libprisma.git diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 52cb7501d..d2c6768fd 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -28,6 +28,7 @@ include(cmake/lib_ffmpeg.cmake) include(cmake/lib_stripe.cmake) include(cmake/lib_tgvoip.cmake) include(cmake/lib_tgcalls.cmake) +include(cmake/lib_prisma.cmake) include(cmake/td_export.cmake) include(cmake/td_mtproto.cmake) include(cmake/td_lang.cmake) @@ -218,6 +219,8 @@ PRIVATE api/api_sensitive_content.h api/api_single_message_search.cpp api/api_single_message_search.h + api/api_statistics.cpp + api/api_statistics.h api/api_text_entities.cpp api/api_text_entities.h api/api_toggling_media.cpp @@ -517,6 +520,7 @@ PRIVATE data/data_audio_msg_id.h data/data_auto_download.cpp data/data_auto_download.h + data/data_boosts.h data/data_bot_app.cpp data/data_bot_app.h data/data_chat.cpp @@ -619,6 +623,7 @@ PRIVATE data/data_sparse_ids.h data/data_sponsored_messages.cpp data/data_sponsored_messages.h + data/data_statistics.h data/data_stories.cpp data/data_stories.h data/data_stories_ids.cpp @@ -892,6 +897,10 @@ PRIVATE info/info_top_bar.h info/info_wrap_widget.cpp info/info_wrap_widget.h + info/boosts/info_boosts_inner_widget.cpp + info/boosts/info_boosts_inner_widget.h + info/boosts/info_boosts_widget.cpp + info/boosts/info_boosts_widget.h info/common_groups/info_common_groups_inner_widget.cpp info/common_groups/info_common_groups_inner_widget.h info/common_groups/info_common_groups_widget.cpp @@ -947,6 +956,15 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/statistics/info_statistics_common.h + info/statistics/info_statistics_inner_widget.cpp + info/statistics/info_statistics_inner_widget.h + info/statistics/info_statistics_list_controllers.cpp + info/statistics/info_statistics_list_controllers.h + info/statistics/info_statistics_recent_message.cpp + info/statistics/info_statistics_recent_message.h + info/statistics/info_statistics_widget.cpp + info/statistics/info_statistics_widget.h info/stories/info_stories_inner_widget.cpp info/stories/info_stories_inner_widget.h info/stories/info_stories_provider.cpp @@ -1608,6 +1626,7 @@ elseif (APPLE) PRE_LINK COMMAND mkdir -p $/../Resources COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $/../Resources + COMMAND cp ${CMAKE_BINARY_DIR}/lib_spellcheck.rcc $/../Resources ) if (NOT build_macstore) add_custom_command(TARGET Telegram diff --git a/Telegram/Resources/animations/stats.tgs b/Telegram/Resources/animations/stats.tgs new file mode 100644 index 000000000..4966041ce Binary files /dev/null and b/Telegram/Resources/animations/stats.tgs differ diff --git a/Telegram/Resources/icons/chat/mini_copy.png b/Telegram/Resources/icons/chat/mini_copy.png new file mode 100644 index 000000000..05af01e49 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_copy.png differ diff --git a/Telegram/Resources/icons/chat/mini_copy@2x.png b/Telegram/Resources/icons/chat/mini_copy@2x.png new file mode 100644 index 000000000..69693d749 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_copy@2x.png differ diff --git a/Telegram/Resources/icons/chat/mini_copy@3x.png b/Telegram/Resources/icons/chat/mini_copy@3x.png new file mode 100644 index 000000000..809361d53 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_copy@3x.png differ diff --git a/Telegram/Resources/icons/chat/mini_quote.png b/Telegram/Resources/icons/chat/mini_quote.png new file mode 100644 index 000000000..08a844820 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_quote.png differ diff --git a/Telegram/Resources/icons/chat/mini_quote@2x.png b/Telegram/Resources/icons/chat/mini_quote@2x.png new file mode 100644 index 000000000..8068b2208 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_quote@2x.png differ diff --git a/Telegram/Resources/icons/chat/mini_quote@3x.png b/Telegram/Resources/icons/chat/mini_quote@3x.png new file mode 100644 index 000000000..2399ff033 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_quote@3x.png differ diff --git a/Telegram/Resources/icons/menu/stats.png b/Telegram/Resources/icons/menu/stats.png new file mode 100644 index 000000000..0bafaa395 Binary files /dev/null and b/Telegram/Resources/icons/menu/stats.png differ diff --git a/Telegram/Resources/icons/menu/stats@2x.png b/Telegram/Resources/icons/menu/stats@2x.png new file mode 100644 index 000000000..18db4e9ec Binary files /dev/null and b/Telegram/Resources/icons/menu/stats@2x.png differ diff --git a/Telegram/Resources/icons/menu/stats@3x.png b/Telegram/Resources/icons/menu/stats@3x.png new file mode 100644 index 000000000..77bc3547d Binary files /dev/null and b/Telegram/Resources/icons/menu/stats@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9dea0b91e..28906fea5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2686,6 +2686,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_formatting_link_create" = "Create"; "lng_text_copied" = "Text copied to clipboard."; +"lng_code_copied" = "Code copied to clipboard."; "lng_spellchecker_submenu" = "Spelling"; "lng_spellchecker_add" = "Add to Dictionary"; @@ -4065,6 +4066,80 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_link_invalid" = "This link is broken or has expired."; +"lng_stats_title" = "Statistics"; +"lng_stats_message_title" = "Message Statistic"; +"lng_stats_zoom_out" = "Zoom Out"; + +"lng_stats_overview_title" = "Overview"; +"lng_stats_overview_member_count" = "Followers"; +"lng_stats_overview_mean_view_count" = "Views Per Post"; +"lng_stats_overview_mean_share_count" = "Shared Per Post"; +"lng_stats_overview_enabled_notifications" = "Enabled Notifications"; +"lng_stats_overview_messages" = "Messages"; +"lng_stats_overview_group_mean_view_count" = "Viewing Members"; +"lng_stats_overview_group_mean_post_count" = "Posting Members"; +"lng_stats_overview_message_private_shares" = "Private Shares"; +"lng_stats_overview_message_public_shares" = "Public Shares"; +"lng_stats_overview_message_views" = "Views"; +"lng_stats_overview_message_public_share#one" = "{count} public share"; +"lng_stats_overview_message_public_share#other" = "{count} public shares"; + +"lng_stats_members_title" = "Top members"; +"lng_stats_admins_title" = "Top admins"; +"lng_stats_inviters_title" = "Top inviters"; +"lng_stats_member_messages#one" = "{count} message"; +"lng_stats_member_messages#other" = "{count} messages"; +"lng_stats_member_characters#one" = "{count} symbol per message"; +"lng_stats_member_characters#other" = "{count} symbols per message"; +"lng_stats_member_deletions#one" = "{count} deletions"; +"lng_stats_member_deletions#other" = "{count} deletions"; +"lng_stats_member_bans#one" = "{count} ban"; +"lng_stats_member_bans#other" = "{count} bans"; +"lng_stats_member_restrictions#one" = "{count} restriction"; +"lng_stats_member_restrictions#other" = "{count} restrictions"; +"lng_stats_member_invitations#one" = "{count} invitation"; +"lng_stats_member_invitations#other" = "{count} invitations"; + +"lng_stats_recent_messages_title" = "Recent posts"; +"lng_stats_recent_messages_views#one" = "{count} view"; +"lng_stats_recent_messages_views#other" = "{count} views"; +"lng_stats_recent_messages_shares#one" = "{count} share"; +"lng_stats_recent_messages_shares#other" = "{count} shares"; + +"lng_stats_loading" = "Loading stats..."; +"lng_stats_loading_subtext" = "Please wait a few moments while we generate your stats."; + +"lng_chart_title_member_count" = "Growth"; +"lng_chart_title_join" = "Followers"; +"lng_chart_title_mute" = "Notifications"; +"lng_chart_title_view_count_by_hour" = "Views by hours"; +"lng_chart_title_view_count_by_source" = "Views by source"; +"lng_chart_title_join_by_source" = "New followers by source"; +"lng_chart_title_language" = "Languages"; +"lng_chart_title_message_interaction" = "Interactions"; +"lng_chart_title_instant_view_interaction" = "IV Interactions"; + +"lng_chart_title_group_join" = "Group members"; +"lng_chart_title_group_join_by_source" = "New members by source"; +"lng_chart_title_group_language" = "Members's primary language"; +"lng_chart_title_group_message_content" = "Messages"; +"lng_chart_title_group_action" = "Actions"; +"lng_chart_title_group_day" = "Views by hours"; +"lng_chart_title_group_week" = "Top days of week"; + +"lng_boosts_title" = "Boosts"; +"lng_boosts_level" = "Level"; +"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_subtext" = "Your channel is currently boosted by these users."; +"lng_boosts_show_more" = "Show More Boosts"; +"lng_boosts_list_status" = "boost expires on {date}"; +"lng_boosts_link_title" = "Link for boosting"; +"lng_boosts_link_subtext" = "Share this link with your subscribers to get more boosts."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/Resources/night-custom-base.tdesktop-theme b/Telegram/Resources/night-custom-base.tdesktop-theme index e651fa9cc..5fc6424ae 100644 Binary files a/Telegram/Resources/night-custom-base.tdesktop-theme and b/Telegram/Resources/night-custom-base.tdesktop-theme differ diff --git a/Telegram/Resources/night-green.tdesktop-theme b/Telegram/Resources/night-green.tdesktop-theme index 1647e4654..a60d844be 100644 Binary files a/Telegram/Resources/night-green.tdesktop-theme and b/Telegram/Resources/night-green.tdesktop-theme differ diff --git a/Telegram/Resources/night.tdesktop-theme b/Telegram/Resources/night.tdesktop-theme index fcb8dc8d6..96f788d13 100644 Binary files a/Telegram/Resources/night.tdesktop-theme and b/Telegram/Resources/night.tdesktop-theme differ diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 28b0c58cc..705b508a2 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -10,5 +10,6 @@ ../../animations/cloud_password/email.tgs ../../animations/ttl.tgs ../../animations/discussion.tgs + ../../animations/stats.tgs diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index a64e3b239..a95a14af9 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.10.4.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 3c262b4b3..c68845489 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,10,2,0 - PRODUCTVERSION 4,10,2,0 + FILEVERSION 4,10,4,0 + PRODUCTVERSION 4,10,4,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "4.10.2.0" + VALUE "FileVersion", "4.10.4.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.10.2.0" + VALUE "ProductVersion", "4.10.4.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 760e5f1b7..de9ad07c2 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,10,2,0 - PRODUCTVERSION 4,10,2,0 + FILEVERSION 4,10,4,0 + PRODUCTVERSION 4,10,4,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "4.10.2.0" + VALUE "FileVersion", "4.10.4.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.10.2.0" + VALUE "ProductVersion", "4.10.4.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp new file mode 100644 index 000000000..1bd5eb314 --- /dev/null +++ b/Telegram/SourceFiles/api/api_statistics.cpp @@ -0,0 +1,570 @@ +/* +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 "api/api_statistics.h" + +#include "apiwrap.h" +#include "data/data_channel.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history.h" +#include "main/main_session.h" +#include "statistics/statistics_data_deserialize.h" + +namespace Api { +namespace { + +[[nodiscard]] Data::StatisticalGraph StatisticalGraphFromTL( + const MTPStatsGraph &tl) { + return tl.match([&](const MTPDstatsGraph &d) { + using namespace Statistic; + const auto zoomToken = d.vzoom_token().has_value() + ? qs(*d.vzoom_token()).toUtf8() + : QByteArray(); + return Data::StatisticalGraph{ + StatisticalChartFromJSON(qs(d.vjson().data().vdata()).toUtf8()), + zoomToken, + }; + }, [&](const MTPDstatsGraphAsync &data) { + return Data::StatisticalGraph{ + .zoomToken = qs(data.vtoken()).toUtf8(), + }; + }, [&](const MTPDstatsGraphError &data) { + return Data::StatisticalGraph{ .error = qs(data.verror()) }; + }); +} + +[[nodiscard]] Data::StatisticalValue StatisticalValueFromTL( + const MTPStatsAbsValueAndPrev &tl) { + const auto current = tl.data().vcurrent().v; + const auto previous = tl.data().vprevious().v; + return Data::StatisticalValue{ + .value = current, + .previousValue = previous, + .growthRatePercentage = previous + ? std::abs((current - previous) / float64(previous) * 100.) + : 0, + }; +} + +[[nodiscard]] Data::ChannelStatistics ChannelStatisticsFromTL( + const MTPDstats_broadcastStats &data) { + const auto &tlUnmuted = data.venabled_notifications().data(); + const auto unmuted = (!tlUnmuted.vtotal().v) + ? 0. + : std::clamp( + tlUnmuted.vpart().v / tlUnmuted.vtotal().v * 100., + 0., + 100.); + using Recent = MTPMessageInteractionCounters; + auto recentMessages = ranges::views::all( + data.vrecent_message_interactions().v + ) | ranges::views::transform([&](const Recent &tl) { + return Data::StatisticsMessageInteractionInfo{ + .messageId = tl.data().vmsg_id().v, + .viewsCount = tl.data().vviews().v, + .forwardsCount = tl.data().vforwards().v, + }; + }) | ranges::to_vector; + + return { + .startDate = data.vperiod().data().vmin_date().v, + .endDate = data.vperiod().data().vmax_date().v, + + .memberCount = StatisticalValueFromTL(data.vfollowers()), + .meanViewCount = StatisticalValueFromTL(data.vviews_per_post()), + .meanShareCount = StatisticalValueFromTL(data.vshares_per_post()), + + .enabledNotificationsPercentage = unmuted, + + .memberCountGraph = StatisticalGraphFromTL( + data.vgrowth_graph()), + + .joinGraph = StatisticalGraphFromTL( + data.vfollowers_graph()), + + .muteGraph = StatisticalGraphFromTL( + data.vmute_graph()), + + .viewCountByHourGraph = StatisticalGraphFromTL( + data.vtop_hours_graph()), + + .viewCountBySourceGraph = StatisticalGraphFromTL( + data.vviews_by_source_graph()), + + .joinBySourceGraph = StatisticalGraphFromTL( + data.vnew_followers_by_source_graph()), + + .languageGraph = StatisticalGraphFromTL( + data.vlanguages_graph()), + + .messageInteractionGraph = StatisticalGraphFromTL( + data.vinteractions_graph()), + + .instantViewInteractionGraph = StatisticalGraphFromTL( + data.viv_interactions_graph()), + + .recentMessageInteractions = std::move(recentMessages), + }; +} + +[[nodiscard]] Data::SupergroupStatistics SupergroupStatisticsFromTL( + const MTPDstats_megagroupStats &data) { + using Senders = MTPStatsGroupTopPoster; + using Administrators = MTPStatsGroupTopAdmin; + using Inviters = MTPStatsGroupTopInviter; + + auto topSenders = ranges::views::all( + data.vtop_posters().v + ) | ranges::views::transform([&](const Senders &tl) { + return Data::StatisticsMessageSenderInfo{ + .userId = UserId(tl.data().vuser_id().v), + .sentMessageCount = tl.data().vmessages().v, + .averageCharacterCount = tl.data().vavg_chars().v, + }; + }) | ranges::to_vector; + auto topAdministrators = ranges::views::all( + data.vtop_admins().v + ) | ranges::views::transform([&](const Administrators &tl) { + return Data::StatisticsAdministratorActionsInfo{ + .userId = UserId(tl.data().vuser_id().v), + .deletedMessageCount = tl.data().vdeleted().v, + .bannedUserCount = tl.data().vkicked().v, + .restrictedUserCount = tl.data().vbanned().v, + }; + }) | ranges::to_vector; + auto topInviters = ranges::views::all( + data.vtop_inviters().v + ) | ranges::views::transform([&](const Inviters &tl) { + return Data::StatisticsInviterInfo{ + .userId = UserId(tl.data().vuser_id().v), + .addedMemberCount = tl.data().vinvitations().v, + }; + }) | ranges::to_vector; + + return { + .startDate = data.vperiod().data().vmin_date().v, + .endDate = data.vperiod().data().vmax_date().v, + + .memberCount = StatisticalValueFromTL(data.vmembers()), + .messageCount = StatisticalValueFromTL(data.vmessages()), + .viewerCount = StatisticalValueFromTL(data.vviewers()), + .senderCount = StatisticalValueFromTL(data.vposters()), + + .memberCountGraph = StatisticalGraphFromTL( + data.vgrowth_graph()), + + .joinGraph = StatisticalGraphFromTL( + data.vmembers_graph()), + + .joinBySourceGraph = StatisticalGraphFromTL( + data.vnew_members_by_source_graph()), + + .languageGraph = StatisticalGraphFromTL( + data.vlanguages_graph()), + + .messageContentGraph = StatisticalGraphFromTL( + data.vmessages_graph()), + + .actionGraph = StatisticalGraphFromTL( + data.vactions_graph()), + + .dayGraph = StatisticalGraphFromTL( + data.vtop_hours_graph()), + + .weekGraph = StatisticalGraphFromTL( + data.vweekdays_graph()), + + .topSenders = std::move(topSenders), + .topAdministrators = std::move(topAdministrators), + .topInviters = std::move(topInviters), + }; +} + +} // namespace + +Statistics::Statistics(not_null api) +: _api(&api->instance()) { +} + +rpl::producer Statistics::request( + not_null peer) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto channel = peer->asChannel(); + if (!channel) { + return lifetime; + } + + if (!channel->isMegagroup()) { + _api.request(MTPstats_GetBroadcastStats( + MTP_flags(MTPstats_GetBroadcastStats::Flags(0)), + channel->inputChannel + )).done([=](const MTPstats_BroadcastStats &result) { + _channelStats = ChannelStatisticsFromTL(result.data()); + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + } else { + _api.request(MTPstats_GetMegagroupStats( + MTP_flags(MTPstats_GetMegagroupStats::Flags(0)), + channel->inputChannel + )).done([=](const MTPstats_MegagroupStats &result) { + _supergroupStats = SupergroupStatisticsFromTL(result.data()); + peer->owner().processUsers(result.data().vusers()); + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + } + + return lifetime; + }; +} + +Statistics::GraphResult Statistics::requestZoom( + not_null peer, + const QString &token, + float64 x) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto channel = peer->asChannel(); + if (!channel) { + return lifetime; + } + const auto wasEmpty = _zoomDeque.empty(); + _zoomDeque.push_back([=] { + _api.request(MTPstats_LoadAsyncGraph( + MTP_flags(x + ? MTPstats_LoadAsyncGraph::Flag::f_x + : MTPstats_LoadAsyncGraph::Flag(0)), + MTP_string(token), + MTP_long(x) + )).done([=](const MTPStatsGraph &result) { + consumer.put_next(StatisticalGraphFromTL(result)); + consumer.put_done(); + if (!_zoomDeque.empty()) { + _zoomDeque.pop_front(); + if (!_zoomDeque.empty()) { + _zoomDeque.front()(); + } + } + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + }); + if (wasEmpty) { + _zoomDeque.front()(); + } + + return lifetime; + }; +} + +Statistics::GraphResult Statistics::requestMessage( + not_null peer, + MsgId msgId) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto channel = peer->asChannel(); + if (!channel) { + return lifetime; + } + + _api.request(MTPstats_GetMessageStats( + MTP_flags(MTPstats_GetMessageStats::Flags(0)), + channel->inputChannel, + MTP_int(msgId.bare) + )).done([=](const MTPstats_MessageStats &result) { + consumer.put_next( + StatisticalGraphFromTL(result.data().vviews_graph())); + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + + return lifetime; + }; +} + +Data::ChannelStatistics Statistics::channelStats() const { + return _channelStats; +} + +Data::SupergroupStatistics Statistics::supergroupStats() const { + return _supergroupStats; +} + +PublicForwards::PublicForwards( + not_null channel, + FullMsgId fullId) +: _channel(channel) +, _fullId(fullId) +, _api(&channel->session().api().instance()) { +} + +void PublicForwards::request( + const Data::PublicForwardsSlice::OffsetToken &token, + Fn done) { + if (_requestId) { + return; + } + const auto offsetPeer = _channel->owner().peer(token.fullId.peer); + const auto tlOffsetPeer = offsetPeer + ? offsetPeer->input + : MTP_inputPeerEmpty(); + constexpr auto kLimit = tl::make_int(100); + _requestId = _api.request(MTPstats_GetMessagePublicForwards( + _channel->inputChannel, + MTP_int(_fullId.msg), + MTP_int(token.rate), + tlOffsetPeer, + MTP_int(token.fullId.msg), + kLimit + )).done([=, channel = _channel](const MTPmessages_Messages &result) { + using Messages = QVector; + _requestId = 0; + + auto nextToken = Data::PublicForwardsSlice::OffsetToken(); + const auto process = [&](const MTPVector &messages) { + auto result = Messages(); + for (const auto &message : messages.v) { + const auto msgId = IdFromMessage(message); + const auto peerId = PeerFromMessage(message); + const auto lastDate = DateFromMessage(message); + if (const auto peer = channel->owner().peerLoaded(peerId)) { + if (lastDate) { + channel->owner().addNewMessage( + message, + MessageFlags(), + NewMessageType::Existing); + nextToken.fullId = { peerId, msgId }; + result.push_back(nextToken.fullId); + } + } + } + return result; + }; + + auto allLoaded = false; + auto fullCount = 0; + auto messages = result.match([&](const MTPDmessages_messages &data) { + channel->owner().processUsers(data.vusers()); + channel->owner().processChats(data.vchats()); + auto list = process(data.vmessages()); + allLoaded = true; + fullCount = list.size(); + return list; + }, [&](const MTPDmessages_messagesSlice &data) { + channel->owner().processUsers(data.vusers()); + channel->owner().processChats(data.vchats()); + auto list = process(data.vmessages()); + + if (const auto nextRate = data.vnext_rate()) { + const auto rateUpdated = (nextRate->v != token.rate); + if (rateUpdated) { + nextToken.rate = nextRate->v; + } else { + allLoaded = true; + } + } + fullCount = data.vcount().v; + return list; + }, [&](const MTPDmessages_channelMessages &data) { + channel->owner().processUsers(data.vusers()); + channel->owner().processChats(data.vchats()); + auto list = process(data.vmessages()); + allLoaded = true; + fullCount = data.vcount().v; + return list; + }, [&](const MTPDmessages_messagesNotModified &) { + allLoaded = true; + return Messages(); + }); + + _lastTotal = std::max(_lastTotal, fullCount); + done({ + .list = std::move(messages), + .total = _lastTotal, + .allLoaded = allLoaded, + .token = nextToken, + }); + }).fail([=] { + _requestId = 0; + }).send(); +} + +MessageStatistics::MessageStatistics( + not_null channel, + FullMsgId fullId) +: _publicForwards(channel, fullId) +, _channel(channel) +, _fullId(fullId) +, _api(&channel->session().api().instance()) { +} + +Data::PublicForwardsSlice MessageStatistics::firstSlice() const { + return _firstSlice; +} + +void MessageStatistics::request(Fn done) { + if (_channel->isMegagroup()) { + return; + } + + const auto requestFirstPublicForwards = [=]( + const Data::StatisticalGraph &messageGraph, + const Data::StatisticsMessageInteractionInfo &info) { + _publicForwards.request({}, [=](Data::PublicForwardsSlice slice) { + const auto total = slice.total; + _firstSlice = std::move(slice); + done({ + .messageInteractionGraph = messageGraph, + .publicForwards = total, + .privateForwards = info.forwardsCount - total, + .views = info.viewsCount, + }); + }); + }; + + const auto requestPrivateForwards = [=]( + const Data::StatisticalGraph &messageGraph) { + _api.request(MTPstats_GetBroadcastStats( + MTP_flags(MTPstats_GetBroadcastStats::Flags(0)), + _channel->inputChannel + )).done([=](const MTPstats_BroadcastStats &result) { + const auto channelStats = ChannelStatisticsFromTL(result.data()); + auto info = Data::StatisticsMessageInteractionInfo(); + for (const auto &r : channelStats.recentMessageInteractions) { + if (r.messageId == _fullId.msg) { + info = r; + break; + } + } + requestFirstPublicForwards(messageGraph, info); + }).fail([=](const MTP::Error &error) { + requestFirstPublicForwards(messageGraph, {}); + }).send(); + }; + + _api.request(MTPstats_GetMessageStats( + MTP_flags(MTPstats_GetMessageStats::Flags(0)), + _channel->inputChannel, + MTP_int(_fullId.msg.bare) + )).done([=](const MTPstats_MessageStats &result) { + requestPrivateForwards( + StatisticalGraphFromTL(result.data().vviews_graph())); + }).fail([=](const MTP::Error &error) { + requestPrivateForwards({}); + }).send(); + +} + +Boosts::Boosts(not_null peer) +: _peer(peer) +, _api(&peer->session().api().instance()) { +} + +rpl::producer Boosts::request() { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto channel = _peer->asChannel(); + if (!channel || channel->isMegagroup()) { + return lifetime; + } + + _api.request(MTPstories_GetBoostsStatus( + _peer->input + )).done([=](const MTPstories_BoostsStatus &result) { + const auto &data = result.data(); + const auto hasPremium = !!data.vpremium_audience(); + const auto premiumMemberCount = hasPremium + ? std::max(0, int(data.vpremium_audience()->data().vpart().v)) + : 0; + const auto participantCount = hasPremium + ? std::max( + int(data.vpremium_audience()->data().vtotal().v), + premiumMemberCount) + : 0; + const auto premiumMemberPercentage = (participantCount > 0) + ? (100. * premiumMemberCount / participantCount) + : 0; + + _boostStatus.overview = Data::BoostsOverview{ + .isBoosted = data.is_my_boost(), + .level = std::max(data.vlevel().v, 0), + .boostCount = std::max( + data.vboosts().v, + data.vcurrent_level_boosts().v), + .currentLevelBoostCount = data.vcurrent_level_boosts().v, + .nextLevelBoostCount = data.vnext_level_boosts() + ? data.vnext_level_boosts()->v + : 0, + .premiumMemberCount = premiumMemberCount, + .premiumMemberPercentage = premiumMemberPercentage, + }; + _boostStatus.link = qs(data.vboost_url()); + + requestBoosts({}, [=](Data::BoostsListSlice &&slice) { + _boostStatus.firstSlice = std::move(slice); + consumer.put_done(); + }); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + + return lifetime; + }; +} + +void Boosts::requestBoosts( + const Data::BoostsListSlice::OffsetToken &token, + Fn done) { + if (_requestId) { + return; + } + constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice); + constexpr auto kTlLimit = tl::make_int(kLimit); + _requestId = _api.request(MTPstories_GetBoostersList( + _peer->input, + MTP_string(token.next), + token.next.isEmpty() ? kTlFirstSlice : kTlLimit + )).done([=](const MTPstories_BoostersList &result) { + _requestId = 0; + + const auto &data = result.data(); + _peer->owner().processUsers(data.vusers()); + + auto list = std::vector(); + list.reserve(data.vboosters().v.size()); + for (const auto &boost : data.vboosters().v) { + list.push_back({ + boost.data().vuser_id().v, + QDateTime::fromSecsSinceEpoch(boost.data().vexpires().v), + }); + } + done(Data::BoostsListSlice{ + .list = std::move(list), + .total = data.vcount().v, + .allLoaded = (data.vcount().v == data.vboosters().v.size()), + .token = Data::BoostsListSlice::OffsetToken{ + data.vnext_offset() + ? qs(*data.vnext_offset()) + : QString() + }, + }); + }).fail([=] { + _requestId = 0; + }).send(); +} + +Data::BoostStatus Boosts::boostStatus() const { + return _boostStatus; +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_statistics.h b/Telegram/SourceFiles/api/api_statistics.h new file mode 100644 index 000000000..460c9df9d --- /dev/null +++ b/Telegram/SourceFiles/api/api_statistics.h @@ -0,0 +1,110 @@ +/* +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 "data/data_boosts.h" +#include "data/data_statistics.h" +#include "mtproto/sender.h" + +class ApiWrap; +class ChannelData; +class PeerData; + +namespace Api { + +class Statistics final { +public: + explicit Statistics(not_null api); + + [[nodiscard]] rpl::producer request( + not_null peer); + using GraphResult = rpl::producer; + [[nodiscard]] GraphResult requestZoom( + not_null peer, + const QString &token, + float64 x); + [[nodiscard]] GraphResult requestMessage( + not_null peer, + MsgId msgId); + + [[nodiscard]] Data::ChannelStatistics channelStats() const; + [[nodiscard]] Data::SupergroupStatistics supergroupStats() const; + +private: + Data::ChannelStatistics _channelStats; + Data::SupergroupStatistics _supergroupStats; + MTP::Sender _api; + + std::deque> _zoomDeque; + +}; + +class PublicForwards final { +public: + explicit PublicForwards(not_null channel, FullMsgId fullId); + + void request( + const Data::PublicForwardsSlice::OffsetToken &token, + Fn done); + +private: + const not_null _channel; + const FullMsgId _fullId; + mtpRequestId _requestId = 0; + int _lastTotal = 0; + + MTP::Sender _api; + +}; + +class MessageStatistics final { +public: + explicit MessageStatistics( + not_null channel, + FullMsgId fullId); + + void request(Fn done); + + [[nodiscard]] Data::PublicForwardsSlice firstSlice() const; + +private: + PublicForwards _publicForwards; + const not_null _channel; + const FullMsgId _fullId; + + Data::PublicForwardsSlice _firstSlice; + + mtpRequestId _requestId = 0; + MTP::Sender _api; + +}; + +class Boosts final { +public: + explicit Boosts(not_null peer); + + [[nodiscard]] rpl::producer request(); + void requestBoosts( + const Data::BoostsListSlice::OffsetToken &token, + Fn done); + + [[nodiscard]] Data::BoostStatus boostStatus() const; + + static constexpr auto kFirstSlice = int(10); + static constexpr auto kLimit = int(40); + +private: + const not_null _peer; + Data::BoostStatus _boostStatus; + + MTP::Sender _api; + mtpRequestId _requestId = 0; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index b952e5a6e..7cf49f4f8 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -85,8 +85,6 @@ confirmInviteTitle: FlatLabel(defaultFlatLabel) { textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { font: font(18px semibold); - linkFont: font(18px semibold); - linkFontOver: font(18px semibold underline); } } confirmInviteAbout: FlatLabel(boxLabel) { @@ -143,8 +141,6 @@ contactsPadding: margins(16px, 7px, 16px, 7px); contactsNameTop: 2px; contactsNameStyle: TextStyle(defaultTextStyle) { font: semiboldFont; - linkFont: semiboldFont; - linkFontOver: semiboldFont; } contactsStatusTop: 23px; contactsStatusFont: font(fsize); @@ -199,8 +195,6 @@ localStorageRowTitle: FlatLabel(defaultFlatLabel) { maxHeight: 20px; style: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold); } } localStorageRowSize: FlatLabel(defaultFlatLabel) { @@ -208,8 +202,6 @@ localStorageRowSize: FlatLabel(defaultFlatLabel) { maxHeight: 20px; style: TextStyle(defaultTextStyle) { font: font(14px); - linkFont: font(14px); - linkFontOver: font(14px); } } localStorageClear: defaultBoxButton; @@ -228,8 +220,6 @@ sharePhotoTop: 6px; shareBoxListItem: PeerListItem(defaultPeerListItem) { nameStyle: TextStyle(defaultTextStyle) { font: font(11px); - linkFont: font(11px); - linkFontOver: font(11px); } nameFg: windowFg; nameFgChecked: windowActiveTextFg; @@ -537,8 +527,6 @@ adminLogFilterLittleSkip: 16px; adminLogFilterCheckbox: Checkbox(defaultBoxCheckbox) { style: TextStyle(boxTextStyle) { font: font(boxFontSize semibold); - linkFont: font(boxFontSize semibold); - linkFontOver: font(boxFontSize semibold underline); } } adminLogFilterSkip: 32px; @@ -580,16 +568,12 @@ rightsPhotoButton: UserpicButton(defaultUserpicButton) { rightsPhotoMargin: margins(20px, 0px, 15px, 18px); rightsNameStyle: TextStyle(semiboldTextStyle) { font: font(15px semibold); - linkFont: font(15px semibold); - linkFontOver: font(15px semibold underline); } rightsNameTop: 8px; rightsStatusTop: 32px; rightsHeaderLabel: FlatLabel(boxLabel) { style: TextStyle(semiboldTextStyle) { font: font(boxFontSize semibold); - linkFont: font(boxFontSize semibold); - linkFontOver: font(boxFontSize semibold underline); } textFg: windowActiveTextFg; } @@ -623,8 +607,6 @@ proxyRowTitlePalette: TextPalette(defaultTextPalette) { } proxyRowTitleStyle: TextStyle(defaultTextStyle) { font: semiboldFont; - linkFont: normalFont; - linkFontOver: normalFont; } proxyRowStatusFg: windowSubTextFg; proxyRowStatusFgOnline: windowActiveTextFg; @@ -807,8 +789,6 @@ pollResultsQuestion: FlatLabel(defaultFlatLabel) { textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { font: font(16px semibold); - linkFont: font(16px semibold); - linkFontOver: font(16px semibold underline); } } pollResultsVotesCount: FlatLabel(defaultFlatLabel) { @@ -837,8 +817,6 @@ inviteViaLinkButton: SettingsButton(defaultSettingsButton) { style: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold underline); } height: 20px; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 3274e4eeb..ef53b2d62 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -194,7 +194,7 @@ not_null CreateWarningLabel( if (value >= 0) { result->setText(QString::number(value)); } else { - result->setMarkedText(Ui::Text::PlainLink( + result->setMarkedText(Ui::Text::Colorized( QString::number(value))); } result->setVisible(shown); diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 874e61e23..3e2c3abcd 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -170,7 +170,7 @@ void PeerListRowWithLink::rightActionPaint( int outerWidth, bool selected, bool actionSelected) { - p.setFont(actionSelected ? st::linkOverFont : st::linkFont); + p.setFont(actionSelected ? st::linkFontOver : st::linkFont); p.setPen(actionSelected ? st::defaultLinkButton.overColor : st::defaultLinkButton.color); p.drawTextLeft(x, y, outerWidth, _action, _actionWidth); } diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 2791724dd..a8d3b5600 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -97,8 +97,6 @@ callButtonLabel: FlatLabel(defaultFlatLabel) { textFg: callNameFg; style: TextStyle(defaultTextStyle) { font: font(11px); - linkFont: font(11px); - linkFontOver: font(11px underline); } } @@ -218,8 +216,6 @@ callMuteButtonLabel: FlatLabel(defaultFlatLabel) { textFg: groupCallMembersFg; style: TextStyle(defaultTextStyle) { font: font(14px); - linkFont: font(14px); - linkFontOver: font(14px underline); } } callMuteButtonActiveInner: IconButton { @@ -294,8 +290,6 @@ callName: FlatLabel(defaultFlatLabel) { align: align(top); style: TextStyle(defaultTextStyle) { font: font(21px semibold); - linkFont: font(21px semibold); - linkFontOver: font(21px semibold underline); } } callStatus: FlatLabel(defaultFlatLabel) { @@ -305,8 +299,6 @@ callStatus: FlatLabel(defaultFlatLabel) { align: align(top); style: TextStyle(defaultTextStyle) { font: font(14px); - linkFont: font(14px); - linkFontOver: font(14px underline); } } callRemoteAudioMute: FlatLabel(callStatus) { @@ -314,8 +306,6 @@ callRemoteAudioMute: FlatLabel(callStatus) { textFg: videoPlayIconFg; style: TextStyle(defaultTextStyle) { font: font(12px); - linkFont: font(12px); - linkFontOver: font(12px underline); } } callRemoteAudioMuteSkip: 12px; @@ -746,8 +736,6 @@ groupCallShareBoxList: PeerList(groupCallMembersList) { item: PeerListItem(groupCallMembersListItem) { nameStyle: TextStyle(defaultTextStyle) { font: font(11px); - linkFont: font(11px); - linkFontOver: font(11px); } checkbox: RoundImageCheckbox(groupCallMembersListCheckbox) { imageRadius: 28px; @@ -784,8 +772,6 @@ groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) { textFg: groupCallMembersFg; style: TextStyle(defaultTextStyle) { font: font(semibold 14px); - linkFont: font(semibold 14px); - linkFontOver: font(semibold 14px); } } groupCallTitleSeparator: 4px; @@ -1203,8 +1189,6 @@ callTopBarMuteCrossLine: CrossLineAnimation { groupCallStartsIn: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { font: font(20px semibold); - linkFont: font(20px semibold); - linkFontOver: font(20px semibold underline); } textFg: groupCallMembersFg; } diff --git a/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp b/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp index a5c15c8fa..708cf3cde 100644 --- a/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp +++ b/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_calls.h" #include +#include namespace Calls::Group::Ui { diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 20b1d6287..81ff92cf0 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -509,8 +509,6 @@ emojiPanColorAllLabel: FlatLabel(defaultFlatLabel) { minWidth: 40px; style: TextStyle(defaultTextStyle) { font: font(12px); - linkFont: font(12px); - linkFontOver: font(12px); } } emojiPanColorAllPadding: margins(10px, 6px, 10px, -1px); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index a9c0fd789..b988a9b3f 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -787,7 +787,7 @@ void MessageLinksParser::parse() { } offset = matchOffset = p - start; } - processTagsBefore(QFIXED_MAX); + processTagsBefore(Ui::kQFixedMax); apply(text, ranges); } diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index ba10ed476..ca4e21ba2 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -512,14 +512,16 @@ void Application::startMediaView() { InvokeQueued(this, [=] { _mediaView = std::make_unique(); }); -#else // Q_OS_MAC +#elif defined Q_OS_WIN // Q_OS_MAC || Q_OS_WIN // On Windows we needed such hack for the main window, otherwise // somewhere inside the media viewer creating code its geometry // was broken / lost to some invalid values. const auto current = _lastActivePrimaryWindow->widget()->geometry(); _mediaView = std::make_unique(); _lastActivePrimaryWindow->widget()->Ui::RpWidget::setGeometry(current); -#endif // Q_OS_MAC +#else + _mediaView = std::make_unique(); +#endif // Q_OS_MAC || Q_OS_WIN } void Application::startTray() { diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index 09810abfd..2d0d52fe0 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -30,6 +30,16 @@ std::map BetaLogs() { "- Fix memory leak in Direct3D 11 media viewer on Windows.\n" }, + { + 4010004, + "- Statistics in channels and group chats.\n" + + "- Nice looking code blocks with syntax highlight.\n" + + "- Copy full code block by click on its header.\n" + + "- Send a highlighted code block using ```language syntax.\n" + } }; }; diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index ef566e64a..82d93ec28 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -94,9 +94,10 @@ const auto CommandByName = base::flat_map{ { u"read_chat"_q , Command::ReadChat }, // Shortcuts that have no default values. - { u"message"_q , Command::JustSendMessage }, - { u"message_silently"_q , Command::SendSilentMessage }, - { u"message_scheduled"_q , Command::ScheduleMessage }, + { u"message"_q , Command::JustSendMessage }, + { u"message_silently"_q , Command::SendSilentMessage }, + { u"message_scheduled"_q , Command::ScheduleMessage }, + { u"mevia_viewer_video_fullscreen"_q , Command::MediaViewerFullscreen }, // }; diff --git a/Telegram/SourceFiles/core/shortcuts.h b/Telegram/SourceFiles/core/shortcuts.h index 6e1904c68..d7b3dc3c5 100644 --- a/Telegram/SourceFiles/core/shortcuts.h +++ b/Telegram/SourceFiles/core/shortcuts.h @@ -59,6 +59,8 @@ enum class Command { ReadChat, + MediaViewerFullscreen, + SupportReloadTemplates, SupportToggleMuted, SupportScrollToCurrent, diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 8f1be8324..1fe33e654 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -243,6 +243,16 @@ bool UiIntegration::handleUrlClick( return true; } +bool UiIntegration::copyPreOnClick(const QVariant &context) { + const auto my = context.value(); + if (const auto window = my.sessionWindow.get()) { + window->showToast(tr::lng_code_copied(tr::now)); + } else if (my.show) { + my.show->showToast(tr::lng_code_copied(tr::now)); + } + return true; +} + std::unique_ptr UiIntegration::createCustomEmoji( const QString &data, const std::any &context) { diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index cd3e4ff58..0515c732f 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -53,6 +53,7 @@ public: bool handleUrlClick( const QString &url, const QVariant &context) override; + bool copyPreOnClick(const QVariant &context) override; rpl::producer<> forcePopupMenuHideRequests() override; const Ui::Emoji::One *defaultEmojiVariant( const Ui::Emoji::One *emoji) override; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index ea54c2b39..7ce8a8b5c 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 4010002; -constexpr auto AppVersionStr = "4.10.2"; -constexpr auto AppBetaVersion = false; +constexpr auto AppVersion = 4010004; +constexpr auto AppVersionStr = "4.10.4"; +constexpr auto AppBetaVersion = true; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/countries/countries_instance.cpp b/Telegram/SourceFiles/countries/countries_instance.cpp index 36af0ce12..0a7844179 100644 --- a/Telegram/SourceFiles/countries/countries_instance.cpp +++ b/Telegram/SourceFiles/countries/countries_instance.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "countries/countries_instance.h" #include "base/qt/qt_common_adapters.h" +#include "base/qt/qt_string_view.h" namespace Countries { namespace { diff --git a/Telegram/SourceFiles/data/data_boosts.h b/Telegram/SourceFiles/data/data_boosts.h new file mode 100644 index 000000000..6ad253169 --- /dev/null +++ b/Telegram/SourceFiles/data/data_boosts.h @@ -0,0 +1,43 @@ +/* +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 Data { + +struct BoostsOverview final { + bool isBoosted = false; + int level = 0; + int boostCount = 0; + int currentLevelBoostCount = 0; + int nextLevelBoostCount = 0; + int premiumMemberCount = 0; + float64 premiumMemberPercentage = 0; +}; + +struct Boost final { + UserId userId = UserId(0); + QDateTime expirationDate; +}; + +struct BoostsListSlice final { + struct OffsetToken final { + QString next; + }; + std::vector list; + int total = 0; + bool allLoaded = false; + OffsetToken token; +}; + +struct BoostStatus final { + BoostsOverview overview; + BoostsListSlice firstSlice; + QString link; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index cf266c5a2..3b26c1eb9 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -997,7 +997,8 @@ void ApplyChannelUpdate( | Flag::PreHistoryHidden | Flag::AntiSpam | Flag::Location - | Flag::ParticipantsHidden; + | Flag::ParticipantsHidden + | Flag::CanGetStatistics; channel->setFlags((channel->flags() & ~mask) | (update.is_can_set_username() ? Flag::CanSetUsername : Flag()) | (update.is_can_view_participants() @@ -1009,7 +1010,8 @@ void ApplyChannelUpdate( | (update.vlocation() ? Flag::Location : Flag()) | (update.is_participants_hidden() ? Flag::ParticipantsHidden - : Flag())); + : Flag()) + | (update.is_can_view_stats() ? Flag::CanGetStatistics : Flag())); channel->setUserpicPhoto(update.vchat_photo()); if (const auto migratedFrom = update.vmigrated_from_chat_id()) { channel->addFlags(Flag::Megagroup); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index ee0550a6d..bdf0f4205 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -62,6 +62,7 @@ enum class ChannelDataFlag { StoriesHidden = (1 << 26), HasActiveStories = (1 << 27), HasUnreadStories = (1 << 28), + CanGetStatistics = (1 << 29), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index 0a6d64122..00c9ba02d 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -69,7 +69,7 @@ void ApplyPeerCloudDraft( textWithTags, replyTo, topicRootId, - MessageCursor(QFIXED_MAX, QFIXED_MAX, QFIXED_MAX), + MessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax), (draft.is_no_webpage() ? Data::PreviewState::Cancelled : Data::PreviewState::Allowed)); diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index 743487c85..bbd4ce987 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -75,7 +75,7 @@ constexpr auto kShowChatNamesCount = 8; .entities = (history->chatListBadgesState().unread ? EntitiesInText{ { EntityType::Semibold, 0, int(name.size()), QString() }, - { EntityType::PlainLink, 0, int(name.size()), QString() }, + { EntityType::Colorized, 0, int(name.size()), QString() }, } : EntitiesInText{}), }; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 7a36a5515..8d87fb77a 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -85,7 +85,7 @@ using ItemPreviewImage = HistoryView::ItemPreviewImage; const TextWithEntities &caption, bool hasMiniImages = false) { if (caption.text.isEmpty()) { - return Ui::Text::PlainLink(attachType); + return Ui::Text::Colorized(attachType); } return hasMiniImages @@ -96,7 +96,7 @@ using ItemPreviewImage = HistoryView::ItemPreviewImage; tr::lng_dialogs_text_media_wrapped( tr::now, lt_media, - Ui::Text::PlainLink(attachType), + Ui::Text::Colorized(attachType), Ui::Text::WithEntities), lt_caption, caption, @@ -558,7 +558,7 @@ ItemPreview Media::toGroupPreview( : fileCount ? tr::lng_in_dlg_file_count(tr::now, lt_count, fileCount) : tr::lng_in_dlg_album(tr::now); - result.text = Ui::Text::PlainLink(genericText); + result.text = Ui::Text::Colorized(genericText); } if (!loadingContext.empty()) { result.loadingContext = std::move(loadingContext); @@ -937,7 +937,7 @@ TextWithEntities MediaFile::notificationText() const { const auto text = _emoji.isEmpty() ? tr::lng_in_dlg_sticker(tr::now) : tr::lng_in_dlg_sticker_emoji(tr::now, lt_emoji, _emoji); - return Ui::Text::PlainLink(text); + return Ui::Text::Colorized(text); } const auto type = [&] { if (_document->isVideoMessage()) { @@ -1719,7 +1719,11 @@ PollData *MediaPoll::poll() const { } TextWithEntities MediaPoll::notificationText() const { - return Ui::Text::PlainLink(_poll->question); + return TextWithEntities() + .append(QChar(0xD83D)) + .append(QChar(0xDCCA)) + .append(QChar(' ')) + .append(Ui::Text::Colorized(_poll->question)); } QString MediaPoll::pinnedTextSubstring() const { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 091818594..f95fdc3ca 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -80,6 +80,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "base/call_delayed.h" #include "base/random.h" +#include "spellcheck/spellcheck_highlight_syntax.h" #include "styles/style_boxes.h" // st::backgroundSize // AyuGram includes @@ -306,6 +307,11 @@ Session::Session(not_null session) } }, _lifetime); + Spellchecker::HighlightReady( + ) | rpl::start_with_next([=](uint64 processId) { + highlightProcessDone(processId); + }, _lifetime); + subscribeForTopicRepliesLists(); crl::on_main(_session, [=] { @@ -1771,6 +1777,27 @@ void Session::requestItemTextRefresh(not_null item) { } } +void Session::registerHighlightProcess( + uint64 processId, + not_null item) { + Expects(item->inHighlightProcess()); + + const auto [i, ok] = _highlightings.emplace(processId, item); + + Ensures(ok); +} + +void Session::highlightProcessDone(uint64 processId) { + if (const auto done = _highlightings.take(processId)) { + for (const auto &[id, item] : _highlightings) { + if (item == *done) { + return; + } + } + (*done)->highlightProcessDone(); + } +} + void Session::requestUnreadReactionsAnimation(not_null item) { enumerateItemViews(item, [&](not_null view) { view->animateUnreadReactions(); @@ -2496,6 +2523,13 @@ void Session::unregisterMessage(not_null item) { Data::MessageUpdate::Flag::Destroyed); groups().unregisterMessage(item); removeDependencyMessage(item); + for (auto i = begin(_highlightings); i != end(_highlightings);) { + if (i->second == item) { + i = _highlightings.erase(i); + } else { + ++i; + } + } messagesListForInsert(peerId)->erase(itemId); if (!peerIsChannel(peerId) && IsServerMsgId(itemId)) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 9ba13d1a9..86cb08c6f 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -299,6 +299,10 @@ public: void notifyPinnedDialogsOrderUpdated(); [[nodiscard]] rpl::producer<> pinnedDialogsOrderUpdated() const; + void registerHighlightProcess( + uint64 processId, + not_null item); + void registerHeavyViewPart(not_null view); void unregisterHeavyViewPart(not_null view); void unloadHeavyViewParts( @@ -845,6 +849,7 @@ private: TimeId date); void setWallpapers(const QVector &data, uint64 hash); + void highlightProcessDone(uint64 processId); void checkPollsClosings(); @@ -955,6 +960,7 @@ private: std::unordered_map< FullStoryId, base::flat_set>> _storyItems; + base::flat_map> _highlightings; base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; diff --git a/Telegram/SourceFiles/data/data_statistics.h b/Telegram/SourceFiles/data/data_statistics.h new file mode 100644 index 000000000..ce59e4d58 --- /dev/null +++ b/Telegram/SourceFiles/data/data_statistics.h @@ -0,0 +1,133 @@ +/* +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 "data/data_statistics_chart.h" + +namespace Data { + +struct StatisticsMessageInteractionInfo final { + MsgId messageId; + int viewsCount = 0; + int forwardsCount = 0; +}; + +struct StatisticsMessageSenderInfo final { + UserId userId = UserId(0); + int sentMessageCount = 0; + int averageCharacterCount = 0; +}; + +struct StatisticsAdministratorActionsInfo final { + UserId userId = UserId(0); + int deletedMessageCount = 0; + int bannedUserCount = 0; + int restrictedUserCount = 0; +}; + +struct StatisticsInviterInfo final { + UserId userId = UserId(0); + int addedMemberCount = 0; +}; + +struct StatisticalValue final { + float64 value = 0.; + float64 previousValue = 0.; + float64 growthRatePercentage = 0.; +}; + +struct ChannelStatistics final { + [[nodiscard]] bool empty() const { + return !startDate || !endDate; + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + + int startDate = 0; + int endDate = 0; + + StatisticalValue memberCount; + StatisticalValue meanViewCount; + StatisticalValue meanShareCount; + + float64 enabledNotificationsPercentage = 0.; + + StatisticalGraph memberCountGraph; + StatisticalGraph joinGraph; + StatisticalGraph muteGraph; + StatisticalGraph viewCountByHourGraph; + StatisticalGraph viewCountBySourceGraph; + StatisticalGraph joinBySourceGraph; + StatisticalGraph languageGraph; + StatisticalGraph messageInteractionGraph; + StatisticalGraph instantViewInteractionGraph; + + std::vector recentMessageInteractions; + +}; + +struct SupergroupStatistics final { + [[nodiscard]] bool empty() const { + return !startDate || !endDate; + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + + int startDate = 0; + int endDate = 0; + + StatisticalValue memberCount; + StatisticalValue messageCount; + StatisticalValue viewerCount; + StatisticalValue senderCount; + + StatisticalGraph memberCountGraph; + StatisticalGraph joinGraph; + StatisticalGraph joinBySourceGraph; + StatisticalGraph languageGraph; + StatisticalGraph messageContentGraph; + StatisticalGraph actionGraph; + StatisticalGraph dayGraph; + StatisticalGraph weekGraph; + + std::vector topSenders; + std::vector topAdministrators; + std::vector topInviters; + +}; + +struct MessageStatistics final { + explicit operator bool() const { + return !messageInteractionGraph.chart.empty() || views; + } + Data::StatisticalGraph messageInteractionGraph; + int publicForwards = 0; + int privateForwards = 0; + int views = 0; +}; + +struct AnyStatistics final { + Data::ChannelStatistics channel; + Data::SupergroupStatistics supergroup; + Data::MessageStatistics message; +}; + +struct PublicForwardsSlice final { + struct OffsetToken final { + int rate = 0; + FullMsgId fullId; + }; + QVector list; + int total = 0; + bool allLoaded = false; + OffsetToken token; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_statistics_chart.cpp b/Telegram/SourceFiles/data/data_statistics_chart.cpp new file mode 100644 index 000000000..3014bf04f --- /dev/null +++ b/Telegram/SourceFiles/data/data_statistics_chart.cpp @@ -0,0 +1,156 @@ +/* +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 "data/data_statistics_chart.h" + +#include +#include + +namespace Data { + +void StatisticalChart::measure() { + if (x.empty()) { + return; + } + const auto n = x.size(); + const auto start = x.front(); + const auto end = x.back(); + + xPercentage.clear(); + xPercentage.resize(n); + if (n == 1) { + xPercentage[0] = 1; + } else { + for (auto i = 0; i < n; i++) { + xPercentage[i] = (x[i] - start) / float64(end - start); + } + } + + for (auto &line : lines) { + if (line.maxValue > maxValue) { + maxValue = line.maxValue; + } + if (line.minValue < minValue) { + minValue = line.minValue; + } + line.segmentTree = Statistic::SegmentTree(line.y); + } + + daysLookup.clear(); + const auto dateCount = int((end - start) / timeStep) + 10; + daysLookup.reserve(dateCount); + constexpr auto kOneDay = 3600 * 24 * 1000; + const auto formatter = u"d MMM"_q; + for (auto i = 0; i < dateCount; i++) { + const auto r = (start + (i * timeStep)) / 1000; + const auto dateTime = QDateTime::fromSecsSinceEpoch(r); + if (timeStep == 1) { + daysLookup.push_back( + QString(((i < 10) ? u"0%1:00"_q : u"%1:00"_q).arg(i))); + } else if (timeStep < kOneDay) { + daysLookup.push_back(u"%1:%2"_q + .arg(dateTime.time().hour(), 2, 10, QChar('0')) + .arg(dateTime.time().minute(), 2, 10, QChar('0'))); + } else { + const auto date = dateTime.date(); + daysLookup.push_back(QLocale().toString(date, formatter)); + } + } + + oneDayPercentage = timeStep / float64(end - start); +} + +QString StatisticalChart::getDayString(int i) const { + return daysLookup[int((x[i] - x[0]) / timeStep)]; +} + +int StatisticalChart::findStartIndex(float64 v) const { + if (!v) { + return 0; + } + const auto n = int(xPercentage.size()); + + if (n < 2) { + return 0; + } + auto left = 0; + auto right = n - 1; + + while (left <= right) { + const auto middle = (right + left) >> 1; + if (v < xPercentage[middle] + && (!middle || (v > xPercentage[middle - 1]))) { + return middle; + } + if (v == xPercentage[middle]) { + return middle; + } + if (v < xPercentage[middle]) { + right = middle - 1; + } else if (v > xPercentage[middle]) { + left = middle + 1; + } + } + return left; +} + +int StatisticalChart::findEndIndex(int left, float64 v) const { + const auto n = int(xPercentage.size()); + if (v == 1.) { + return n - 1; + } + auto right = n - 1; + + while (left <= right) { + const auto middle = (right + left) >> 1; + if (v > xPercentage[middle] + && ((middle == n - 1) || (v < xPercentage[middle + 1]))) { + return middle; + } + if (v == xPercentage[middle]) { + return middle; + } + if (v < xPercentage[middle]) { + right = middle - 1; + } else if (v > xPercentage[middle]) { + left = middle + 1; + } + } + return right; +} + + +int StatisticalChart::findIndex(int left, int right, float64 v) const { + const auto n = int(xPercentage.size()); + + if (v <= xPercentage[left]) { + return left; + } + if (v >= xPercentage[right]) { + return right; + } + + while (left <= right) { + const auto middle = (right + left) >> 1; + if (v > xPercentage[middle] + && ((middle == n - 1) || (v < xPercentage[middle + 1]))) { + return middle; + } + + if (v == xPercentage[middle]) { + return middle; + } + if (v < xPercentage[middle]) { + right = middle - 1; + } else if (v > xPercentage[middle]) { + left = middle + 1; + } + } + return right; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_statistics_chart.h b/Telegram/SourceFiles/data/data_statistics_chart.h new file mode 100644 index 000000000..9fd644cd7 --- /dev/null +++ b/Telegram/SourceFiles/data/data_statistics_chart.h @@ -0,0 +1,77 @@ +/* +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 "statistics/segment_tree.h" + +namespace Data { + +struct StatisticalChart { + StatisticalChart() = default; + + [[nodiscard]] bool empty() const { + return lines.empty(); + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + + void measure(); + + [[nodiscard]] QString getDayString(int i) const; + + [[nodiscard]] int findStartIndex(float64 v) const; + [[nodiscard]] int findEndIndex(int left, float64 v) const; + [[nodiscard]] int findIndex(int left, int right, float64 v) const; + + struct Line final { + std::vector y; + + Statistic::SegmentTree segmentTree; + int id = 0; + QString idString; + QString name; + int maxValue = 0; + int minValue = std::numeric_limits::max(); + QString colorKey; + QColor color; + QColor colorDark; + bool isHiddenOnStart = false; + }; + + std::vector x; + std::vector xPercentage; + std::vector daysLookup; + + std::vector lines; + + struct { + float64 min = 0.; + float64 max = 0.; + } defaultZoomXIndex; + + int maxValue = 0; + int minValue = std::numeric_limits::max(); + + float64 oneDayPercentage = 0.; + + float64 timeStep = 0.; + + bool isFooterHidden = false; + bool hasPercentages = false; + bool weekFormat = false; + +}; + +struct StatisticalGraph final { + StatisticalChart chart; + QString zoomToken; + QString error; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 13c453e06..6aba706b9 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -308,14 +308,14 @@ Image *Story::replyPreview() const { TextWithEntities Story::inReplyText() const { const auto type = tr::lng_in_dlg_story(tr::now); return _caption.text.isEmpty() - ? Ui::Text::PlainLink(type) + ? Ui::Text::Colorized(type) : tr::lng_dialogs_text_media( tr::now, lt_media_part, tr::lng_dialogs_text_media_wrapped( tr::now, lt_media, - Ui::Text::PlainLink(type), + Ui::Text::Colorized(type), Ui::Text::WithEntities), lt_caption, _caption, diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index ba8a44470..997d4282e 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -102,7 +102,7 @@ void MessageCursor::fillFrom(not_null field) { position = cursor.position(); anchor = cursor.anchor(); const auto top = field->scrollTop().current(); - scroll = (top != field->scrollTopMax()) ? top : QFIXED_MAX; + scroll = (top != field->scrollTopMax()) ? top : Ui::kQFixedMax; } void MessageCursor::applyTo(not_null field) { diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index a296e6cf4..6fd5cc51a 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "ui/text/text.h" // For QFIXED_MAX +#include "ui/text/text.h" // Ui::kQFixedMax. #include "data/data_peer_id.h" #include "data/data_msg_id.h" #include "base/qt/qt_compare.h" @@ -196,7 +196,7 @@ struct MessageCursor { int position = 0; int anchor = 0; - int scroll = QFIXED_MAX; + int scroll = Ui::kQFixedMax; }; @@ -303,6 +303,8 @@ enum class MessageFlag : uint64 { FakeBotAbout = (1ULL << 36), StoryItem = (1ULL << 37), + + InHighlightProcess = (1ULL << 38), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index b9a8c39b2..9516af32f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -65,12 +65,8 @@ dialogsRipple: RippleAnimation(defaultRippleAnimation) { color: dialogsRippleBg; } -dialogsTextFont: font(fsize); -dialogsTextStyle: TextStyle(defaultTextStyle) { - font: dialogsTextFont; - linkFont: dialogsTextFont; - linkFontOver: dialogsTextFont; -} +dialogsTextFont: normalFont; +dialogsTextStyle: defaultTextStyle; dialogsDateFont: font(13px); dialogsDateSkip: 5px; @@ -447,11 +443,7 @@ dialogsSearchInHeight: 52px; dialogsSearchInPhotoSize: 36px; dialogsSearchInPhotoPadding: 10px; dialogsSearchInSkip: 7px; -dialogsSearchFromStyle: TextStyle(defaultTextStyle) { - font: normalFont; - linkFont: semiboldFont; - linkFontOver: semiboldFont; -} +dialogsSearchFromStyle: defaultTextStyle; dialogsSearchFromPalette: TextPalette(defaultTextPalette) { linkFg: dialogsNameFg; } @@ -507,8 +499,6 @@ downloadTitleLeft: 57px; downloadTitleTop: 4px; downloadInfoStyle: TextStyle(defaultTextStyle) { font: font(12px); - linkFont: font(12px); - linkFontOver: font(12px underline); } downloadInfoLeft: 57px; downloadInfoTop: 23px; @@ -541,8 +531,6 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) { namePosition: point(55px, 11px); nameStyle: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold); } } chooseTopicList: PeerList(defaultPeerList) { @@ -599,8 +587,6 @@ dialogsStoriesFull: DialogsStories { nameTop: 56px; nameStyle: TextStyle(defaultTextStyle) { font: font(11px); - linkFont: font(11px); - linkFontOver: font(11px); } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index eadcc89ee..a6863e357 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -2913,7 +2913,7 @@ void InnerWidget::refreshSearchInChatLabel() { const auto fromUserText = tr::lng_dlg_search_from( tr::now, lt_user, - Ui::Text::Link(from), + Ui::Text::Semibold(from), Ui::Text::WithEntities); _searchFromUserText.setMarkedText( st::dialogsSearchFromStyle, diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index e273ec9b7..e2dea5579 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -540,14 +540,12 @@ void Widget::chosenRow(const ChosenRow &row) { return; } else if (history) { const auto peer = history->peer; - if (const auto user = peer->asUser()) { - if (row.message.fullId.msg == ShowAtUnreadMsgId) { - if (row.userpicClick - && user->hasActiveStories() - && !user->isSelf()) { - controller()->openPeerStories(user->id); - return; - } + if (row.message.fullId.msg == ShowAtUnreadMsgId) { + if (row.userpicClick + && peer->hasActiveStories() + && !peer->isSelf()) { + controller()->openPeerStories(peer->id); + return; } } const auto showAtMsgId = controller()->uniqueChatsInSearchResults() diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 3ef60a4fe..4b9c3fe87 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -260,7 +260,7 @@ void PaintFolderEntryText( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionLines = rect.height() / st::dialogsTextFont->height, + .elisionHeight = rect.height(), }); } @@ -420,7 +420,7 @@ void PaintRow( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionLines = 1, + .elisionOneLine = true, }); } else if (draft || (supportMode @@ -462,13 +462,13 @@ void PaintRow( auto &cache = thread->cloudDraftTextCache(); if (cache.isEmpty()) { using namespace TextUtilities; - auto draftWrapped = Text::PlainLink( + auto draftWrapped = Text::Colorized( tr::lng_dialogs_text_from_wrapped( tr::now, lt_from, tr::lng_from_draft(tr::now))); auto draftText = supportMode - ? Text::PlainLink( + ? Text::Colorized( Support::ChatOccupiedString(history)) : tr::lng_dialogs_text_with_from( tr::now, @@ -514,7 +514,7 @@ void PaintRow( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionLines = 1, + .elisionOneLine = true, }); } } else if (!item) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index f40bf5308..1202d5aaf 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -94,7 +94,7 @@ TextWithEntities DialogsPreviewText(TextWithEntities text) { EntityType::Underline, EntityType::Italic, EntityType::CustomEmoji, - EntityType::PlainLink, + EntityType::Colorized, }); for (auto &entity : result.entities) { if (entity.type() == EntityType::Pre) { @@ -102,6 +102,13 @@ TextWithEntities DialogsPreviewText(TextWithEntities text) { EntityType::Code, entity.offset(), entity.length()); + } else if (entity.type() == EntityType::Colorized + && !entity.data().isEmpty()) { + // Drop 'data' so that only link-color colorization takes place. + entity = EntityInText( + EntityType::Colorized, + entity.offset(), + entity.length()); } } return result; @@ -188,7 +195,7 @@ void MessageView::prepare( TextUtilities::Trim(preview.text); auto textToCache = DialogsPreviewText(std::move(preview.text)); _hasPlainLinkAtBegin = !textToCache.entities.empty() - && (textToCache.entities.front().type() == EntityType::PlainLink); + && (textToCache.entities.front().type() == EntityType::Colorized); _textCache.setMarkedText( st::dialogsTextStyle, std::move(textToCache), @@ -305,7 +312,6 @@ void MessageView::paint( rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip); finalRight -= st::forumDialogJumpArrowSkip; } - const auto lines = rect.height() / st::dialogsTextFont->height; const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); if (!_senderCache.isEmpty()) { @@ -313,7 +319,7 @@ void MessageView::paint( .position = rect.topLeft(), .availableWidth = rect.width(), .palette = palette, - .elisionLines = lines, + .elisionHeight = rect.height(), }); rect.setLeft(rect.x() + _senderCache.maxWidth()); if (!_imagesCache.empty() && !_leftIcon) { @@ -381,7 +387,7 @@ void MessageView::paint( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = pausedSpoiler, - .elisionLines = lines, + .elisionHeight = rect.height(), }); rect.setLeft(rect.x() + _textCache.maxWidth()); } @@ -457,7 +463,7 @@ HistoryView::ItemPreview PreviewWithSender( auto fullWithOffset = tr::lng_dialogs_text_with_from( tr::now, lt_from_part, - Ui::Text::PlainLink(std::move(wrappedWithOffset.text)), + Ui::Text::Colorized(std::move(wrappedWithOffset.text)), lt_message, std::move(preview.text), TextWithTagOffset::FromString); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index 20d74e916..f9fe16062 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -75,7 +75,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { title.title.setMarkedText( st::dialogsTextStyle, (unread - ? Ui::Text::PlainLink( + ? Ui::Text::Colorized( Ui::Text::Wrapped( std::move(topicTitle), EntityType::Bold)) @@ -141,7 +141,7 @@ void TopicsView::paint( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionLines = 1, + .elisionOneLine = true, }); const auto skip = skipBig ? context.st->topicsSkipBig diff --git a/Telegram/SourceFiles/editor/editor.style b/Telegram/SourceFiles/editor/editor.style index 68ec4c396..3a56f21c8 100644 --- a/Telegram/SourceFiles/editor/editor.style +++ b/Telegram/SourceFiles/editor/editor.style @@ -38,8 +38,6 @@ photoEditorTextButtonPadding: margins(22px, 0px, 22px, 0px); photoEditorButtonStyle: TextStyle(semiboldTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold underline); } photoEditorButtonTextTop: 15px; diff --git a/Telegram/SourceFiles/export/view/export.style b/Telegram/SourceFiles/export/view/export.style index 5091de896..d72530823 100644 --- a/Telegram/SourceFiles/export/view/export.style +++ b/Telegram/SourceFiles/export/view/export.style @@ -16,8 +16,6 @@ exportSubSettingPadding: margins(56px, 4px, 22px, 12px); exportHeaderLabel: FlatLabel(boxTitle) { style: TextStyle(defaultTextStyle) { font: font(15px semibold); - linkFont: font(15px semibold); - linkFontOver: font(15px semibold underline); } } exportHeaderPadding: margins(22px, 20px, 22px, 9px); @@ -57,8 +55,6 @@ exportProgressLabel: FlatLabel(boxLabel) { maxHeight: 20px; style: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold); } } exportProgressInfoLabel: FlatLabel(boxLabel) { diff --git a/Telegram/SourceFiles/export/view/export_view_top_bar.cpp b/Telegram/SourceFiles/export/view/export_view_top_bar.cpp index 62f65e81e..dbdb9fc6e 100644 --- a/Telegram/SourceFiles/export/view/export_view_top_bar.cpp +++ b/Telegram/SourceFiles/export/view/export_view_top_bar.cpp @@ -45,7 +45,7 @@ void TopBar::updateData(Content &&content) { .append(" \xe2\x80\x93 ") .append(row.label) .append(' ') - .append(Ui::Text::PlainLink(row.info))); + .append(Ui::Text::Colorized(row.info))); _progress->setValue(row.progress); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 4c9ac2fa4..4114ed923 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -67,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "payments/payments_checkout_process.h" // CheckoutProcess::Start. +#include "spellcheck/spellcheck_highlight_syntax.h" #include "styles/style_dialogs.h" // AyuGram includes @@ -2922,15 +2923,32 @@ void HistoryItem::setText(const TextWithEntities &textWithEntities) { : std::move(textWithEntities)); } -void HistoryItem::setTextValue(TextWithEntities text) { +void HistoryItem::setTextValue(TextWithEntities text, bool force) { + if (const auto processId = Spellchecker::TryHighlightSyntax(text)) { + _flags |= MessageFlag::InHighlightProcess; + history()->owner().registerHighlightProcess(processId, this); + } const auto had = !_text.empty(); _text = std::move(text); RemoveComponents(HistoryMessageTranslation::Bit()); - if (had) { + if (had || force) { history()->owner().requestItemTextRefresh(this); } } +bool HistoryItem::inHighlightProcess() const { + return _flags & MessageFlag::InHighlightProcess; +} + +void HistoryItem::highlightProcessDone() { + Expects(inHighlightProcess()); + + _flags &= ~MessageFlag::InHighlightProcess; + if (!_text.empty()) { + setTextValue(base::take(_text), true); + } +} + bool HistoryItem::showNotification() const { const auto channel = _history->peer->asChannel(); if (channel && !channel->amIn()) { @@ -2983,9 +3001,7 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const { // Because larger version is shown exactly to the left of the small. //auto media = _media ? _media->toPreview(options) : ItemPreview(); return { - .text = Ui::Text::Wrapped( - notificationText(), - EntityType::PlainLink), + .text = Ui::Text::Colorized(notificationText()), //.images = std::move(media.images), //.loadingContext = std::move(media.loadingContext), }; @@ -3062,7 +3078,7 @@ TextWithEntities HistoryItem::inReplyText() const { result = Ui::Text::Mid(result, name.size()); TextUtilities::Trim(result); } - return Ui::Text::Wrapped(result, EntityType::PlainLink); + return Ui::Text::Colorized(result); } const std::vector &HistoryItem::customTextLinks() const { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index f4bee5394..0041db4c5 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -322,6 +322,8 @@ public: [[nodiscard]] bool repliesAreComments() const; [[nodiscard]] bool externalReply() const; [[nodiscard]] bool hasExtendedMediaPreview() const; + [[nodiscard]] bool inHighlightProcess() const; + void highlightProcessDone(); void setCommentsInboxReadTill(MsgId readTillId); void setCommentsMaxId(MsgId maxId); @@ -538,7 +540,7 @@ private: [[nodiscard]] bool generateLocalEntitiesByReply() const; [[nodiscard]] TextWithEntities withLocalEntities( const TextWithEntities &textWithEntities) const; - void setTextValue(TextWithEntities text); + void setTextValue(TextWithEntities text, bool force = false); [[nodiscard]] bool isTooOldForEdit(TimeId now) const; [[nodiscard]] bool isLegacyMessage() const { return _flags & MessageFlag::Legacy; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 2ae09fd62..3274c23cc 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_key_modifiers.h" #include "lang/lang_keys.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/spoiler_mess.h" #include "ui/image/image.h" #include "ui/toast/toast.h" #include "ui/text/text_options.h" @@ -260,6 +261,17 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { } } +HistoryMessageReply::HistoryMessageReply() = default; + +HistoryMessageReply &HistoryMessageReply::operator=( + HistoryMessageReply &&other) = default; + +HistoryMessageReply::~HistoryMessageReply() { + // clearData() should be called by holder. + Expects(replyToMsg.empty()); + Expects(replyToVia == nullptr); +} + bool HistoryMessageReply::updateData( not_null holder, bool force) { @@ -311,7 +323,7 @@ bool HistoryMessageReply::updateData( .customEmojiRepaint = repaint, }; replyToText.setMarkedText( - st::messageTextStyle, + st::defaultTextStyle, (replyToMsg ? replyToMsg->inReplyText() : replyToStory->inReplyText()), @@ -333,7 +345,8 @@ bool HistoryMessageReply::updateData( if (replyToMsg) { const auto peer = replyToMsg->history()->peer; replyToColorKey = (!holder->out() - && (peer->isMegagroup() || peer->isChat())) + && (peer->isMegagroup() || peer->isChat()) + && replyToMsg->from()->isUser()) ? replyToMsg->from()->id : PeerId(0); } else { @@ -630,7 +643,7 @@ void HistoryMessageReply::paint( .pausedEmoji = (context.paused || On(PowerSaving::kEmojiChat)), .pausedSpoiler = pausedSpoiler, - .elisionLines = 1, + .elisionOneLine = true, }); p.setTextPalette(stm->textPalette); } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index d5414ba14..6416475ab 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -22,6 +22,7 @@ namespace Ui { struct ChatPaintContext; class ChatStyle; struct PeerUserpicView; +class SpoilerAnimation; } // namespace Ui namespace Data { @@ -227,17 +228,13 @@ private: struct HistoryMessageReply : public RuntimeComponent { - HistoryMessageReply() = default; + HistoryMessageReply(); HistoryMessageReply(const HistoryMessageReply &other) = delete; HistoryMessageReply(HistoryMessageReply &&other) = delete; HistoryMessageReply &operator=( const HistoryMessageReply &other) = delete; - HistoryMessageReply &operator=(HistoryMessageReply &&other) = default; - ~HistoryMessageReply() { - // clearData() should be called by holder. - Expects(replyToMsg.empty()); - Expects(replyToVia == nullptr); - } + HistoryMessageReply &operator=(HistoryMessageReply &&other); + ~HistoryMessageReply(); static constexpr auto kBarAlpha = 230. / 255.; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index eb6235c48..718c60e22 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bar.h" #include "ui/chat/attach/attach_send_files_way.h" #include "ui/chat/choose_send_as.h" +#include "ui/effects/spoiler_mess.h" #include "ui/image/image.h" #include "ui/painter.h" #include "ui/power_saving.h" @@ -1779,7 +1780,7 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived( MessageCursor cursor = { int(textWithTags.text.size()), int(textWithTags.text.size()), - QFIXED_MAX, + Ui::kQFixedMax, }; _history->setLocalDraft(std::make_unique( textWithTags, @@ -6216,7 +6217,11 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { crl::guard(_list, [=] { cancelEdit(); })); } else if (_inReplyEditForward) { if (isReadyToForward) { - _forwardPanel->editOptions(controller()->uiShow()); + if (e->button() != Qt::LeftButton) { + _forwardPanel->editToNextOption(); + } else { + _forwardPanel->editOptions(controller()->uiShow()); + } } else { controller()->showPeerHistory( _peer, @@ -7201,7 +7206,7 @@ void HistoryWidget::editMessage(not_null item) { const auto cursor = MessageCursor { int(editData.text.size()), int(editData.text.size()), - QFIXED_MAX + Ui::kQFixedMax }; const auto previewPage = [&]() -> WebPageData* { if (const auto media = item->media()) { @@ -7503,7 +7508,7 @@ void HistoryWidget::updatePreview() { Ui::NameTextOptions()); auto linkText = QStringView(_previewLinks).split(' ').at(0).toString(); _previewDescription.setText( - st::messageTextStyle, + st::defaultTextStyle, linkText, Ui::DialogTextOptions()); @@ -7524,7 +7529,7 @@ void HistoryWidget::updatePreview() { preview.title, Ui::NameTextOptions()); _previewDescription.setText( - st::messageTextStyle, + st::defaultTextStyle, preview.description, Ui::DialogTextOptions()); } @@ -7779,7 +7784,7 @@ void HistoryWidget::updateReplyEditText(not_null item) { .customEmojiRepaint = [=] { updateField(); }, }; _replyEditMsgText.setMarkedText( - st::messageTextStyle, + st::defaultTextStyle, item->inReplyText(), Ui::DialogTextOptions(), context); @@ -7970,7 +7975,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { .now = now, .pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = pausedSpoiler, - .elisionLines = 1, + .elisionOneLine = true, }); } else { p.setFont(st::msgDateFont); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index cb1affa57..d19da9a57 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/send_as_button.h" #include "ui/controls/silent_toggle.h" #include "ui/chat/choose_send_as.h" +#include "ui/effects/spoiler_mess.h" #include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "mainwindow.h" @@ -902,7 +903,7 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { .now = crl::now(), .pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat), .pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler), - .elisionLines = 1, + .elisionOneLine = true, }); } @@ -2883,7 +2884,7 @@ void ComposeControls::editMessage(not_null item) { const auto cursor = MessageCursor{ int(editData.text.size()), int(editData.text.size()), - QFIXED_MAX + Ui::kQFixedMax }; const auto previewPage = [&]() -> WebPageData* { if (const auto media = item->media()) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index b00624867..f74453919 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -36,6 +36,31 @@ constexpr auto kUnknownVersion = -1; constexpr auto kNameWithCaptionsVersion = -2; constexpr auto kNameNoCaptionsVersion = -3; +[[nodiscard]] bool HasCaptions(const HistoryItemsList &list) { + for (const auto &item : list) { + if (const auto media = item->media()) { + if (!item->originalText().text.isEmpty() + && media->allowsEditCaption()) { + return true; + } + } + } + return false; +} + +[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) { + for (const auto &item : list) { + if (const auto media = item->media()) { + if (!media->forceForwardedInfo()) { + return false; + } + } else { + return false; + } + } + return true; +} + } // namespace ForwardPanel::ForwardPanel(Fn repaint) @@ -181,7 +206,7 @@ void ForwardPanel::updateTexts() { text = DropCustomEmoji(std::move(text)); } } else { - text = Ui::Text::PlainLink( + text = Ui::Text::Colorized( tr::lng_forward_messages(tr::now, lt_count, count)); } } @@ -191,7 +216,7 @@ void ForwardPanel::updateTexts() { .customEmojiRepaint = _repaint, }; _text.setMarkedText( - st::messageTextStyle, + st::defaultTextStyle, text, Ui::DialogTextOptions(), context); @@ -224,32 +249,10 @@ void ForwardPanel::editOptions(std::shared_ptr show) { const auto now = _data.options; const auto count = _data.items.size(); const auto dropNames = (now != Options::PreserveInfo); - const auto hasCaptions = [&] { - for (const auto item : _data.items) { - if (const auto media = item->media()) { - if (!item->originalText().text.isEmpty() - && media->allowsEditCaption()) { - return true; - } - } - } - return false; - }(); - const auto hasOnlyForcedForwardedInfo = [&] { - if (hasCaptions) { - return false; - } - for (const auto item : _data.items) { - if (const auto media = item->media()) { - if (!media->forceForwardedInfo()) { - return false; - } - } else { - return false; - } - } - return true; - }(); + const auto hasCaptions = HasCaptions(_data.items); + const auto hasOnlyForcedForwardedInfo = hasCaptions + ? false + : HasOnlyForcedForwardedInfo(_data.items); const auto dropCaptions = (now == Options::NoNamesAndCaptions); const auto weak = base::make_weak(this); const auto changeRecipient = crl::guard(this, [=] { @@ -299,6 +302,30 @@ void ForwardPanel::editOptions(std::shared_ptr show) { changeRecipient)); } +void ForwardPanel::editToNextOption() { + using Options = Data::ForwardOptions; + const auto hasCaptions = HasCaptions(_data.items); + const auto hasOnlyForcedForwardedInfo = hasCaptions + ? false + : HasOnlyForcedForwardedInfo(_data.items); + if (hasOnlyForcedForwardedInfo) { + return; + } + + const auto now = _data.options; + const auto next = (now == Options::PreserveInfo) + ? Options::NoSenderNames + : ((now == Options::NoSenderNames) && hasCaptions) + ? Options::NoNamesAndCaptions + : Options::PreserveInfo; + + _to->owningHistory()->setForwardDraft(_to->topicRootId(), { + .ids = _to->owner().itemsToIds(_data.items), + .options = next, + }); + _repaint(); +} + void ForwardPanel::paint( Painter &p, int x, @@ -364,7 +391,7 @@ void ForwardPanel::paint( .now = now, .pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = pausedSpoiler, - .elisionLines = 1, + .elisionOneLine = true, }); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index b178e27d0..038a0fbf7 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -47,6 +47,7 @@ public: [[nodiscard]] rpl::producer<> itemsUpdated() const; void editOptions(std::shared_ptr show); + void editToNextOption(); [[nodiscard]] const HistoryItemsList &items() const; [[nodiscard]] bool empty() const; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index aa453c225..dca4e0af0 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1622,6 +1622,9 @@ void Message::paintText( .position = trect.topLeft(), .availableWidth = trect.width(), .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = stm->blockquoteCache.get(), + .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index d1d7953a6..44ddb1c17 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -358,15 +358,15 @@ void ServiceMessagePainter::PaintComplexBubble( } } -QVector ServiceMessagePainter::CountLineWidths( +std::vector ServiceMessagePainter::CountLineWidths( const Ui::Text::String &text, const QRect &textRect) { const auto linesCount = qMax( textRect.height() / st::msgServiceFont->height, 1); - auto result = QVector(); - result.reserve(linesCount); - text.countLineWidths(textRect.width(), &result); + auto result = text.countLineWidths(textRect.width(), { + .reserve = linesCount, + }); const auto minDelta = 2 * (Ui::HistoryServiceMsgRadius() + Ui::HistoryServiceMsgInvertedRadius() diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index 5ee09aa7e..4fcd5748f 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -113,7 +113,7 @@ public: const QRect &textRect); private: - static QVector CountLineWidths( + static std::vector CountLineWidths( const Ui::Text::String &text, const QRect &textRect); diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index fc22f3de4..0fd011ec7 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1113,7 +1113,9 @@ void TopBarWidget::updateControlsVisibility() { const auto callsEnabled = [&] { if (const auto peer = _activeChat.key.peer()) { if (const auto user = peer->asUser()) { - return !user->isSelf() && !user->isBot(); + return !user->isSelf() + && !user->isBot() + && !peer->isServiceUser(); } } return false; diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index d1ddb2b96..c8e25a551 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -748,6 +748,9 @@ void Document::draw( .position = { st::msgPadding.left(), captiontop }, .availableWidth = captionw, .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = stm->blockquoteCache.get(), + .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp index f2768fef1..f34ca7574 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp @@ -235,6 +235,9 @@ void ExtendedPreview::draw(Painter &p, const PaintContext &context) const { painty + painth + st::mediaCaptionSkip), .availableWidth = captionw, .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = stm->blockquoteCache.get(), + .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp index 788b28e2c..4767bfcce 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp @@ -257,7 +257,7 @@ void Game::draw(Painter &p, const PaintContext &context) const { .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .selection = toDescriptionSelection(context.selection), - .elisionLines = _descriptionLines, + .elisionHeight = _descriptionLines * lineHeight, .elisionRemoveFromEnd = endskip, }); tshift += _descriptionLines * lineHeight; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index ba5a5bbef..52008c66f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -709,6 +709,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const { .position = QPoint(st::msgPadding.left(), top), .availableWidth = captionw, .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = stm->blockquoteCache.get(), + .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), diff --git a/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h b/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h index 16a31614a..85852e96c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h +++ b/Telegram/SourceFiles/history/view/media/history_view_large_emoji.h @@ -14,6 +14,10 @@ namespace Stickers { struct LargeEmojiImage; } // namespace Stickers +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + namespace HistoryView { using LargeEmojiMedia = std::variant< diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 22b5ef296..4e19fa291 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/item_text_options.h" #include "ui/chat/chat_style.h" #include "ui/chat/message_bubble.h" +#include "ui/effects/spoiler_mess.h" #include "ui/image/image_prepare.h" #include "ui/power_saving.h" #include "core/ui_integration.h" diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index e20cf8f1e..2333e334d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -367,6 +367,9 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { captiony), .availableWidth = captionw, .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = stm->blockquoteCache.get(), + .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_spoiler.h b/Telegram/SourceFiles/history/view/media/history_view_media_spoiler.h index 605b3d4c8..2b885a25a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_spoiler.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_spoiler.h @@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bubble.h" #include "ui/effects/animations.h" +namespace Ui { +class SpoilerAnimation; +} // namespace Ui + namespace HistoryView { struct MediaSpoiler { diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 0c8849699..8e0763e39 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -405,6 +405,9 @@ void Photo::draw(Painter &p, const PaintContext &context) const { .position = QPoint(st::msgPadding.left(), top), .availableWidth = captionw, .palette = &stm->textPalette, + .pre = stm->preCache.get(), + .blockquote = stm->blockquoteCache.get(), + .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), 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 4fb1cef59..454475369 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -583,7 +583,9 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .selection = toDescriptionSelection(context.selection), - .elisionLines = std::max(_descriptionLines, 0), + .elisionHeight = ((_descriptionLines > 0) + ? (_descriptionLines * lineHeight) + : 0), .elisionRemoveFromEnd = (_descriptionLines > 0) ? endskip : 0, }); tshift += (_descriptionLines > 0) diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index ef2758d5c..398ec0cd8 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -20,6 +20,10 @@ struct ReactionFlyAnimationArgs; class ReactionFlyAnimation; } // namespace Ui +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + namespace HistoryView { using PaintContext = Ui::ChatPaintContext; class Message; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h index f3acf9826..51d776004 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/text/text_custom_emoji.h" // Ui::Text::CustomEmojiFactory. + namespace Ui { enum class WhoReadType; } // namespace Ui diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp new file mode 100644 index 000000000..26b12a690 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp @@ -0,0 +1,305 @@ +/* +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/info_boosts_inner_widget.h" + +#include "api/api_statistics.h" +#include "boxes/peers/edit_peer_invite_link.h" +#include "info/boosts/info_boosts_widget.h" +#include "info/info_controller.h" +#include "info/statistics/info_statistics_list_controllers.h" +#include "lang/lang_keys.h" +#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/rect.h" +#include "ui/widgets/labels.h" +#include "styles/style_info.h" +#include "styles/style_statistics.h" + +#include + +namespace Info::Boosts { +namespace { + +void AddHeader( + not_null content, + tr::phrase<> text) { + const auto header = content->add( + object_ptr(content), + st::statisticsLayerMargins + st::boostsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(text(tr::now)); + header->setSubTitle({}); +} + +void FillOverview( + not_null content, + const Data::BoostStatus &status) { + const auto &stats = status.overview; + + ::Settings::AddSkip(content, st::boostsLayerOverviewMargins.top()); + AddHeader(content, tr::lng_stats_overview_title); + ::Settings::AddSkip(content); + + const auto diffBetweenHeaders = 0 + + st::statisticsOverviewValue.style.font->height + - st::statisticsHeaderTitleTextStyle.font->height; + + const auto container = content->add( + object_ptr(content), + st::statisticsLayerMargins); + + const auto addPrimary = [&](float64 v) { + return Ui::CreateChild( + container, + (v >= 0) + ? Lang::FormatCountToShort(v).string + : QString(), + st::statisticsOverviewValue); + }; + const auto addSub = [&]( + not_null primary, + float64 percentage, + tr::phrase<> text) { + const auto second = Ui::CreateChild( + container, + percentage + ? u"%1%"_q.arg(std::abs(std::round(percentage * 10.) / 10.)) + : QString(), + st::statisticsOverviewSecondValue); + second->setTextColorOverride(st::windowSubTextFg->c); + const auto sub = Ui::CreateChild( + container, + text(), + st::statisticsOverviewSubtext); + sub->setTextColorOverride(st::windowSubTextFg->c); + + primary->geometryValue( + ) | rpl::start_with_next([=](const QRect &g) { + const auto &padding = st::statisticsOverviewSecondValuePadding; + second->moveToLeft( + rect::right(g) + padding.left(), + g.y() + padding.top()); + sub->moveToLeft( + g.x(), + st::statisticsChartHeaderHeight + - st::statisticsOverviewSubtext.style.font->height + + g.y() + + diffBetweenHeaders); + }, primary->lifetime()); + }; + + + const auto topLeftLabel = addPrimary(stats.level); + const auto topRightLabel = addPrimary(stats.premiumMemberCount); + const auto bottomLeftLabel = addPrimary(stats.boostCount); + const auto bottomRightLabel = addPrimary( + stats.nextLevelBoostCount - stats.boostCount); + + addSub( + topLeftLabel, + 0, + tr::lng_boosts_level); + addSub( + topRightLabel, + stats.premiumMemberPercentage, + tr::lng_boosts_premium_audience); + addSub( + bottomLeftLabel, + 0, + tr::lng_boosts_existing); + addSub( + bottomRightLabel, + 0, + tr::lng_boosts_next_level); + + container->showChildren(); + container->resize(container->width(), topLeftLabel->height() * 5); + container->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto halfWidth = s.width() / 2; + { + const auto &p = st::boostsOverviewValuePadding; + topLeftLabel->moveToLeft(p.left(), p.top()); + } + topRightLabel->moveToLeft( + topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip, + topLeftLabel->y()); + bottomLeftLabel->moveToLeft( + topLeftLabel->x(), + topLeftLabel->y() + st::statisticsOverviewMidSkip); + bottomRightLabel->moveToLeft( + topRightLabel->x(), + bottomLeftLabel->y()); + }, container->lifetime()); + ::Settings::AddSkip(content, st::boostsLayerOverviewMargins.bottom()); +} + +void FillShareLink( + not_null content, + std::shared_ptr show, + const QString &link, + not_null peer) { + const auto weak = Ui::MakeWeak(content); + const auto copyLink = crl::guard(weak, [=] { + QGuiApplication::clipboard()->setText(link); + show->showToast(tr::lng_channel_public_link_copied(tr::now)); + }); + const auto shareLink = crl::guard(weak, [=] { + show->showBox(ShareInviteLinkBox(peer, link)); + }); + + const auto label = content->lifetime().make_state( + content, + rpl::single(link), + nullptr); + content->add( + label->take(), + st::boostsLinkFieldPadding); + + 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(); + ::Settings::AddSkip(content, st::boostsLinkFieldPadding.bottom()); +} + +} // namespace + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller, + not_null peer) +: VerticalLayout(parent) +, _controller(controller) +, _peer(peer) +, _show(controller->uiShow()) { +} + +void InnerWidget::load() { + const auto api = lifetime().make_state(_peer); + + _showFinished.events( + ) | rpl::take(1) | rpl::start_with_next([=] { + api->request( + ) | rpl::start_with_error_done([](const QString &error) { + }, [=] { + _state = api->boostStatus(); + fill(); + }, lifetime()); + }, lifetime()); +} + +void InnerWidget::fill() { + const auto fakeShowed = lifetime().make_state>(); + const auto &status = _state; + const auto inner = this; + + { + auto dividerContent = object_ptr(inner); + Ui::FillBoostLimit( + fakeShowed->events(), + rpl::single(status.overview.isBoosted), + dividerContent.data(), + Ui::BoostBoxData{ + .boost = Ui::BoostCounters{ + .level = status.overview.level, + .boosts = status.overview.boostCount, + .thisLevelBoosts + = status.overview.currentLevelBoostCount, + .nextLevelBoosts + = status.overview.nextLevelBoostCount, + .mine = status.overview.isBoosted, + } + }, + st::statisticsLimitsLinePadding); + inner->add(object_ptr( + inner, + std::move(dividerContent), + st::statisticsLimitsDividerPadding)); + } + + FillOverview(inner, status); + + ::Settings::AddSkip(inner); + ::Settings::AddDivider(inner); + ::Settings::AddSkip(inner); + + if (status.firstSlice.total > 0) { + ::Settings::AddSkip(inner); + using PeerPtr = not_null; + const auto header = inner->add( + object_ptr(inner), + st::statisticsLayerMargins + + st::boostsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(tr::lng_boosts_list_title( + tr::now, + lt_count, + status.firstSlice.total)); + header->setSubTitle({}); + Statistics::AddBoostsList( + status.firstSlice, + inner, + [=](PeerPtr p) { _controller->showPeerInfo(p); }, + _peer, + tr::lng_boosts_title()); + ::Settings::AddSkip(inner); + ::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); + FillShareLink(inner, _show, status.link, _peer); + ::Settings::AddSkip(inner); + ::Settings::AddDividerText(inner, tr::lng_boosts_link_subtext()); + + resizeToWidth(width()); + crl::on_main([=]{ fakeShowed->fire({}); }); +} + +void InnerWidget::saveState(not_null memento) { + memento->setState(base::take(_state)); +} + +void InnerWidget::restoreState(not_null memento) { + _state = memento->state(); + if (!_state.link.isEmpty()) { + fill(); + } else { + load(); + } + Ui::RpWidget::resizeToWidth(width()); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +auto InnerWidget::showRequests() const -> rpl::producer { + return _showRequests.events(); +} + +void InnerWidget::showFinished() { + _showFinished.fire({}); +} + +not_null InnerWidget::peer() const { + return _peer; +} + +} // namespace Info::Boosts + diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.h b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.h new file mode 100644 index 000000000..3161f81c0 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.h @@ -0,0 +1,63 @@ +/* +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 "data/data_boosts.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/vertical_layout.h" + +namespace Ui { +class Show; +} // namespace Ui + +namespace Info { +class Controller; +} // namespace Info + +namespace Info::Boosts { + +class Memento; + +class InnerWidget final : public Ui::VerticalLayout { +public: + struct ShowRequest final { + }; + + InnerWidget( + QWidget *parent, + not_null controller, + not_null peer); + + [[nodiscard]] not_null peer() const; + + [[nodiscard]] rpl::producer scrollToRequests() const; + [[nodiscard]] rpl::producer showRequests() const; + + void showFinished(); + + void saveState(not_null memento); + void restoreState(not_null memento); + +private: + void load(); + void fill(); + + not_null _controller; + not_null _peer; + std::shared_ptr _show; + + Data::BoostStatus _state; + + rpl::event_stream _scrollToRequests; + rpl::event_stream _showRequests; + rpl::event_stream<> _showFinished; + rpl::event_stream _loaded; + +}; + +} // namespace Info::Boosts diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp new file mode 100644 index 000000000..2679f67a0 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp @@ -0,0 +1,121 @@ +/* +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/info_boosts_widget.h" + +#include "info/boosts/info_boosts_inner_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "lang/lang_keys.h" + +namespace Info::Boosts { + +Memento::Memento(not_null controller) +: ContentMemento(Info::Statistics::Tag{ + controller->statisticsPeer(), + {} +}) { +} + +Memento::Memento(not_null peer) +: ContentMemento(Info::Statistics::Tag{ peer, {} }) { +} + +Memento::~Memento() = default; + +Section Memento::section() const { + return Section(Section::Type::Boosts); +} + +void Memento::setState(SavedState state) { + _state = std::move(state); +} + +Memento::SavedState Memento::state() { + return base::take(_state); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +Widget::Widget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) +, _inner(setInnerWidget( + object_ptr( + this, + controller, + controller->statisticsPeer()))) { + _inner->showRequests( + ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { + }, _inner->lifetime()); + _inner->scrollToRequests( + ) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) { + scrollTo(request); + }, _inner->lifetime()); +} + +not_null Widget::peer() const { + return _inner->peer(); +} + +bool Widget::showInternal(not_null memento) { + return false; +} + +rpl::producer Widget::title() { + return tr::lng_boosts_title(); +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +rpl::producer Widget::desiredShadowVisibility() const { + return rpl::single(true); +} + +void Widget::showFinished() { + _inner->showFinished(); +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared(controller()); + saveState(result.get()); + return result; +} + +void Widget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); + _inner->saveState(memento); +} + +void Widget::restoreState(not_null memento) { + _inner->restoreState(memento); + scrollTopRestore(memento->scrollTop()); +} + +std::shared_ptr Make(not_null peer) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer))); +} + +} // namespace Info::Boosts + diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_widget.h b/Telegram/SourceFiles/info/boosts/info_boosts_widget.h new file mode 100644 index 000000000..d67648267 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/info_boosts_widget.h @@ -0,0 +1,68 @@ +/* +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 "data/data_boosts.h" +#include "info/info_content_widget.h" + +namespace Info::Boosts { + +class InnerWidget; + +class Memento final : public ContentMemento { +public: + Memento(not_null controller); + Memento(not_null peer); + ~Memento(); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + using SavedState = Data::BoostStatus; + + void setState(SavedState states); + [[nodiscard]] SavedState state(); + +private: + SavedState _state; + +}; + +class Widget final : public ContentWidget { +public: + Widget(QWidget *parent, not_null controller); + + bool showInternal(not_null memento) override; + rpl::producer title() override; + rpl::producer desiredShadowVisibility() const override; + void showFinished() override; + + [[nodiscard]] not_null peer() const; + [[nodiscard]] FullMsgId contextId() const; + + void setInternalState( + const QRect &geometry, + not_null memento); + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + const not_null _inner; + +}; + +[[nodiscard]] std::shared_ptr Make(not_null peer); + +} // namespace Info::Boosts diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index ffd146b89..6bf6dc70a 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -131,8 +131,6 @@ infoTopBarTitle: FlatLabel(defaultFlatLabel) { maxHeight: 20px; style: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold); } } infoTopBarMediaCancel: IconButton(infoTopBarBack) { @@ -301,11 +299,6 @@ infoProfilePhotoSize: size( infoProfileStatus: FlatLabel(defaultFlatLabel) { maxHeight: 18px; textFg: windowSubTextFg; - style: TextStyle(defaultTextStyle) { - font: normalFont; - linkFont: normalFont; - linkFontOver: normalFont; - } } infoProfileCover: InfoProfileCover { height: 108px; @@ -320,8 +313,6 @@ infoProfileCover: InfoProfileCover { textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { font: font(16px semibold); - linkFont: font(16px semibold); - linkFontOver: font(16px semibold underline); } } nameLeft: 109px; @@ -333,7 +324,6 @@ infoProfileCover: InfoProfileCover { } infoProfileMegagroupCover: InfoProfileCover(infoProfileCover) { status: FlatLabel(infoProfileStatus) { - style: defaultTextStyle; palette: TextPalette(defaultTextPalette) { linkFg: windowSubTextFg; } @@ -427,8 +417,6 @@ infoBlockHeaderLabel: FlatLabel(infoProfileStatus) { textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { font: semiboldFont; - linkFont: semiboldFont; - linkFontOver: semiboldFont; } } infoBlockHeaderPosition: point(79px, 17px); @@ -550,8 +538,6 @@ infoCommonGroupsListItem: PeerListItem(defaultPeerListItem) { namePosition: point(71px, 15px); nameStyle: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold); } statusPosition: point(79px, 31px); } @@ -582,12 +568,11 @@ manageGroupButtonInner: SettingsButton(infoProfileButton) { } manageGroupButton: SettingsCountButton(managePeerButton) { button: manageGroupButtonInner; - labelPosition: point(22px, 12px); + labelPosition: point(22px, 10px); iconPosition: point(20px, 4px); } manageGroupTopButtonWithText: SettingsCountButton(manageGroupButton) { - labelPosition: point(22px, 10px); iconPosition: point(0px, 0px); } manageGroupTopicsButton: SettingsCountButton(manageGroupTopButtonWithText) { @@ -773,7 +758,8 @@ topBarConnectingAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimatio size: size(8px, 8px); } -inviteLinkFieldHeight: 44px; +inviteLinkFieldRadius: 5px; +inviteLinkFieldHeight: 42px; inviteLinkFieldMargin: margins(14px, 12px, 36px, 9px); inviteLinkThreeDotsIcon: icon {{ "info/edit/dotsmini", dialogsMenuIconFg }}; inviteLinkThreeDotsIconOver: icon {{ "info/edit/dotsmini", dialogsMenuIconFgOver }}; @@ -788,10 +774,14 @@ inviteLinkThreeDots: IconButton(defaultIconButton) { rippleAreaSize: 0px; } inviteLinkFieldPadding: margins(22px, 7px, 22px, 14px); +inviteLinkFieldLabel: FlatLabel(defaultFlatLabel) { + align: align(center); +} inviteLinkButton: RoundButton(defaultActiveButton) { height: 36px; textTop: 9px; + radius: 6px; } inviteLinkButtonsPadding: margins(22px, 0px, 22px, 0px); inviteLinkButtonsSkip: 10px; @@ -925,8 +915,6 @@ shortInfoCover: ShortInfoCover { maxHeight: 19px; style: TextStyle(defaultTextStyle) { font: font(15px semibold); - linkFont: font(15px semibold); - linkFontOver: font(15px semibold underline); } } namePosition: point(25px, 37px); diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index c14624677..89c0f7aad 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -339,6 +339,8 @@ Key ContentMemento::key() const { return Settings::Tag{ self }; } else if (const auto peer = storiesPeer()) { return Stories::Tag{ peer, storiesTab() }; + } else if (const auto peer = statisticsPeer()) { + return Statistics::Tag{ peer, statisticsContextId() }; } else { return Downloads::Tag(); } @@ -375,4 +377,9 @@ ContentMemento::ContentMemento(Stories::Tag stories) , _storiesTab(stories.tab) { } +ContentMemento::ContentMemento(Statistics::Tag statistics) +: _statisticsPeer(statistics.peer) +, _statisticsContextId(statistics.contextId) { +} + } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 6a6f75bbd..5f0c8480e 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -41,6 +41,10 @@ struct Tag; enum class Tab; } // namespace Info::Stories +namespace Info::Statistics { +struct Tag; +} // namespace Info::Statistics + namespace Info { class ContentMemento; @@ -163,6 +167,7 @@ public: explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); explicit ContentMemento(Stories::Tag stories); + explicit ContentMemento(Statistics::Tag statistics); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollContextId(contextId) { @@ -191,6 +196,12 @@ public: Stories::Tab storiesTab() const { return _storiesTab; } + PeerData *statisticsPeer() const { + return _statisticsPeer; + } + FullMsgId statisticsContextId() const { + return _statisticsContextId; + } PollData *poll() const { return _poll; } @@ -235,6 +246,8 @@ private: UserData * const _settingsSelf = nullptr; PeerData * const _storiesPeer = nullptr; Stories::Tab _storiesTab = {}; + PeerData * const _statisticsPeer = nullptr; + const FullMsgId _statisticsContextId; PollData * const _poll = nullptr; const FullMsgId _pollContextId; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index d7beb981e..85758a9d7 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -43,6 +43,9 @@ Key::Key(Downloads::Tag downloads) : _value(downloads) { Key::Key(Stories::Tag stories) : _value(stories) { } +Key::Key(Statistics::Tag statistics) : _value(statistics) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -89,6 +92,20 @@ Stories::Tab Key::storiesTab() const { return Stories::Tab(); } +PeerData *Key::statisticsPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +FullMsgId Key::statisticsContextId() const { + if (const auto tag = std::get_if(&_value)) { + return tag->contextId; + } + return {}; +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 82eb6f8eb..cb779f37f 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -55,6 +55,20 @@ struct Tag { } // namespace Info::Stories +namespace Info::Statistics { + +struct Tag { + explicit Tag(not_null peer, FullMsgId contextId) + : peer(peer) + , contextId(contextId) { + } + + not_null peer; + FullMsgId contextId; +}; + +} // namespace Info::Statistics + namespace Info { class Key { @@ -64,6 +78,7 @@ public: Key(Settings::Tag settings); Key(Downloads::Tag downloads); Key(Stories::Tag stories); + Key(Statistics::Tag statistics); Key(not_null poll, FullMsgId contextId); PeerData *peer() const; @@ -72,6 +87,8 @@ public: bool isDownloads() const; PeerData *storiesPeer() const; Stories::Tab storiesTab() const; + PeerData *statisticsPeer() const; + FullMsgId statisticsContextId() const; PollData *poll() const; FullMsgId pollContextId() const; @@ -86,6 +103,7 @@ private: Settings::Tag, Downloads::Tag, Stories::Tag, + Statistics::Tag, PollKey> _value; }; @@ -106,6 +124,8 @@ public: Downloads, Stories, PollResults, + Statistics, + Boosts, }; using SettingsType = ::Settings::Type; using MediaType = Storage::SharedMediaType; @@ -168,6 +188,12 @@ public: [[nodiscard]] Stories::Tab storiesTab() const { return key().storiesTab(); } + [[nodiscard]] PeerData *statisticsPeer() const { + return key().statisticsPeer(); + } + [[nodiscard]] FullMsgId statisticsContextId() const { + return key().statisticsContextId(); + } [[nodiscard]] PollData *poll() const; [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index 2dba25b5d..24932a701 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -152,11 +152,16 @@ std::shared_ptr Memento::DefaultContent( std::shared_ptr Memento::DefaultContent( not_null topic, Section section) { + const auto peer = topic->peer(); + const auto migrated = peer->migrateFrom(); + const auto migratedPeerId = migrated ? migrated->id : PeerId(0); switch (section.type()) { case Section::Type::Profile: return std::make_shared(topic); case Section::Type::Media: return std::make_shared(topic, section.mediaType()); + case Section::Type::Members: + return std::make_shared(peer, migratedPeerId); } Unexpected("Wrong section type in Info::Memento::DefaultContent()"); } diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 214e191fb..0bfb715a8 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -250,7 +250,10 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const { storiesPeer->owner().history(storiesPeer), FullMsgId()) : Dialogs::RowDescriptor(); - } else if (key().settingsSelf() || key().isDownloads() || key().poll()) { + } else if (key().settingsSelf() + || key().isDownloads() + || key().poll() + || key().statisticsPeer()) { return Dialogs::RowDescriptor(); } Unexpected("Owner in WrapWidget::activeChat()."); diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp index 73437e1dd..1cbcef23e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp @@ -76,6 +76,8 @@ Badge::Badge( }, _lifetime); } +Badge::~Badge() = default; + Ui::RpWidget *Badge::widget() const { return _view.data(); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.h b/Telegram/SourceFiles/info/profile/info_profile_badge.h index daada96d1..f0e770d3f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.h +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.h @@ -27,6 +27,10 @@ class RpWidget; class AbstractButton; } // namespace Ui +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + namespace Info::Profile { class EmojiStatusPanel; @@ -69,6 +73,8 @@ public: base::flags allowed = base::flags::from_raw(-1)); + ~Badge(); + [[nodiscard]] Ui::RpWidget *widget() const; void setPremiumClickCallback(Fn callback); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index eae3b777f..a15e49ccb 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -524,7 +524,7 @@ void Cover::refreshStatusText() { _refreshStatusTimer.callOnce(updateIn); } return showOnline - ? PlainLink(result) + ? Ui::Text::Colorized(result) : TextWithEntities{ .text = result }; } else if (auto chat = _peer->asChat()) { if (!chat->amIn()) { @@ -543,7 +543,7 @@ void Cover::refreshStatusText() { onlineCount, channel->isMegagroup()); return hasMembersLink - ? PlainLink(result) + ? Ui::Text::Link(result) : TextWithEntities{ .text = result }; } return tr::lng_chat_status_unaccessible(tr::now, WithEntities); diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 58e5e65f1..42633f1fe 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -45,7 +45,7 @@ Widget::Widget( , _self(controller->key().settingsSelf()) , _type(controller->section().settingsType()) , _inner([&] { - auto inner = _type()->create(this, controller->parentController()); + auto inner = _type->create(this, controller->parentController()); if (inner->hasFlexibleTopBar()) { auto filler = setInnerWidget(object_ptr(this)); filler->resize(1, 1); diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_common.h b/Telegram/SourceFiles/info/statistics/info_statistics_common.h new file mode 100644 index 000000000..30a3e092f --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_common.h @@ -0,0 +1,20 @@ +/* +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 "data/data_statistics.h" + +namespace Info::Statistics { + +struct SavedState final { + Data::AnyStatistics stats; + base::flat_map recentPostPreviews; + Data::PublicForwardsSlice publicForwardsFirstSlice; +}; + +} // 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 new file mode 100644 index 000000000..311752426 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -0,0 +1,732 @@ +/* +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/statistics/info_statistics_inner_widget.h" + +#include "api/api_statistics.h" +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "info/boosts/info_boosts_widget.h" +#include "info/statistics/info_statistics_list_controllers.h" +#include "info/statistics/info_statistics_recent_message.h" +#include "info/statistics/info_statistics_widget.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "statistics/chart_widget.h" +#include "statistics/statistics_common.h" +#include "statistics/widgets/chart_header_widget.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_boxes.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" + +namespace Info::Statistics { +namespace { + +struct Descriptor final { + not_null peer; + not_null api; + not_null toastParent; +}; + +void ProcessZoom( + const Descriptor &d, + not_null widget, + const QString &zoomToken, + Statistic::ChartViewType type) { + if (zoomToken.isEmpty()) { + return; + } + widget->zoomRequests( + ) | rpl::start_with_next([=](float64 x) { + d.api->requestZoom( + d.peer, + zoomToken, + x + ) | rpl::start_with_next_error_done([=]( + const Data::StatisticalGraph &graph) { + if (graph.chart) { + widget->setZoomedChartData(graph.chart, x, type); + } else if (!graph.error.isEmpty()) { + Ui::Toast::Show(d.toastParent, graph.error); + } + }, [=](const QString &error) { + }, [=] { + }, widget->lifetime()); + }, widget->lifetime()); +} + +void FillStatistic( + not_null content, + const Descriptor &descriptor, + Data::AnyStatistics &stats) { + using Type = Statistic::ChartViewType; + const auto &padding = st::statisticsChartEntryPadding; + const auto &m = st::statisticsLayerMargins; + const auto addSkip = [&](not_null c) { + ::Settings::AddSkip(c, padding.bottom()); + ::Settings::AddDivider(c); + ::Settings::AddSkip(c, padding.top()); + }; + const auto addChart = [&]( + Data::StatisticalGraph &graphData, + rpl::producer &&title, + Statistic::ChartViewType type) { + if (graphData.chart) { + const auto widget = content->add( + object_ptr(content), + m); + + widget->setChartData(graphData.chart, type); + ProcessZoom(descriptor, widget, graphData.zoomToken, type); + widget->setTitle(std::move(title)); + + addSkip(content); + } else if (!graphData.zoomToken.isEmpty()) { + const auto wrap = content->add( + object_ptr>( + content, + object_ptr(content))); + wrap->toggle(false, anim::type::instant); + const auto widget = wrap->entity()->add( + object_ptr(content), + m); + + descriptor.api->requestZoom( + descriptor.peer, + graphData.zoomToken, + 0 + ) | rpl::start_with_next_error_done([=, graphPtr = &graphData]( + const Data::StatisticalGraph &graph) mutable { + { + // Save the loaded async data to cache. + // Guarded by content->lifetime(). + *graphPtr = graph; + } + + if (graph.chart) { + widget->setChartData(graph.chart, type); + wrap->toggle(true, anim::type::normal); + ProcessZoom(descriptor, widget, graph.zoomToken, type); + widget->setTitle(rpl::duplicate(title)); + } else if (!graph.error.isEmpty()) { + Ui::Toast::Show(descriptor.toastParent, graph.error); + } + }, [=](const QString &error) { + }, [=] { + }, content->lifetime()); + + addSkip(wrap->entity()); + } + }; + addSkip(content); + if (stats.channel) { + addChart( + stats.channel.memberCountGraph, + tr::lng_chart_title_member_count(), + Type::Linear); + addChart( + stats.channel.joinGraph, + tr::lng_chart_title_join(), + Type::Linear); + addChart( + stats.channel.muteGraph, + tr::lng_chart_title_mute(), + Type::Linear); + addChart( + stats.channel.viewCountByHourGraph, + tr::lng_chart_title_view_count_by_hour(), + Type::Linear); + addChart( + stats.channel.viewCountBySourceGraph, + tr::lng_chart_title_view_count_by_source(), + Type::Stack); + addChart( + stats.channel.joinBySourceGraph, + tr::lng_chart_title_join_by_source(), + Type::Stack); + addChart( + stats.channel.languageGraph, + tr::lng_chart_title_language(), + Type::StackLinear); + addChart( + stats.channel.messageInteractionGraph, + tr::lng_chart_title_message_interaction(), + Type::DoubleLinear); + addChart( + stats.channel.instantViewInteractionGraph, + tr::lng_chart_title_instant_view_interaction(), + Type::DoubleLinear); + } else if (stats.supergroup) { + addChart( + stats.supergroup.memberCountGraph, + tr::lng_chart_title_member_count(), + Type::Linear); + addChart( + stats.supergroup.joinGraph, + tr::lng_chart_title_group_join(), + Type::Linear); + addChart( + stats.supergroup.joinBySourceGraph, + tr::lng_chart_title_group_join_by_source(), + Type::Stack); + addChart( + stats.supergroup.languageGraph, + tr::lng_chart_title_group_language(), + Type::StackLinear); + addChart( + stats.supergroup.messageContentGraph, + tr::lng_chart_title_group_message_content(), + Type::Stack); + addChart( + stats.supergroup.actionGraph, + tr::lng_chart_title_group_action(), + Type::DoubleLinear); + addChart( + stats.supergroup.dayGraph, + tr::lng_chart_title_group_day(), + Type::Linear); + addChart( + stats.supergroup.weekGraph, + tr::lng_chart_title_group_week(), + Type::StackLinear); + } else if (stats.message) { + addChart( + stats.message.messageInteractionGraph, + tr::lng_chart_title_message_interaction(), + Type::DoubleLinear); + } +} + +void FillLoading( + not_null container, + rpl::producer toggleOn, + rpl::producer<> showFinished) { + const auto emptyWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + emptyWrap->toggleOn(std::move(toggleOn), anim::type::instant); + + const auto content = emptyWrap->entity(); + auto icon = ::Settings::CreateLottieIcon( + content, + { .name = u"stats"_q, .sizeOverride = Size(st::changePhoneIconSize) }, + st::settingsBlockedListIconPadding); + + ( + std::move(showFinished) | rpl::take(1) + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::loop); + }, icon.widget->lifetime()); + content->add(std::move(icon.widget)); + + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_stats_loading(), + st::changePhoneTitle)), + st::changePhoneTitlePadding + st::boxRowPadding); + + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_stats_loading_subtext(), + st::statisticsLoadingSubtext)), + st::changePhoneDescriptionPadding + st::boxRowPadding); + + ::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top()); +} + +void AddHeader( + not_null content, + tr::phrase<> text, + const Data::AnyStatistics &stats) { + const auto startDate = stats.channel + ? stats.channel.startDate + : stats.supergroup.startDate; + const auto endDate = stats.channel + ? stats.channel.endDate + : stats.supergroup.endDate; + const auto header = content->add( + object_ptr(content), + st::statisticsLayerMargins + st::statisticsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(text(tr::now)); + if (!endDate || !startDate) { + header->setSubTitle({}); + return; + } + const auto formatter = u"d MMM yyyy"_q; + const auto from = QDateTime::fromSecsSinceEpoch(startDate); + const auto to = QDateTime::fromSecsSinceEpoch(endDate); + header->setSubTitle(QLocale().toString(from.date(), formatter) + + ' ' + + QChar(8212) + + ' ' + + QLocale().toString(to.date(), formatter)); +} + +void FillOverview( + not_null content, + const Data::AnyStatistics &stats) { + using Value = Data::StatisticalValue; + + const auto &channel = stats.channel; + const auto &supergroup = stats.supergroup; + + ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.top()); + AddHeader(content, tr::lng_stats_overview_title, stats); + ::Settings::AddSkip(content); + + struct Second final { + QColor color; + QString text; + }; + + const auto parseSecond = [&](const Value &v) -> Second { + const auto diff = v.value - v.previousValue; + if (!diff) { + return {}; + } + constexpr auto kTooMuchDiff = int(1'000'000); + const auto diffAbs = std::abs(diff); + const auto diffText = diffAbs > kTooMuchDiff + ? Lang::FormatCountToShort(std::abs(diff)).string + : QString::number(diffAbs); + return { + (diff < 0 ? st::menuIconAttentionColor : st::settingsIconBg2)->c, + QString("%1%2 (%3%)") + .arg((diff < 0) ? QChar(0x2212) : QChar(0x002B)) + .arg(diffText) + .arg(std::abs(std::round(v.growthRatePercentage * 10.) / 10.)) + }; + }; + + const auto diffBetweenHeaders = 0 + + st::statisticsOverviewValue.style.font->height + - st::statisticsHeaderTitleTextStyle.font->height; + + const auto container = content->add( + object_ptr(content), + st::statisticsLayerMargins); + + const auto addPrimary = [&](const Value &v) { + return Ui::CreateChild( + container, + (v.value >= 0) + ? Lang::FormatCountToShort(v.value).string + : QString(), + st::statisticsOverviewValue); + }; + const auto addSub = [&]( + not_null primary, + const Value &v, + tr::phrase<> text) { + const auto data = parseSecond(v); + const auto second = Ui::CreateChild( + container, + data.text, + st::statisticsOverviewSecondValue); + second->setTextColorOverride(data.color); + const auto sub = Ui::CreateChild( + container, + text(), + st::statisticsOverviewSubtext); + sub->setTextColorOverride(st::windowSubTextFg->c); + + primary->geometryValue( + ) | rpl::start_with_next([=](const QRect &g) { + const auto &padding = st::statisticsOverviewSecondValuePadding; + second->moveToLeft( + rect::right(g) + padding.left(), + g.y() + padding.top()); + sub->moveToLeft( + g.x(), + st::statisticsChartHeaderHeight + - st::statisticsOverviewSubtext.style.font->height + + g.y() + + diffBetweenHeaders); + }, primary->lifetime()); + }; + + const auto isChannel = (!!channel); + const auto isMessage = (!!stats.message); + const auto topLeftLabel = isChannel + ? addPrimary(channel.memberCount) + : isMessage + ? addPrimary({ .value = float64(stats.message.views) }) + : addPrimary(supergroup.memberCount); + const auto topRightLabel = isChannel + ? Ui::CreateChild( + container, + QString("%1%").arg(0.01 + * std::round(channel.enabledNotificationsPercentage * 100.)), + st::statisticsOverviewValue) + : isMessage + ? addPrimary({ .value = float64(stats.message.publicForwards) }) + : addPrimary(supergroup.messageCount); + const auto bottomLeftLabel = isChannel + ? addPrimary(channel.meanViewCount) + : isMessage + ? addPrimary({ .value = float64(stats.message.privateForwards) }) + : addPrimary(supergroup.viewerCount); + const auto bottomRightLabel = isChannel + ? addPrimary(channel.meanShareCount) + : isMessage + ? addPrimary({ .value = -1. }) + : addPrimary(supergroup.senderCount); + if (const auto &s = channel) { + addSub( + topLeftLabel, + s.memberCount, + tr::lng_stats_overview_member_count); + addSub( + topRightLabel, + {}, + tr::lng_stats_overview_enabled_notifications); + addSub( + bottomLeftLabel, + s.meanViewCount, + tr::lng_stats_overview_mean_view_count); + addSub( + bottomRightLabel, + s.meanShareCount, + tr::lng_stats_overview_mean_share_count); + } else if (const auto &s = supergroup) { + addSub( + topLeftLabel, + s.memberCount, + tr::lng_manage_peer_members); + addSub( + topRightLabel, + s.messageCount, + tr::lng_stats_overview_messages); + addSub( + bottomLeftLabel, + s.viewerCount, + tr::lng_stats_overview_group_mean_view_count); + addSub( + bottomRightLabel, + s.senderCount, + tr::lng_stats_overview_group_mean_post_count); + } else if (const auto &s = stats.message) { + if (s.views >= 0) { + addSub( + topLeftLabel, + {}, + tr::lng_stats_overview_message_views); + } + if (s.publicForwards >= 0) { + addSub( + topRightLabel, + {}, + tr::lng_stats_overview_message_public_shares); + } + if (s.privateForwards >= 0) { + addSub( + bottomLeftLabel, + {}, + tr::lng_stats_overview_message_private_shares); + } + } + container->showChildren(); + container->resize(container->width(), topLeftLabel->height() * 5); + container->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto halfWidth = s.width() / 2; + { + const auto &p = st::statisticsOverviewValuePadding; + topLeftLabel->moveToLeft(p.left(), p.top()); + } + topRightLabel->moveToLeft( + topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip, + topLeftLabel->y()); + bottomLeftLabel->moveToLeft( + topLeftLabel->x(), + topLeftLabel->y() + st::statisticsOverviewMidSkip); + bottomRightLabel->moveToLeft( + topRightLabel->x(), + bottomLeftLabel->y()); + }, container->lifetime()); + ::Settings::AddSkip(content, st::statisticsLayerOverviewMargins.bottom()); +} + +} // namespace + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller, + not_null peer, + FullMsgId contextId) +: VerticalLayout(parent) +, _controller(controller) +, _peer(peer) +, _contextId(contextId) { +} + +void InnerWidget::load() { + const auto inner = this; + + const auto descriptor = Descriptor{ + _peer, + lifetime().make_state(&_peer->session().api()), + _controller->uiShow()->toastParent(), + }; + + FillLoading( + inner, + _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1), + _showFinished.events()); + + const auto finishLoading = [=] { + _loaded.fire(true); + inner->resizeToWidth(width()); + inner->showChildren(); + }; + + _showFinished.events( + ) | rpl::take(1) | rpl::start_with_next([=] { + if (!_contextId) { + descriptor.api->request( + descriptor.peer + ) | rpl::start_with_done([=] { + _state.stats = Data::AnyStatistics{ + descriptor.api->channelStats(), + descriptor.api->supergroupStats(), + }; + if (_state.stats.channel) { + ::Settings::AddSkip(inner); + const auto button = ::Settings::AddButton( + inner, + tr::lng_boosts_title(), + st::boostsButton); + const auto controller = _controller; + button->setClickedCallback([=, peer = descriptor.peer] { + controller->showSection(Info::Boosts::Make(peer)); + }); + ::Settings::AddSkip(inner); + ::Settings::AddDivider(inner); + ::Settings::AddSkip(inner); + } + fill(); + + finishLoading(); + }, lifetime()); + } else { + const auto lifetimeApi = lifetime().make_state(); + const auto api = lifetimeApi->make_state( + descriptor.peer->asChannel(), + _contextId); + + api->request([=](const Data::MessageStatistics &data) { + _state.stats = Data::AnyStatistics{ .message = data }; + _state.publicForwardsFirstSlice = api->firstSlice(); + fill(); + + finishLoading(); + lifetimeApi->destroy(); + }); + } + }, lifetime()); +} + +void InnerWidget::fill() { + const auto inner = this; + const auto descriptor = Descriptor{ + _peer, + lifetime().make_state(&_peer->session().api()), + _controller->uiShow()->toastParent(), + }; + FillOverview(inner, _state.stats); + FillStatistic(inner, descriptor, _state.stats); + const auto &channel = _state.stats.channel; + const auto &supergroup = _state.stats.supergroup; + const auto &message = _state.stats.message; + if (channel) { + fillRecentPosts(); + } else if (supergroup) { + const auto showPeerInfo = [=](not_null peer) { + _showRequests.fire({ .info = peer->id }); + }; + const auto addSkip = [&]( + not_null c) { + ::Settings::AddSkip(c); + ::Settings::AddDivider(c); + ::Settings::AddSkip(c); + ::Settings::AddSkip(c); + }; + if (!supergroup.topSenders.empty()) { + AddMembersList( + { .topSenders = supergroup.topSenders }, + inner, + showPeerInfo, + descriptor.peer, + tr::lng_stats_members_title()); + } + if (!supergroup.topAdministrators.empty()) { + addSkip(inner); + AddMembersList( + { .topAdministrators + = supergroup.topAdministrators }, + inner, + showPeerInfo, + descriptor.peer, + tr::lng_stats_admins_title()); + } + if (!supergroup.topInviters.empty()) { + addSkip(inner); + AddMembersList( + { .topInviters = supergroup.topInviters }, + inner, + showPeerInfo, + descriptor.peer, + tr::lng_stats_inviters_title()); + } + } else if (message) { + AddPublicForwards( + _state.publicForwardsFirstSlice, + inner, + [=](FullMsgId id) { _showRequests.fire({ .history = id }); }, + descriptor.peer, + _contextId); + } +} + +void InnerWidget::fillRecentPosts() { + const auto &stats = _state.stats.channel; + if (!stats || stats.recentMessageInteractions.empty()) { + return; + } + _messagePreviews.reserve(stats.recentMessageInteractions.size()); + const auto container = this; + + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto content = wrap->entity(); + AddHeader(content, tr::lng_stats_recent_messages_title, { stats, {} }); + ::Settings::AddSkip(content); + + const auto addMessage = [=]( + not_null messageWrap, + not_null item, + const Data::StatisticsMessageInteractionInfo &info) { + const auto button = messageWrap->add( + object_ptr( + messageWrap, + rpl::never(), + st::statisticsRecentPostButton)); + auto it = _state.recentPostPreviews.find(item->fullId().msg); + auto cachedPreview = (it != end(_state.recentPostPreviews)) + ? base::take(it->second) + : QImage(); + const auto raw = Ui::CreateChild( + button, + item, + info.viewsCount, + info.forwardsCount, + std::move(cachedPreview)); + + _messagePreviews.push_back(raw); + raw->show(); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + if (!s.isNull()) { + raw->setGeometry(Rect(s) + - st::statisticsRecentPostButton.padding); + } + }, raw->lifetime()); + button->setClickedCallback([=, fullId = item->fullId()] { + _showRequests.fire({ .messageStatistic = fullId }); + }); + ::Settings::AddSkip(messageWrap); + if (!wrap->toggled()) { + wrap->toggle(true, anim::type::normal); + } + }; + + 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 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); + } + if (!foundLoaded) { + wrap->toggle(false, anim::type::instant); + } +} + +void InnerWidget::saveState(not_null memento) { + for (const auto &message : _messagePreviews) { + message->saveState(_state); + } + memento->setState(base::take(_state)); +} + +void InnerWidget::restoreState(not_null memento) { + _state = memento->state(); + if (_state.stats.channel + || _state.stats.supergroup + || _state.stats.message) { + fill(); + } else { + load(); + } + Ui::RpWidget::resizeToWidth(width()); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +auto InnerWidget::showRequests() const -> rpl::producer { + return _showRequests.events(); +} + +void InnerWidget::showFinished() { + _showFinished.fire({}); +} + +not_null InnerWidget::peer() const { + return _peer; +} + +FullMsgId InnerWidget::contextId() const { + return _contextId; +} + +} // namespace Info::Statistics + diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h new file mode 100644 index 000000000..cd3aa9348 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h @@ -0,0 +1,69 @@ +/* +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" +#include "info/statistics/info_statistics_common.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/vertical_layout.h" + +namespace Info { +class Controller; +} // namespace Info + +namespace Info::Statistics { + +class Memento; +class MessagePreview; + +class InnerWidget final : public Ui::VerticalLayout { +public: + struct ShowRequest final { + PeerId info = PeerId(0); + FullMsgId history; + FullMsgId messageStatistic; + }; + + InnerWidget( + QWidget *parent, + not_null controller, + not_null peer, + FullMsgId contextId); + + [[nodiscard]] not_null peer() const; + [[nodiscard]] FullMsgId contextId() const; + + [[nodiscard]] rpl::producer scrollToRequests() const; + [[nodiscard]] rpl::producer showRequests() const; + + void showFinished(); + + void saveState(not_null memento); + void restoreState(not_null memento); + +private: + void load(); + void fill(); + void fillRecentPosts(); + + not_null _controller; + not_null _peer; + FullMsgId _contextId; + + std::vector> _messagePreviews; + + SavedState _state; + + rpl::event_stream _scrollToRequests; + rpl::event_stream _showRequests; + rpl::event_stream<> _showFinished; + rpl::event_stream _loaded; + +}; + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp new file mode 100644 index 000000000..1666abbec --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -0,0 +1,561 @@ +/* +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/statistics/info_statistics_list_controllers.h" + +#include "api/api_statistics.h" +#include "boxes/peer_list_controllers.h" +#include "data/data_boosts.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/effects/toggle_arrow.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" +#include "styles/style_window.h" + +namespace Info::Statistics { +namespace { + +void AddArrow(not_null parent) { + const auto arrow = Ui::CreateChild(parent.get()); + arrow->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(arrow); + + const auto path = Ui::ToggleUpDownArrowPath( + st::statisticsShowMoreButtonArrowSize, + st::statisticsShowMoreButtonArrowSize, + st::statisticsShowMoreButtonArrowSize, + st::mainMenuToggleFourStrokes, + 0.); + + auto hq = PainterHighQualityEnabler(p); + p.fillPath(path, st::lightButtonFg); + }, arrow->lifetime()); + arrow->resize(Size(st::statisticsShowMoreButtonArrowSize * 2)); + arrow->move(st::statisticsShowMoreButtonArrowPosition); + arrow->show(); +} + +void AddSubsectionTitle( + not_null container, + rpl::producer title) { + const auto &subtitlePadding = st::settingsButton.padding; + ::Settings::AddSubsectionTitle( + container, + std::move(title), + { 0, -subtitlePadding.top(), 0, -subtitlePadding.bottom() }); +} + +[[nodiscard]] QString FormatText( + int value1, tr::phrase phrase1, + int value2, tr::phrase phrase2, + int value3, tr::phrase phrase3) { + const auto separator = u", "_q; + auto resultText = QString(); + if (value1 > 0) { + resultText += phrase1(tr::now, lt_count, value1); + } + if (value2 > 0) { + if (!resultText.isEmpty()) { + resultText += separator; + } + resultText += phrase2(tr::now, lt_count, value2); + } + if (value3 > 0) { + if (!resultText.isEmpty()) { + resultText += separator; + } + resultText += phrase3(tr::now, lt_count, value3); + } + return resultText; +} + +struct Descriptor final { + Data::PublicForwardsSlice firstSlice; + Fn showPeerHistory; + not_null peer; + FullMsgId contextId; +}; + +struct MembersDescriptor final { + not_null session; + Fn)> showPeerInfo; + Data::SupergroupStatistics data; +}; + +struct BoostsDescriptor final { + Data::BoostsListSlice firstSlice; + Fn)> showPeerInfo; + not_null peer; +}; + +class PeerListRowWithMsgId : public PeerListRow { +public: + using PeerListRow::PeerListRow; + + void setMsgId(MsgId msgId); + [[nodiscard]] MsgId msgId() const; + +private: + MsgId _msgId; + +}; + +void PeerListRowWithMsgId::setMsgId(MsgId msgId) { + _msgId = msgId; +} + +MsgId PeerListRowWithMsgId::msgId() const { + return _msgId; +} + +class MembersController final : public PeerListController { +public: + MembersController(MembersDescriptor d); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void loadMoreRows() override; + + void setLimit(int limit); + +private: + void addRows(int from, int to); + + const not_null _session; + Fn)> _showPeerInfo; + Data::SupergroupStatistics _data; + int _limit = 0; + +}; + +MembersController::MembersController(MembersDescriptor d) +: _session(std::move(d.session)) +, _showPeerInfo(std::move(d.showPeerInfo)) +, _data(std::move(d.data)) { +} + +Main::Session &MembersController::session() const { + return *_session; +} + +void MembersController::setLimit(int limit) { + addRows(_limit, limit); + _limit = limit; +} + +void MembersController::addRows(int from, int to) { + const auto addRow = [&](UserId userId, QString text) { + const auto user = _session->data().user(userId); + auto row = std::make_unique(user); + row->setCustomStatus(std::move(text)); + delegate()->peerListAppendRow(std::move(row)); + }; + if (!_data.topSenders.empty()) { + for (auto i = from; i < to; i++) { + const auto &member = _data.topSenders[i]; + addRow( + member.userId, + FormatText( + member.sentMessageCount, + tr::lng_stats_member_messages, + member.averageCharacterCount, + tr::lng_stats_member_characters, + 0, + {})); + } + } else if (!_data.topAdministrators.empty()) { + for (auto i = from; i < to; i++) { + const auto &admin = _data.topAdministrators[i]; + addRow( + admin.userId, + FormatText( + admin.deletedMessageCount, + tr::lng_stats_member_deletions, + admin.bannedUserCount, + tr::lng_stats_member_bans, + admin.restrictedUserCount, + tr::lng_stats_member_restrictions)); + } + } else if (!_data.topInviters.empty()) { + for (auto i = from; i < to; i++) { + const auto &inviter = _data.topInviters[i]; + addRow( + inviter.userId, + FormatText( + inviter.addedMemberCount, + tr::lng_stats_member_invitations, + 0, + {}, + 0, + {})); + } + } +} + +void MembersController::prepare() { +} + +void MembersController::loadMoreRows() { +} + +void MembersController::rowClicked(not_null row) { + crl::on_main([=, peer = row->peer()] { + _showPeerInfo(peer); + }); +} + +class PublicForwardsController final : public PeerListController { +public: + explicit PublicForwardsController(Descriptor d); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void loadMoreRows() override; + +private: + void appendRow(not_null peer, MsgId msgId); + void applySlice(const Data::PublicForwardsSlice &slice); + + const not_null _session; + Fn _showPeerHistory; + + Api::PublicForwards _api; + Data::PublicForwardsSlice _firstSlice; + Data::PublicForwardsSlice::OffsetToken _apiToken; + + bool _allLoaded = false; + +}; + +PublicForwardsController::PublicForwardsController(Descriptor d) +: _session(&d.peer->session()) +, _showPeerHistory(std::move(d.showPeerHistory)) +, _api(d.peer->asChannel(), d.contextId) +, _firstSlice(std::move(d.firstSlice)) { +} + +Main::Session &PublicForwardsController::session() const { + return *_session; +} + +void PublicForwardsController::prepare() { + applySlice(base::take(_firstSlice)); + delegate()->peerListRefreshRows(); +} + +void PublicForwardsController::loadMoreRows() { + if (_allLoaded) { + return; + } + _api.request(_apiToken, [=](const Data::PublicForwardsSlice &slice) { + applySlice(slice); + }); +} + +void PublicForwardsController::applySlice( + const Data::PublicForwardsSlice &slice) { + _allLoaded = slice.allLoaded; + _apiToken = slice.token; + + for (const auto &item : slice.list) { + if (const auto peer = session().data().peerLoaded(item.peer)) { + appendRow(peer, item.msg); + } + } + delegate()->peerListRefreshRows(); +} + +void PublicForwardsController::rowClicked(not_null row) { + const auto rowWithMsgId = static_cast(row.get()); + crl::on_main([=, msgId = rowWithMsgId->msgId(), peer = row->peer()] { + _showPeerHistory({ peer->id, msgId }); + }); +} + +void PublicForwardsController::appendRow( + not_null peer, + MsgId msgId) { + if (delegate()->peerListFindRow(peer->id.value)) { + return; + } + + auto row = std::make_unique(peer); + row->setMsgId(msgId); + + const auto members = peer->asChannel()->membersCount(); + const auto message = peer->owner().message({ peer->id, msgId }); + const auto views = message ? message->viewsCount() : 0; + + const auto membersText = !members + ? QString() + : peer->isMegagroup() + ? tr::lng_chat_status_members(tr::now, lt_count_decimal, members) + : tr::lng_chat_status_subscribers(tr::now, lt_count_decimal, members); + const auto viewsText = views + ? tr::lng_stats_recent_messages_views({}, lt_count_decimal, views) + : QString(); + const auto resultText = (membersText.isEmpty() || viewsText.isEmpty()) + ? membersText + viewsText + : QString("%1, %2").arg(membersText, viewsText); + row->setCustomStatus(resultText); + + delegate()->peerListAppendRow(std::move(row)); + return; +} + +class BoostsController final : public PeerListController { +public: + explicit BoostsController(BoostsDescriptor d); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void loadMoreRows() override; + + [[nodiscard]] bool skipRequest() const; + void setLimit(int limit); + +private: + void applySlice(const Data::BoostsListSlice &slice); + + const not_null _session; + Fn)> _showPeerInfo; + + Api::Boosts _api; + Data::BoostsListSlice _firstSlice; + Data::BoostsListSlice::OffsetToken _apiToken; + + int _limit = 0; + + bool _allLoaded = false; + bool _requesting = false; + +}; + +BoostsController::BoostsController(BoostsDescriptor d) +: _session(&d.peer->session()) +, _showPeerInfo(std::move(d.showPeerInfo)) +, _api(d.peer) +, _firstSlice(std::move(d.firstSlice)) { + PeerListController::setStyleOverrides(&st::boostsListBox); +} + +Main::Session &BoostsController::session() const { + return *_session; +} + +bool BoostsController::skipRequest() const { + return _requesting || _allLoaded; +} + +void BoostsController::setLimit(int limit) { + _limit = limit; + _requesting = true; + _api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) { + _requesting = false; + applySlice(slice); + }); +} + +void BoostsController::prepare() { + applySlice(base::take(_firstSlice)); + delegate()->peerListRefreshRows(); +} + +void BoostsController::loadMoreRows() { +} + +void BoostsController::applySlice(const Data::BoostsListSlice &slice) { + _allLoaded = slice.allLoaded; + _apiToken = slice.token; + + const auto formatter = u"MMM d, yyyy"_q; + for (const auto &item : slice.list) { + const auto user = session().data().user(item.userId); + if (delegate()->peerListFindRow(user->id.value)) { + continue; + } + auto row = std::make_unique(user); + row->setCustomStatus(tr::lng_boosts_list_status( + tr::now, + lt_date, + QLocale().toString(item.expirationDate, formatter))); + delegate()->peerListAppendRow(std::move(row)); + } + delegate()->peerListRefreshRows(); +} + +void BoostsController::rowClicked(not_null row) { + crl::on_main([=, peer = row->peer()] { + _showPeerInfo(peer); + }); +} + +} // namespace + +void AddPublicForwards( + const Data::PublicForwardsSlice &firstSlice, + not_null container, + Fn showPeerHistory, + not_null peer, + FullMsgId contextId) { + if (!peer->isChannel()) { + return; + } + + struct State final { + State(Descriptor d) : controller(std::move(d)) { + } + PeerListContentDelegateSimple delegate; + PublicForwardsController controller; + }; + const auto state = container->lifetime().make_state(Descriptor{ + firstSlice, + std::move(showPeerHistory), + peer, + contextId, + }); + + if (const auto total = firstSlice.total; total > 0) { + AddSubsectionTitle( + container, + tr::lng_stats_overview_message_public_share( + lt_count_decimal, + rpl::single(total))); + } + + state->delegate.setContent(container->add( + object_ptr(container, &state->controller))); + state->controller.setDelegate(&state->delegate); +} + +void AddMembersList( + Data::SupergroupStatistics data, + not_null container, + Fn)> showPeerInfo, + not_null peer, + rpl::producer title) { + if (!peer->isMegagroup()) { + return; + } + const auto max = !data.topSenders.empty() + ? data.topSenders.size() + : !data.topAdministrators.empty() + ? data.topAdministrators.size() + : !data.topInviters.empty() + ? data.topInviters.size() + : 0; + if (!max) { + return; + } + + constexpr auto kPerPage = 40; + struct State final { + State(MembersDescriptor d) : controller(std::move(d)) { + } + PeerListContentDelegateSimple delegate; + MembersController controller; + int limit = 0; + }; + auto d = MembersDescriptor{ + &peer->session(), + std::move(showPeerInfo), + std::move(data), + }; + const auto state = container->lifetime().make_state(std::move(d)); + + AddSubsectionTitle(container, std::move(title)); + + state->delegate.setContent(container->add( + object_ptr(container, &state->controller))); + state->controller.setDelegate(&state->delegate); + + const auto wrap = container->add( + object_ptr>( + container, + object_ptr( + 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); + state->controller.setLimit(state->limit); + if (state->limit == max) { + wrap->toggle(false, anim::type::instant); + } + container->resizeToWidth(container->width()); + }; + button->setClickedCallback(showMore); + showMore(); +} + +void AddBoostsList( + const Data::BoostsListSlice &firstSlice, + not_null container, + Fn)> showPeerInfo, + not_null peer, + rpl::producer title) { + const auto max = firstSlice.total; + struct State final { + State(BoostsDescriptor d) : controller(std::move(d)) { + } + PeerListContentDelegateSimple delegate; + BoostsController controller; + int limit = Api::Boosts::kFirstSlice; + }; + auto d = BoostsDescriptor{ firstSlice, std::move(showPeerInfo), peer }; + const auto state = container->lifetime().make_state(std::move(d)); + + state->delegate.setContent(container->add( + object_ptr(container, &state->controller))); + state->controller.setDelegate(&state->delegate); + + const auto wrap = container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_boosts_show_more(), + st::statisticsShowMoreButton)), + { 0, -st::settingsButton.padding.top(), 0, 0 }); + const auto button = wrap->entity(); + AddArrow(button); + + const auto showMore = [=] { + if (state->controller.skipRequest()) { + return; + } + state->limit = std::min(int(max), state->limit + Api::Boosts::kLimit); + state->controller.setLimit(state->limit); + if (state->limit == max) { + wrap->toggle(false, anim::type::instant); + } + container->resizeToWidth(container->width()); + }; + button->setClickedCallback(showMore); + if (state->limit == max) { + wrap->toggle(false, anim::type::instant); + } +} + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h new file mode 100644 index 000000000..dc619362d --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h @@ -0,0 +1,45 @@ +/* +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 + +class PeerData; + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Data { +struct BoostsListSlice; +struct PublicForwardsSlice; +struct SupergroupStatistics; +} // namespace Data + +namespace Info::Statistics { + +void AddPublicForwards( + const Data::PublicForwardsSlice &firstSlice, + not_null container, + Fn showPeerHistory, + not_null peer, + FullMsgId contextId); + +void AddMembersList( + Data::SupergroupStatistics data, + not_null container, + Fn)> showPeerInfo, + not_null peer, + rpl::producer title); + +void AddBoostsList( + const Data::BoostsListSlice &firstSlice, + not_null container, + Fn)> showPeerInfo, + not_null peer, + rpl::producer title); + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp new file mode 100644 index 000000000..e4825c712 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp @@ -0,0 +1,240 @@ +/* +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/statistics/info_statistics_recent_message.h" + +#include "core/ui_integration.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/view/history_view_item_preview.h" +#include "info/statistics/info_statistics_common.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/spoiler_mess.h" +#include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" +#include "ui/text/text_options.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_statistics.h" + +namespace Info::Statistics { +namespace { + +[[nodiscard]] QImage PreparePreviewImage( + QImage original, + ImageRoundRadius radius, + bool spoiler) { + if (original.width() * 10 < original.height() + || original.height() * 10 < original.width()) { + return QImage(); + } + const auto factor = style::DevicePixelRatio(); + const auto size = st::peerListBoxItem.photoSize * factor; + const auto scaled = original.scaled( + QSize(size, size), + Qt::KeepAspectRatioByExpanding, + Qt::FastTransformation); + auto square = scaled.copy( + (scaled.width() - size) / 2, + (scaled.height() - size) / 2, + size, + size + ).convertToFormat(QImage::Format_ARGB32_Premultiplied); + if (spoiler) { + square = Images::BlurLargeImage( + std::move(square), + style::ConvertScale(3) * factor); + } + square = Images::Round(std::move(square), radius); + square.setDevicePixelRatio(factor); + return square; +} + +} // namespace + +MessagePreview::MessagePreview( + not_null parent, + not_null item, + int views, + int shares, + QImage cachedPreview) +: Ui::RpWidget(parent) +, _item(item) +, _date( + st::statisticsHeaderTitleTextStyle, + Ui::FormatDateTime(ItemDateTime(item))) +, _views( + st::defaultPeerListItem.nameStyle, + tr::lng_stats_recent_messages_views( + tr::now, + lt_count_decimal, + views)) +, _shares( + st::statisticsHeaderTitleTextStyle, + tr::lng_stats_recent_messages_shares( + tr::now, + lt_count_decimal, + shares)) +, _viewsWidth(_views.maxWidth()) +, _sharesWidth(_shares.maxWidth()) +, _preview(std::move(cachedPreview)) { + _text.setMarkedText( + st::defaultPeerListItem.nameStyle, + _item->toPreview({ .generateImages = false }).text, + Ui::DialogTextOptions(), + Core::MarkedTextContext{ + .session = &item->history()->session(), + .customEmojiRepaint = [=] { update(); }, + }); + if (_preview.isNull()) { + processPreview(item); + } +} + +void MessagePreview::processPreview(not_null item) { + if (const auto media = item->media()) { + if (item->media()->hasSpoiler()) { + _spoiler = std::make_unique([=] { + update(); + }); + } + if (const auto photo = media->photo()) { + _photoMedia = photo->createMediaView(); + _photoMedia->wanted(Data::PhotoSize::Large, item->fullId()); + } else if (const auto document = media->document()) { + _documentMedia = document->createMediaView(); + _documentMedia->thumbnailWanted(item->fullId()); + } + } + const auto session = _photoMedia + ? &_photoMedia->owner()->session() + : _documentMedia + ? &_documentMedia->owner()->session() + : nullptr; + if (!session) { + return; + } + + struct ThumbInfo final { + bool loaded = false; + Image *image = nullptr; + }; + + const auto computeThumbInfo = [=]() -> ThumbInfo { + using Size = Data::PhotoSize; + if (_documentMedia) { + return { true, _documentMedia->thumbnail() }; + } else if (const auto large = _photoMedia->image(Size::Large)) { + return { true, large }; + } else if (const auto thumbnail = _photoMedia->image( + Size::Thumbnail)) { + return { false, thumbnail }; + } else if (const auto small = _photoMedia->image(Size::Small)) { + return { false, small }; + } else { + return { false, _photoMedia->thumbnailInline() }; + } + }; + + rpl::single(rpl::empty) | rpl::then( + session->downloaderTaskFinished() + ) | rpl::start_with_next([=] { + const auto computed = computeThumbInfo(); + if (!computed.image) { + if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) { + _preview = QImage(); + _lifetimeDownload.destroy(); + } + return; + } else if (computed.loaded) { + _lifetimeDownload.destroy(); + } + _preview = PreparePreviewImage( + computed.image->original(), + ImageRoundRadius::Large, + !!_spoiler); + }, _lifetimeDownload); +} + +int MessagePreview::resizeGetHeight(int newWidth) { + return st::peerListBoxItem.height; +} + +void MessagePreview::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto padding = st::boxRowPadding.left() / 2; + const auto rightWidth = std::max(_viewsWidth, _sharesWidth) + padding; + const auto left = _preview.isNull() + ? st::peerListBoxItem.photoPosition.x() + : st::peerListBoxItem.namePosition.x(); + if (left) { + p.drawImage(st::peerListBoxItem.photoPosition, _preview); + if (_spoiler) { + const auto rect = QRect( + st::peerListBoxItem.photoPosition, + Size(st::peerListBoxItem.photoSize)); + const auto paused = On(PowerSaving::kChatSpoiler); + FillSpoilerRect( + p, + rect, + Images::CornersMaskRef( + Images::CornersMask(st::roundRadiusLarge)), + Ui::DefaultImageSpoiler().frame( + _spoiler->index(crl::now(), paused)), + _cornerCache); + } + } + const auto topTextTop = st::peerListBoxItem.namePosition.y(); + const auto bottomTextTop = st::peerListBoxItem.statusPosition.y(); + + p.setBrush(Qt::NoBrush); + p.setPen(st::boxTextFg); + _text.draw(p, { + .position = { left, topTextTop }, + .outerWidth = width() - left, + .availableWidth = width() - rightWidth - left, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = crl::now(), + .elisionHeight = st::statisticsDetailsPopupHeaderStyle.font->height, + }); + _views.draw(p, { + .position = { width() - _viewsWidth, topTextTop }, + .outerWidth = _viewsWidth, + .availableWidth = _viewsWidth, + }); + + p.setPen(st::windowSubTextFg); + _date.draw(p, { + .position = { left, bottomTextTop }, + .outerWidth = width() - left, + .availableWidth = width() - rightWidth - left, + }); + _shares.draw(p, { + .position = { width() - _sharesWidth, bottomTextTop }, + .outerWidth = _sharesWidth, + .availableWidth = _sharesWidth, + }); +} + +void MessagePreview::saveState(SavedState &state) const { + if (!_lifetimeDownload) { + state.recentPostPreviews[_item->fullId().msg] = _preview; + } +} + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h new file mode 100644 index 000000000..147b5bcb2 --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h @@ -0,0 +1,66 @@ +/* +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 "ui/rp_widget.h" + +class HistoryItem; + +namespace Data { +class DocumentMedia; +class PhotoMedia; +} // namespace Data + +namespace Ui { +class SpoilerAnimation; +} // namespace Ui + +namespace Info::Statistics { + +struct SavedState; + +class MessagePreview final : public Ui::RpWidget { +public: + MessagePreview( + not_null parent, + not_null item, + int views, + int shares, + QImage cachedPreview); + + void saveState(SavedState &state) const; + +protected: + void paintEvent(QPaintEvent *e) override; + + int resizeGetHeight(int newWidth) override; + +private: + void processPreview(not_null item); + + not_null _item; + Ui::Text::String _text; + Ui::Text::String _date; + Ui::Text::String _views; + Ui::Text::String _shares; + + int _viewsWidth = 0; + int _sharesWidth = 0; + + QImage _cornerCache; + QImage _preview; + + std::shared_ptr _photoMedia; + std::shared_ptr _documentMedia; + std::unique_ptr _spoiler; + + rpl::lifetime _lifetimeDownload; + +}; + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp new file mode 100644 index 000000000..1edae544a --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp @@ -0,0 +1,142 @@ +/* +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/statistics/info_statistics_widget.h" + +#include "info/statistics/info_statistics_inner_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "lang/lang_keys.h" + +namespace Info::Statistics { + +Memento::Memento(not_null controller) +: ContentMemento(Tag{ + controller->statisticsPeer(), + controller->statisticsContextId(), +}) { +} + +Memento::Memento(not_null peer, FullMsgId contextId) +: ContentMemento(Tag{ peer, contextId }) { +} + +Memento::~Memento() = default; + +Section Memento::section() const { + return Section(Section::Type::Statistics); +} + +void Memento::setState(SavedState state) { + _state = std::move(state); +} + +SavedState Memento::state() { + return base::take(_state); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +Widget::Widget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) +, _inner(setInnerWidget( + object_ptr( + this, + controller, + controller->statisticsPeer(), + controller->statisticsContextId()))) { + _inner->showRequests( + ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { + if (request.history) { + controller->showPeerHistory( + request.history.peer, + Window::SectionShow::Way::Forward, + request.history.msg); + } else if (request.info) { + controller->showPeerInfo(request.info); + } else if (request.messageStatistic) { + controller->showSection(Make( + controller->statisticsPeer(), + request.messageStatistic)); + } + }, _inner->lifetime()); + _inner->scrollToRequests( + ) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) { + scrollTo(request); + }, _inner->lifetime()); +} + +not_null Widget::peer() const { + return _inner->peer(); +} + +FullMsgId Widget::contextId() const { + return _inner->contextId(); +} + +bool Widget::showInternal(not_null memento) { + return false; +} + +rpl::producer Widget::title() { + return _inner->contextId() + ? tr::lng_stats_message_title() + : tr::lng_stats_title(); +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +rpl::producer Widget::desiredShadowVisibility() const { + return rpl::single(true); +} + +void Widget::showFinished() { + _inner->showFinished(); +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared(controller()); + saveState(result.get()); + return result; +} + +void Widget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); + _inner->saveState(memento); +} + +void Widget::restoreState(not_null memento) { + _inner->restoreState(memento); + scrollTopRestore(memento->scrollTop()); +} + +std::shared_ptr Make( + not_null peer, + FullMsgId contextId) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer, contextId))); +} + +} // namespace Info::Statistics + diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.h b/Telegram/SourceFiles/info/statistics/info_statistics_widget.h new file mode 100644 index 000000000..172db8f3e --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.h @@ -0,0 +1,68 @@ +/* +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 "info/info_content_widget.h" +#include "info/statistics/info_statistics_common.h" + +namespace Info::Statistics { + +class InnerWidget; + +class Memento final : public ContentMemento { +public: + Memento(not_null controller); + Memento(not_null peer, FullMsgId contextId); + ~Memento(); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + void setState(SavedState states); + [[nodiscard]] SavedState state(); + +private: + SavedState _state; + +}; + +class Widget final : public ContentWidget { +public: + Widget(QWidget *parent, not_null controller); + + bool showInternal(not_null memento) override; + rpl::producer title() override; + rpl::producer desiredShadowVisibility() const override; + void showFinished() override; + + [[nodiscard]] not_null peer() const; + [[nodiscard]] FullMsgId contextId() const; + + void setInternalState( + const QRect &geometry, + not_null memento); + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + const not_null _inner; + +}; + +[[nodiscard]] std::shared_ptr Make( + not_null peer, + FullMsgId contextId); + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp index 95f383b1e..ffb0204fc 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp @@ -18,22 +18,26 @@ namespace Ui { namespace { void PaintExcludeTopShadow(QPainter &p, int radius, const QRect &r) { - constexpr auto kHorizontalOffset = 1; - constexpr auto kVerticalOffset = 2; + constexpr auto kHorizontalOffset = 1.; + constexpr auto kVerticalOffset = 2.; + constexpr auto kOpacityStep1 = 0.2; + constexpr auto kOpacityStep2 = 0.4; const auto opacity = p.opacity(); - p.setOpacity(opacity * 0.2); + const auto hOffset = style::ConvertScale(kHorizontalOffset); + const auto vOffset = style::ConvertScale(kVerticalOffset); + p.setOpacity(opacity * kOpacityStep1); p.drawRoundedRect( - r + QMargins(kHorizontalOffset, -radius, kHorizontalOffset, 0), + r + QMarginsF(hOffset, -radius, hOffset, 0), radius, radius); - p.setOpacity(opacity * 0.2); + p.setOpacity(opacity * kOpacityStep1); p.drawRoundedRect( - r + QMargins(0, 0, 0, kVerticalOffset), + r + QMarginsF(0, 0, 0, vOffset), radius, radius); - p.setOpacity(opacity * 0.4); + p.setOpacity(opacity * kOpacityStep2); p.drawRoundedRect( - r + QMargins(0, 0, 0, kVerticalOffset / 2), + r + QMarginsF(0, 0, 0, vOffset / 2.), radius, radius); p.setOpacity(opacity); diff --git a/Telegram/SourceFiles/intro/intro.style b/Telegram/SourceFiles/intro/intro.style index 042a744b2..e94a8fb4d 100644 --- a/Telegram/SourceFiles/intro/intro.style +++ b/Telegram/SourceFiles/intro/intro.style @@ -33,8 +33,6 @@ introCoverTitle: FlatLabel(defaultFlatLabel) { align: align(center); style: TextStyle(defaultTextStyle) { font: font(22px semibold); - linkFont: font(22px semibold); - linkFontOver: font(22px semibold underline); } } introCoverTitleTop: 136px; @@ -43,8 +41,6 @@ introCoverDescription: FlatLabel(defaultFlatLabel) { align: align(center); style: TextStyle(defaultTextStyle) { font: font(15px); - linkFont: font(15px); - linkFontOver: font(15px underline); lineHeight: 24px; } } @@ -53,8 +49,6 @@ introTitle: FlatLabel(defaultFlatLabel) { textFg: introTitleFg; style: TextStyle(defaultTextStyle) { font: font(17px semibold); - linkFont: font(17px semibold); - linkFontOver: font(17px semibold underline); } } introTitleTop: 1px; @@ -180,8 +174,6 @@ introQrTitle: FlatLabel(defaultFlatLabel) { minWidth: introQrTitleWidth; style: TextStyle(defaultTextStyle) { font: font(20px semibold); - linkFont: font(20px semibold); - linkFontOver: font(20px semibold underline); } } introQrErrorTop: 336px; diff --git a/Telegram/SourceFiles/lang/lang_instance.cpp b/Telegram/SourceFiles/lang/lang_instance.cpp index e3fb5166b..e2d26b635 100644 --- a/Telegram/SourceFiles/lang/lang_instance.cpp +++ b/Telegram/SourceFiles/lang/lang_instance.cpp @@ -133,8 +133,8 @@ bool ValueParser::readTag() { _tagsUsed.insert(_currentTagIndex); if (_currentTagReplacer.isEmpty()) { - _currentTagReplacer = QString(4, TextCommand); - _currentTagReplacer[1] = kTextCommandLangTag; + _currentTagReplacer = QString(4, QChar(kTextCommand)); + _currentTagReplacer[1] = QChar(kTextCommandLangTag); } _currentTagReplacer[2] = QChar(0x0020 + _currentTagIndex); @@ -168,8 +168,10 @@ QString PrepareTestValue(const QString ¤t, QChar filler) { auto result = QString(size + 1, filler); auto inCommand = false; for (auto i = 0; i != size; ++i) { - auto ch = current[i]; - auto newInCommand = (ch.unicode() == TextCommand) ? (!inCommand) : inCommand; + const auto ch = current[i]; + const auto newInCommand = (ch.unicode() == kTextCommand) + ? (!inCommand) + : inCommand; if (inCommand || newInCommand || ch.isSpace()) { result[i + 1] = ch; } diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index 2b0f30544..7b1a10acb 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/text/text.h" #include "base/qt/qt_common_adapters.h" +#include "base/qt/qt_string_view.h" namespace Lang { namespace { @@ -868,8 +869,10 @@ auto ChoosePlural = ChoosePluralDefault; int FindTagReplacementPosition(const QString &original, ushort tag) { for (auto s = original.constData(), ch = s, e = ch + original.size(); ch != e;) { - if (*ch == TextCommand) { - if (ch + kTagReplacementSize <= e && (ch + 1)->unicode() == kTextCommandLangTag && *(ch + 3) == TextCommand) { + if (ch->unicode() == kTextCommand) { + if (ch + kTagReplacementSize <= e + && (ch + 1)->unicode() == kTextCommandLangTag + && (ch + 3)->unicode() == kTextCommand) { if ((ch + 2)->unicode() == 0x0020 + tag) { return ch - s; } else { diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 2efa0882c..297ae1ff2 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -11,11 +11,12 @@ enum lngtag_count : int; namespace Lang { +inline constexpr auto kTextCommand = 0x10; inline constexpr auto kTextCommandLangTag = 0x20; constexpr auto kTagReplacementSize = 4; [[nodiscard]] int FindTagReplacementPosition( - const QString &original, + const QString &original, ushort tag); struct ShortenedCount { diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 8abf9f2d2..3473738cf 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -596,7 +596,7 @@ bool MainWidget::shareUrl( const auto cursor = MessageCursor{ int(url.size()) + 1, int(url.size()) + 1 + int(text.size()), - QFIXED_MAX + Ui::kQFixedMax }; const auto history = thread->owningHistory(); const auto topicRootId = thread->topicRootId(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index f314a255c..ebfe59024 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -354,8 +354,6 @@ QImage Sibling::nameImage(const SiblingLayout &layout) { _name.reset(); _nameStyle = std::make_unique(style::TextStyle{ .font = font, - .linkFont = font, - .linkFontOver = font, }); }; const auto text = _peer->isSelf() diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 6b10b078c..b386e67eb 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -194,8 +194,6 @@ mediaviewSaveMsgShown: 2000; mediaviewSaveMsgHiding: 2500; mediaviewSaveMsgStyle: TextStyle(defaultTextStyle) { font: font(16px); - linkFont: font(16px); - linkFontOver: font(16px underline); } mediaviewTextPalette: TextPalette(defaultTextPalette) { linkFg: mediaviewTextLinkFg; @@ -815,8 +813,6 @@ storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) { textFg: mediaviewControlFg; style: TextStyle(defaultTextStyle) { font: font(14px); - linkFont: font(14px); - linkFontOver: font(14px underline); lineHeight: 21px; } align: align(top); @@ -1011,7 +1007,5 @@ storiesLikesEmptyRightSkip: 2px; storiesLikeCountStyle: TextStyle(defaultTextStyle) { font: font(32px semibold); - linkFont: font(32px semibold); - linkFontOver: font(32px semibold underline); } storiesChangelogFooterWidthMin: 240px; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 9d32c1af7..d387f3500 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/ui_integration.h" #include "core/crash_reports.h" #include "core/sandbox.h" +#include "core/shortcuts.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/buttons.h" #include "ui/image/image.h" @@ -462,6 +463,19 @@ OverlayWidget::OverlayWidget() QImage::Format_ARGB32_Premultiplied); _docRectImage.setDevicePixelRatio(cIntRetinaFactor()); + Shortcuts::Requests( + ) | rpl::start_with_next([=](not_null request) { + request->check( + Shortcuts::Command::MediaViewerFullscreen + ) && request->handle([=] { + if (_streamed) { + playbackToggleFullScreen(); + return true; + } + return false; + }); + }, lifetime()); + setupWindow(); const auto mousePosition = [](not_null e) { @@ -5017,7 +5031,6 @@ void OverlayWidget::paintCaptionContent( } if (inner.intersects(clip)) { p.setPen(st::mediaviewCaptionFg); - const auto lineHeight = st::mediaviewCaptionStyle.font->height; _caption.draw(p, { .position = inner.topLeft(), .availableWidth = inner.width(), @@ -5025,7 +5038,7 @@ void OverlayWidget::paintCaptionContent( .spoiler = Ui::Text::DefaultSpoilerCache(), .pausedEmoji = On(PowerSaving::kEmojiChat), .pausedSpoiler = On(PowerSaving::kChatSpoiler), - .elisionLines = inner.height() / lineHeight, + .elisionHeight = inner.height(), .elisionRemoveFromEnd = _captionSkipBlockWidth, }); diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index ea4ebd6ec..47364e953 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -346,6 +346,8 @@ Photo::Photo( } } +Photo::~Photo() = default; + void Photo::initDimensions() { _maxw = 2 * st::overviewPhotoMinSize; _minh = _story ? qRound(_maxw * kStoryRatio) : _maxw; diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index d17ae7f6c..331d7f3e2 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -195,6 +195,7 @@ public: not_null parent, not_null photo, MediaOptions options); + ~Photo(); void initDimensions() override; int32 resizeGetHeight(int32 width) override; diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index 66877542a..c7e2a9466 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -20,8 +20,6 @@ passportPasswordLabel: FlatLabel(boxLabel) { passportPasswordLabelBold: FlatLabel(passportPasswordLabel) { style: TextStyle(boxLabelStyle) { font: font(boxFontSize semibold); - linkFont: font(boxFontSize semibold); - linkFontOver: font(boxFontSize semibold underline); } } passportPasswordSetupLabel: FlatLabel(passportPasswordLabel) { @@ -93,8 +91,7 @@ passportFormDividerHeight: 13px; passportFormLabelPadding: margins(22px, 7px, 22px, 14px); passportFormPolicy: FlatLabel(boxDividerLabel) { style: TextStyle(defaultTextStyle) { - linkFont: font(fsize semibold underline); - linkFontOver: font(fsize semibold underline); + linkUnderline: kLinkUnderlineAlways; } palette: TextPalette(defaultTextPalette) { linkFg: windowSubTextFg; diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp index dd3c894aa..750411455 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp @@ -144,6 +144,14 @@ not_null PanelForm::setupContent() { }); }, lifetime()); const auto policyUrl = _controller->privacyPolicyUrl(); + auto policyLink = tr::lng_passport_policy( + lt_bot, + rpl::single(bot->name()) + ) | Ui::Text::ToLink( + policyUrl + ) | rpl::map([=](TextWithEntities &&text) { + return Ui::Text::Wrapped(std::move(text), EntityType::Bold); + }); auto text = policyUrl.isEmpty() ? tr::lng_passport_allow( lt_bot, @@ -151,10 +159,7 @@ not_null PanelForm::setupContent() { ) | Ui::Text::ToWithEntities() : tr::lng_passport_accept_allow( lt_policy, - tr::lng_passport_policy( - lt_bot, - rpl::single(bot->name()) - ) | Ui::Text::ToLink(policyUrl), + std::move(policyLink), lt_bot, rpl::single('@' + bot->username()) | Ui::Text::ToWithEntities(), Ui::Text::WithEntities); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index cb36c5887..ec436f9e3 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -888,17 +888,13 @@ void Panel::showWebviewError( rich.append("\n\n"); switch (information.error) { case Error::NoWebview2: { - const auto command = QString(QChar(TextCommand)); - const auto text = tr::lng_payments_webview_install_edge( + rich.append(tr::lng_payments_webview_install_edge( tr::now, lt_link, - command); - const auto parts = text.split(command); - rich.append(parts.value(0)) - .append(Text::Link( + Text::Link( "Microsoft Edge WebView2 Runtime", - "https://go.microsoft.com/fwlink/p/?LinkId=2124703")) - .append(parts.value(1)); + "https://go.microsoft.com/fwlink/p/?LinkId=2124703"), + Ui::Text::WithEntities)); } break; case Error::NoWebKitGTK: rich.append(tr::lng_payments_webview_install_webkit(tr::now)); diff --git a/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp b/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp index 4f111a6e0..2f9c0d9f9 100644 --- a/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp @@ -6,40 +6,3 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/linux/file_utilities_linux.h" - -#include "platform/linux/linux_xdp_open_with_dialog.h" - -namespace Platform { -namespace File { - -bool UnsafeShowOpenWith(const QString &filepath) { - return internal::ShowXDPOpenWithDialog(filepath); -} - -} // namespace File - -namespace FileDialog { - -bool Get( - QPointer parent, - QStringList &files, - QByteArray &remoteContent, - const QString &caption, - const QString &filter, - ::FileDialog::internal::Type type, - QString startFile) { - if (parent) { - parent = parent->window(); - } - return ::FileDialog::internal::GetDefault( - parent, - files, - remoteContent, - caption, - filter, - type, - startFile); -} - -} // namespace FileDialog -} // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/file_utilities_linux.h b/Telegram/SourceFiles/platform/linux/file_utilities_linux.h index 76ccf00aa..baced1178 100644 --- a/Telegram/SourceFiles/platform/linux/file_utilities_linux.h +++ b/Telegram/SourceFiles/platform/linux/file_utilities_linux.h @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_file_utilities.h" +#include "platform/linux/linux_xdp_open_with_dialog.h" + namespace Platform { namespace File { @@ -28,6 +30,10 @@ inline bool UnsafeShowOpenWithDropdown(const QString &filepath) { return false; } +inline bool UnsafeShowOpenWith(const QString &filepath) { + return internal::ShowXDPOpenWithDialog(filepath); +} + inline void UnsafeLaunch(const QString &filepath) { return ::File::internal::UnsafeLaunchDefault(filepath); } @@ -43,5 +49,23 @@ inline void InitLastPath() { ::FileDialog::internal::InitLastPathDefault(); } +inline bool Get( + QPointer parent, + QStringList &files, + QByteArray &remoteContent, + const QString &caption, + const QString &filter, + ::FileDialog::internal::Type type, + QString startFile) { + return ::FileDialog::internal::GetDefault( + parent, + files, + remoteContent, + caption, + filter, + type, + startFile); +} + } // namespace FileDialog } // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp index 1e21f57fc..55236a156 100644 --- a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp @@ -43,6 +43,9 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { } const auto justRelaunch = action == UpdaterLaunch::JustRelaunch; + if (action == UpdaterLaunch::PerformUpdate) { + _updating = true; + } std::vector argumentsList; @@ -82,8 +85,10 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { argumentsList.push_back("-key"); argumentsList.push_back(cDataFile().toStdString()); } - argumentsList.push_back("-noupdate"); - argumentsList.push_back("-tosettings"); + if (!_updating) { + argumentsList.push_back("-noupdate"); + argumentsList.push_back("-tosettings"); + } if (customWorkingDir()) { argumentsList.push_back("-workdir"); argumentsList.push_back(cWorkingDir().toStdString()); diff --git a/Telegram/SourceFiles/platform/linux/launcher_linux.h b/Telegram/SourceFiles/platform/linux/launcher_linux.h index e0a45af0f..6c8f28e91 100644 --- a/Telegram/SourceFiles/platform/linux/launcher_linux.h +++ b/Telegram/SourceFiles/platform/linux/launcher_linux.h @@ -21,6 +21,8 @@ private: void initHook() override; bool launchUpdater(UpdaterLaunch action) override; + bool _updating = false; + }; } // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 935d85c2a..c94400ff1 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -42,6 +42,8 @@ constexpr auto kObjectPath = "/org/freedesktop/Notifications"; constexpr auto kInterface = kService; constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; +using PropertyMap = std::map; + struct ServerInformation { Glib::ustring name; Glib::ustring vendor; @@ -53,6 +55,10 @@ bool ServiceRegistered = false; ServerInformation CurrentServerInformation; std::vector CurrentCapabilities; +[[nodiscard]] bool HasCapability(const char *value) { + return ranges::contains(CurrentCapabilities, value, &Glib::ustring::raw); +} + void Noexcept(Fn callback, Fn failed = nullptr) noexcept { try { callback(); @@ -123,12 +129,13 @@ void StartServiceAsync(Fn callback) { }; const auto errorName = - Gio::DBus::ErrorUtils::get_remote_error(e).raw(); + Gio::DBus::ErrorUtils::get_remote_error(e) + .raw(); if (!ranges::contains( NotSupportedErrors, errorName)) { - throw e; + throw; } } }); @@ -192,21 +199,21 @@ void GetServerInformation(Fn callback) { Noexcept([&] { const auto reply = connection->call_finish(result); - const auto name = reply.get_child( - 0 - ).get_dynamic(); + const auto name = reply + .get_child(0) + .get_dynamic(); - const auto vendor = reply.get_child( - 1 - ).get_dynamic(); + const auto vendor = reply + .get_child(1) + .get_dynamic(); - const auto version = reply.get_child( - 2 - ).get_dynamic(); + const auto version = reply + .get_child(2) + .get_dynamic(); - const auto specVersion = reply.get_child( - 3 - ).get_dynamic(); + const auto specVersion = reply + .get_child(3) + .get_dynamic(); callback(ServerInformation{ name, @@ -241,11 +248,9 @@ void GetCapabilities(Fn &)> callback) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { callback( - connection->call_finish( - result - ).get_child( - 0 - ).get_dynamic>() + connection->call_finish(result) + .get_child(0) + .get_dynamic>() ); }, [&] { callback({}); @@ -275,12 +280,10 @@ void GetInhibited(Fn callback) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { callback( - connection->call_finish( - result - ).get_child( - 0 - ).get_dynamic>( - ).get() + connection->call_finish(result) + .get_child(0) + .get_dynamic>() + .get() ); }, [&] { callback(false); @@ -412,7 +415,7 @@ bool NotificationData::init( static const auto set_category = [] { // reset dlerror after dlsym call const auto guard = gsl::finally([] { dlerror(); }); - return reinterpret_cast( + return reinterpret_cast( dlsym(RTLD_DEFAULT, "g_notification_set_category")); }(); @@ -446,7 +449,6 @@ bool NotificationData::init( } const auto weak = base::make_weak(this); - const auto &capabilities = CurrentCapabilities; const auto signalEmitted = crl::guard(weak, [=]( const Glib::RefPtr &connection, @@ -458,35 +460,43 @@ bool NotificationData::init( Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { if (signal_name == "ActionInvoked") { - const auto id = parameters.get_child(0).get_dynamic(); + const auto id = parameters + .get_child(0) + .get_dynamic(); - const auto actionName = parameters.get_child( - 1 - ).get_dynamic(); + const auto actionName = parameters + .get_child(1) + .get_dynamic(); actionInvoked(id, actionName); } else if (signal_name == "ActivationToken") { - const auto id = parameters.get_child(0).get_dynamic(); + const auto id = parameters + .get_child(0) + .get_dynamic(); - const auto token = parameters.get_child( - 1 - ).get_dynamic(); + const auto token = parameters + .get_child(1) + .get_dynamic(); activationToken(id, token); } else if (signal_name == "NotificationReplied") { - const auto id = parameters.get_child(0).get_dynamic(); + const auto id = parameters + .get_child(0) + .get_dynamic(); - const auto text = parameters.get_child( - 1 - ).get_dynamic(); + const auto text = parameters + .get_child(1) + .get_dynamic(); notificationReplied(id, text); } else if (signal_name == "NotificationClosed") { - const auto id = parameters.get_child(0).get_dynamic(); + const auto id = parameters + .get_child(0) + .get_dynamic(); - const auto reason = parameters.get_child( - 1 - ).get_dynamic(); + const auto reason = parameters + .get_child(1) + .get_dynamic(); notificationClosed(id, reason); } @@ -496,7 +506,7 @@ bool NotificationData::init( _imageKey = GetImageKey(CurrentServerInformation.specVersion); - if (ranges::contains(capabilities, "body-markup")) { + if (HasCapability("body-markup")) { _title = title.toStdString(); _body = subtitle.isEmpty() @@ -512,7 +522,7 @@ bool NotificationData::init( _body = msg.toStdString(); } - if (ranges::contains(capabilities, "actions")) { + if (HasCapability("actions")) { _actions.push_back("default"); _actions.push_back(tr::lng_open_link(tr::now).toStdString()); @@ -523,7 +533,7 @@ bool NotificationData::init( tr::lng_context_mark_read(tr::now).toStdString()); } - if (ranges::contains(capabilities, "inline-reply") + if (HasCapability("inline-reply") && !options.hideReplyButton) { _actions.push_back("inline-reply"); _actions.push_back( @@ -553,13 +563,13 @@ bool NotificationData::init( kObjectPath); } - if (ranges::contains(capabilities, "action-icons")) { + if (HasCapability("action-icons")) { _hints["action-icons"] = Glib::create_variant(true); } // suppress system sound if telegram sound activated, // otherwise use system sound - if (ranges::contains(capabilities, "sound")) { + if (HasCapability("sound")) { if (Core::App().settings().soundNotify()) { _hints["suppress-sound"] = Glib::create_variant(true); } else { @@ -569,7 +579,7 @@ bool NotificationData::init( } } - if (ranges::contains(capabilities, "x-canonical-append")) { + if (HasCapability("x-canonical-append")) { _hints["x-canonical-append"] = Glib::create_variant( Glib::ustring("true")); } @@ -637,14 +647,13 @@ void NotificationData::show() { _hints, -1, }), - crl::guard(weak, [=](const Glib::RefPtr &result) { + crl::guard(weak, [=]( + const Glib::RefPtr &result) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { - _notificationId = connection->call_finish( - result - ).get_child( - 0 - ).get_dynamic(); + _notificationId = connection->call_finish(result) + .get_child(0) + .get_dynamic(); }, [&] { _manager->clearNotification(_id); }); @@ -805,7 +814,7 @@ bool ByDefault() { // A list of capabilities that offer feature parity // with custom notifications - return ranges::all_of(std::initializer_list{ + return ranges::all_of(std::array{ // To show message content "body", // To have buttons on notifications @@ -816,7 +825,7 @@ bool ByDefault() { // (no, using sound capability is not a way) "inhibitions", }, [](const auto *capability) { - return ranges::contains(CurrentCapabilities, capability); + return HasCapability(capability); }); } @@ -909,7 +918,6 @@ private: Manager::Private::Private(not_null manager) : _manager(manager) { const auto &serverInformation = CurrentServerInformation; - const auto &capabilities = CurrentCapabilities; if (!serverInformation.name.empty()) { LOG(("Notification daemon product name: %1") @@ -931,17 +939,17 @@ Manager::Private::Private(not_null manager) .arg(serverInformation.specVersion.toString())); } - if (!capabilities.empty()) { + if (!CurrentCapabilities.empty()) { LOG(("Notification daemon capabilities: %1").arg( ranges::fold_left( - capabilities, + CurrentCapabilities, "", [](const Glib::ustring &a, const Glib::ustring &b) { return a + (a.empty() ? "" : ", ") + b; }).c_str())); } - if (ranges::contains(capabilities, "inhibitions")) { + if (HasCapability("inhibitions")) { Noexcept([&] { _dbusConnection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); @@ -966,20 +974,19 @@ Manager::Private::Private(not_null manager) const Glib::VariantContainerBase ¶meters) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { - const auto interface = parameters.get_child( - 0 - ).get_dynamic(); + const auto interface = parameters + .get_child(0) + .get_dynamic(); if (interface != kInterface) { return; } - _inhibited = parameters.get_child( - 1 - ).get_dynamic>( - ).at( - "Inhibited" - ).get_dynamic(); + _inhibited = parameters + .get_child(1) + .get_dynamic() + .at("Inhibited") + .get_dynamic(); }); }); }), diff --git a/Telegram/SourceFiles/platform/win/main_window_win.cpp b/Telegram/SourceFiles/platform/win/main_window_win.cpp index 39a3c8f9a..45ec3fc6c 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.cpp +++ b/Telegram/SourceFiles/platform/win/main_window_win.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include #include #include diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h index ed1d94c7c..d18bca048 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h @@ -162,7 +162,7 @@ public: } [[nodiscard]] static Type Id() { - return &SectionMetaImplementation::Meta; + return SectionFactory::Instance(); } [[nodiscard]] Type id() const final override { return Id(); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 45173b569..84cb8960a 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -102,8 +102,6 @@ settingsStoriesIconLinks: icon {{ "menu/links_profile", premiumButtonBg1 }}; settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) { style: TextStyle(semiboldTextStyle) { font: font(10px semibold); - linkFont: font(10px semibold); - linkFontOver: font(10px semibold); } textFg: windowFgActive; } @@ -133,8 +131,6 @@ settingsThumbSkip: 16px; settingsSubsectionTitle: FlatLabel(defaultFlatLabel) { style: TextStyle(semiboldTextStyle) { font: font(boxFontSize semibold); - linkFont: font(boxFontSize semibold); - linkFontOver: font(boxFontSize semibold underline); } textFg: windowActiveTextFg; minWidth: 240px; @@ -215,8 +211,6 @@ settingsCoverName: FlatLabel(defaultFlatLabel) { maxHeight: 24px; style: TextStyle(defaultTextStyle) { font: font(17px semibold); - linkFont: font(17px semibold); - linkFontOver: font(17px semibold); } } settingsCoverStatus: FlatLabel(defaultFlatLabel) { @@ -305,8 +299,6 @@ settingsDeviceName: InputField(defaultInputField) { dictionariesSectionButton: SettingsButton(settingsUpdateToggle) { style: TextStyle(defaultTextStyle) { font: font(14px semibold); - linkFont: font(14px semibold); - linkFontOver: font(14px semibold underline); } } @@ -349,8 +341,6 @@ sessionBigName: FlatLabel(defaultFlatLabel) { maxHeight: 29px; style: TextStyle(defaultTextStyle) { font: font(20px semibold); - linkFont: font(20px semibold); - linkFontOver: font(20px semibold underline); } align: align(top); } @@ -480,8 +470,7 @@ settingsPremiumPreviewIconPosition: point(20px, 7px); settingsPremiumTitlePadding: margins(0px, 18px, 0px, 11px); settingsPremiumAboutTextStyle: TextStyle(defaultTextStyle) { font: font(12px); - linkFont: font(12px underline); - linkFontOver: font(12px underline); + linkUnderline: kLinkUnderlineAlways; lineHeight: 18px; } settingsPremiumAbout: FlatLabel(defaultFlatLabel) { @@ -504,8 +493,6 @@ settingsPremiumUserTitlePadding: margins(0px, 16px, 0px, 6px); settingsPremiumUserTitle: FlatLabel(boxTitle) { style: TextStyle(defaultTextStyle) { font: boxTitleFont; - linkFont: boxTitleFont; - linkFontOver: font(16px semibold underline); lineHeight: 14px; } minWidth: 300px; @@ -566,13 +553,9 @@ filterInviteBox: Box(defaultBox) { } filterInviteButtonStyle: TextStyle(defaultTextStyle) { font: font(13px semibold); - linkFont: font(13px underline); - linkFontOver: font(13px underline); } filterInviteButtonBadgeStyle: TextStyle(defaultTextStyle) { font: font(12px semibold); - linkFont: font(12px underline); - linkFontOver: font(12px underline); } filterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px); filterInviteButtonBadgeSkip: 5px; @@ -580,8 +563,6 @@ filterLinkDividerLabelPadding: margins(0px, 10px, 0px, 17px); filterLinkTitlePadding: margins(0px, 15px, 0px, 17px); filterLinkAboutTextStyle: TextStyle(defaultTextStyle) { font: font(12px); - linkFont: font(12px underline); - linkFontOver: font(12px underline); lineHeight: 17px; } filterLinkAbout: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index d601a1c1d..0a7a48384 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -52,14 +52,16 @@ using Button = Ui::SettingsButton; class AbstractSection; -struct SectionMeta { +struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr create( not_null parent, not_null controller) const = 0; + + virtual ~AbstractSectionFactory() = default; }; template -struct SectionMetaImplementation : SectionMeta { +struct SectionFactory : AbstractSectionFactory { object_ptr create( not_null parent, not_null controller @@ -67,9 +69,9 @@ struct SectionMetaImplementation : SectionMeta { return object_ptr(parent, controller); } - [[nodiscard]] static not_null Meta() { - static SectionMetaImplementation result; - return &result; + [[nodiscard]] static const std::shared_ptr &Instance() { + static const auto result = std::make_shared(); + return result; } }; @@ -120,7 +122,7 @@ public: using AbstractSection::AbstractSection; [[nodiscard]] static Type Id() { - return &SectionMetaImplementation::Meta; + return SectionFactory::Instance(); } [[nodiscard]] Type id() const final override { return Id(); diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.cpp b/Telegram/SourceFiles/settings/settings_local_passcode.cpp index 49e8ba019..77dad4027 100644 --- a/Telegram/SourceFiles/settings/settings_local_passcode.cpp +++ b/Telegram/SourceFiles/settings/settings_local_passcode.cpp @@ -330,7 +330,7 @@ public: } [[nodiscard]] static Type Id() { - return &SectionMetaImplementation::Meta; + return SectionFactory::Instance(); } [[nodiscard]] Type id() const final override { return Id(); diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index fb8397903..d2076f86a 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -171,7 +171,7 @@ void AddTypeButton( st::settingsNotificationType, { icon }); button->setClickedCallback([=] { - showOther(NotificationsTypeId(type)); + showOther(NotificationsType::Id(type)); }); const auto session = &controller->session(); @@ -290,7 +290,7 @@ void AddTypeButton( tr::lng_notification_exceptions_view(), [=] { box->closeBox(); - showOther(NotificationsTypeId(type)); + showOther(NotificationsType::Id(type)); }); })); } diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index 5f394face..f1240938f 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -36,6 +36,20 @@ namespace { using Notify = Data::DefaultNotify; +struct Factory : AbstractSectionFactory { + explicit Factory(Notify type) : type(type) { + } + + object_ptr create( + not_null parent, + not_null controller + ) const final override { + return object_ptr(parent, controller, type); + } + + const Notify type = {}; +}; + class AddExceptionBoxController final : public ChatsListBoxController , public base::has_weak_ptr { @@ -351,11 +365,6 @@ void ExceptionsController::sort() { delegate()->peerListSortRows(predicate); } -template -[[nodiscard]] Type Id() { - return &NotificationsTypeMetaImplementation::Meta; -} - [[nodiscard]] rpl::producer Title(Notify type) { switch (type) { case Notify::User: return tr::lng_notification_title_private_chats(); @@ -562,15 +571,6 @@ void SetupExceptions( } // namespace -Type NotificationsTypeId(Notify type) { - switch (type) { - case Notify::User: return Id(); - case Notify::Group: return Id(); - case Notify::Broadcast: return Id(); - } - Unexpected("Type in NotificationTypeId."); -} - NotificationsType::NotificationsType( QWidget *parent, not_null controller, @@ -589,8 +589,8 @@ rpl::producer NotificationsType::title() { Unexpected("Type in NotificationsType."); } -Type NotificationsType::id() const { - return NotificationsTypeId(_type); +Type NotificationsType::Id(Notify type) { + return std::make_shared(type); } void NotificationsType::setupContent( diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.h b/Telegram/SourceFiles/settings/settings_notifications_type.h index 59014f0d5..c5f6d95d4 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.h +++ b/Telegram/SourceFiles/settings/settings_notifications_type.h @@ -25,32 +25,19 @@ public: [[nodiscard]] rpl::producer title() override; - [[nodiscard]] Type id() const final override; + [[nodiscard]] static Type Id(Data::DefaultNotify type); + + [[nodiscard]] Type id() const final override { + return Id(_type); + } private: void setupContent(not_null controller); - Data::DefaultNotify _type; + const Data::DefaultNotify _type; }; -template -struct NotificationsTypeMetaImplementation : SectionMeta { - object_ptr create( - not_null parent, - not_null controller - ) const final override { - return object_ptr(parent, controller, kType); - } - - [[nodiscard]] static not_null Meta() { - static NotificationsTypeMetaImplementation result; - return &result; - } -}; - -[[nodiscard]] Type NotificationsTypeId(Data::DefaultNotify type); - [[nodiscard]] bool NotificationsEnabledForType( not_null session, Data::DefaultNotify type); diff --git a/Telegram/SourceFiles/settings/settings_scale_preview.cpp b/Telegram/SourceFiles/settings/settings_scale_preview.cpp index 251814630..6da0a861e 100644 --- a/Telegram/SourceFiles/settings/settings_scale_preview.cpp +++ b/Telegram/SourceFiles/settings/settings_scale_preview.cpp @@ -73,13 +73,16 @@ private: void validateShadowCache(); [[nodiscard]] int scaled(int value) const; + [[nodiscard]] QPoint scaled(QPoint value) const; [[nodiscard]] QMargins scaled(QMargins value) const; [[nodiscard]] style::font scaled( - const style::font &value, int size) const; + const style::font &value, + int size) const; + [[nodiscard]] style::QuoteStyle scaled( + const style::QuoteStyle &value) const; [[nodiscard]] style::TextStyle scaled( const style::TextStyle &value, - int fontSize, - int lineHeight) const; + int fontSize) const; [[nodiscard]] QImage scaled( const style::icon &icon, const QColor &color) const; @@ -307,6 +310,10 @@ int Preview::scaled(int value) const { return style::ConvertScale(value, _scale); } +QPoint Preview::scaled(QPoint value) const { + return { scaled(value.x()), scaled(value.y()) }; +} + QMargins Preview::scaled(QMargins value) const { return { scaled(value.left()), @@ -320,15 +327,29 @@ style::font Preview::scaled(const style::font &font, int size) const { return style::font(scaled(size), font->flags(), font->family()); } +style::QuoteStyle Preview::scaled(const style::QuoteStyle &value) const { + return { + .padding = scaled(value.padding), + .verticalSkip = scaled(value.verticalSkip), + .header = scaled(value.header), + .headerPosition = scaled(value.headerPosition), + .icon = value.icon, + .iconPosition = scaled(value.iconPosition), + .outline = scaled(value.outline), + .radius = scaled(value.radius), + .scrollable = value.scrollable, + }; +} + style::TextStyle Preview::scaled( const style::TextStyle &value, - int fontSize, - int lineHeight) const { + int fontSize) const { return { .font = scaled(value.font, fontSize), - .linkFont = scaled(value.linkFont, fontSize), - .linkFontOver = scaled(value.linkFontOver, fontSize), + .linkUnderline = value.linkUnderline, .lineHeight = scaled(value.lineHeight), + .blockquote = scaled(value.blockquote), + .pre = scaled(value.pre), }; } @@ -345,8 +366,8 @@ void Preview::updateToScale(int scale) { return; } _scale = scale; - _nameStyle = scaled(_nameStyle, 13, 0); - _textStyle = scaled(_textStyle, 13, 0); + _nameStyle = scaled(_nameStyle, 13); + _textStyle = scaled(_textStyle, 13); _nameText.setText( _nameStyle, u"Bob Harris"_q, diff --git a/Telegram/SourceFiles/settings/settings_type.h b/Telegram/SourceFiles/settings/settings_type.h index 76b8d9840..ee5460916 100644 --- a/Telegram/SourceFiles/settings/settings_type.h +++ b/Telegram/SourceFiles/settings/settings_type.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { -struct SectionMeta; -using Type = not_null(*)(); +struct AbstractSectionFactory; +using Type = std::shared_ptr; } // namespace Settings diff --git a/Telegram/SourceFiles/statistics/chart_lines_filter_controller.cpp b/Telegram/SourceFiles/statistics/chart_lines_filter_controller.cpp new file mode 100644 index 000000000..40c3b6129 --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_lines_filter_controller.cpp @@ -0,0 +1,71 @@ +/* +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 "statistics/chart_lines_filter_controller.h" + +namespace Statistic { + +LinesFilterController::LinesFilterController() = default; + +void LinesFilterController::setEnabled(int id, bool enabled, crl::time now) { + const auto it = _entries.find(id); + if (it == end(_entries)) { + _entries[id] = Entry{ + .enabled = enabled, + .startedAt = now, + .anim = anim::value(enabled ? 0. : 1., enabled ? 1. : 0.), + }; + } else if (it->second.enabled != enabled) { + auto &entry = it->second; + entry.enabled = enabled; + entry.startedAt = now; + entry.dtCurrent = 0.; + entry.anim.start(enabled ? 1. : 0.); + } + _isFinished = false; +} + +bool LinesFilterController::isFinished() const { + return _isFinished; +} + +bool LinesFilterController::isEnabled(int id) const { + const auto it = _entries.find(id); + return (it == end(_entries)) ? true : it->second.enabled; +} + +float64 LinesFilterController::alpha(int id) const { + const auto it = _entries.find(id); + return (it == end(_entries)) ? 1. : it->second.alpha; +} + +void LinesFilterController::tick(float64 dtSpeed) { + auto finishedCount = 0; + auto idsToRemove = std::vector(); + for (auto &[id, entry] : _entries) { + if (!entry.startedAt) { + continue; + } + entry.dtCurrent = std::min(entry.dtCurrent + dtSpeed, 1.); + entry.anim.update(entry.dtCurrent, anim::easeInCubic); + const auto progress = entry.anim.current(); + entry.alpha = std::clamp(progress, 0., 1.); + if ((entry.alpha == 1.) && entry.enabled) { + idsToRemove.push_back(id); + } + if (entry.anim.current() == entry.anim.to()) { + finishedCount++; + entry.anim.finish(); + } + } + _isFinished = (finishedCount == _entries.size()); + for (const auto &id : idsToRemove) { + _entries.remove(id); + } +} + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/chart_lines_filter_controller.h b/Telegram/SourceFiles/statistics/chart_lines_filter_controller.h new file mode 100644 index 000000000..417cb23f3 --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_lines_filter_controller.h @@ -0,0 +1,39 @@ +/* +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 "ui/effects/animation_value.h" + +namespace Statistic { + +class LinesFilterController final { +public: + LinesFilterController(); + + void setEnabled(int id, bool enabled, crl::time now); + [[nodiscard]] bool isEnabled(int id) const; + [[nodiscard]] bool isFinished() const; + [[nodiscard]] float64 alpha(int id) const; + + void tick(float64 dtSpeed); + +private: + struct Entry final { + bool enabled = false; + crl::time startedAt = 0; + float64 alpha = 1.; + anim::value anim; + float64 dtCurrent = 0.; + }; + + base::flat_map _entries; + bool _isFinished = true; + +}; + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp new file mode 100644 index 000000000..f4a8f9edd --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp @@ -0,0 +1,125 @@ +/* +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 "statistics/chart_rulers_data.h" + +#include "lang/lang_tag.h" + +namespace Statistic { +namespace { + +constexpr auto kMinLines = int(2); +constexpr auto kMaxLines = int(6); +constexpr auto kStep = 5.; + +[[nodiscard]] int Round(int maxValue) { + const auto k = int(maxValue / kStep); + return (k % 10 == 0) ? maxValue : ((maxValue / 10 + 1) * 10); +} + +[[nodiscard]] QString Format(int absoluteValue) { + constexpr auto kTooMuch = int(10'000); + return (absoluteValue >= kTooMuch) + ? Lang::FormatCountToShort(absoluteValue).string + : QString::number(absoluteValue); +} + +} // namespace + +ChartRulersData::ChartRulersData( + int newMaxHeight, + int newMinHeight, + bool useMinHeight, + float64 rightRatio) { + if (!useMinHeight) { + const auto v = (newMaxHeight > 100) + ? Round(newMaxHeight) + : newMaxHeight; + + const auto step = std::max(1, int(std::ceil(v / kStep))); + + auto n = kMaxLines; + if (v < kMaxLines) { + n = std::max(2, v + 1); + } else if (v / 2 < kMaxLines) { + n = v / 2 + 1; + if (v % 2 != 0) { + n++; + } + } + + lines.resize(n); + + for (auto i = 1; i < n; i++) { + auto &line = lines[i]; + line.absoluteValue = i * step; + line.caption = Lang::FormatCountToShort( + line.absoluteValue).string; + } + } else { + auto n = int(0); + const auto diff = newMaxHeight - newMinHeight; + auto step = 0.; + if (diff == 0) { + newMinHeight--; + n = kMaxLines / 2; + step = 1.; + } else if (diff < kMaxLines) { + n = std::max(kMinLines, diff + 1); + step = 1.; + } else if (diff / 2 < kMaxLines) { + n = diff / 2 + diff % 2 + 1; + step = 2.; + } else { + step = (newMaxHeight - newMinHeight) / kStep; + if (step <= 0) { + step = 1; + n = std::max(kMinLines, newMaxHeight - newMinHeight + 1); + } else { + n = 6; + } + } + + lines.resize(n); + const auto diffAbsoluteValue = int((n - 1) * step); + const auto skipFloatValues = (step / rightRatio) < 1; + for (auto i = 0; i < n; i++) { + auto &line = lines[i]; + const auto value = int(i * step); + line.absoluteValue = newMinHeight + value; + line.relativeValue = 1. - value / float64(diffAbsoluteValue); + line.caption = Format(line.absoluteValue); + if (rightRatio > 0) { + const auto v = (newMinHeight + i * step) / rightRatio; + line.scaledLineCaption = (!skipFloatValues) + ? Format(v) + : ((v - int(v)) < 0.01) + ? Format(v) + : QString(); + } + } + } +} + +void ChartRulersData::computeRelative( + int newMaxHeight, + int newMinHeight) { + for (auto &line : lines) { + line.relativeValue = 1. + - ((line.absoluteValue - newMinHeight) + / (newMaxHeight - newMinHeight)); + } +} + +int ChartRulersData::LookupHeight(int maxValue) { + const auto v = (maxValue > 100) ? Round(maxValue) : maxValue; + + const auto step = int(std::ceil(v / kStep)); + return step * kStep; +} + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/chart_rulers_data.h b/Telegram/SourceFiles/statistics/chart_rulers_data.h new file mode 100644 index 000000000..41833ce67 --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_rulers_data.h @@ -0,0 +1,40 @@ +/* +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 Statistic { + +struct ChartRulersData final { +public: + ChartRulersData( + int newMaxHeight, + int newMinHeight, + bool useMinHeight, + float64 rightRatio); + + void computeRelative( + int newMaxHeight, + int newMinHeight); + + [[nodiscard]] static int LookupHeight(int maxValue); + + struct Line final { + float64 absoluteValue = 0.; + float64 relativeValue = 0.; + QString caption; + QString scaledLineCaption; + float64 rightCaptionWidth = 0.; + }; + + std::vector lines; + float64 alpha = 0.; + float64 fixedAlpha = 1.; + +}; + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/chart_widget.cpp b/Telegram/SourceFiles/statistics/chart_widget.cpp new file mode 100644 index 000000000..f63fbfab3 --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_widget.cpp @@ -0,0 +1,1563 @@ +/* +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 "statistics/chart_widget.h" + +#include "base/qt/qt_key_modifiers.h" +#include "lang/lang_keys.h" +#include "statistics/chart_lines_filter_controller.h" +#include "statistics/view/abstract_chart_view.h" +#include "statistics/view/chart_view_factory.h" +#include "statistics/view/stack_chart_common.h" +#include "statistics/widgets/chart_header_widget.h" +#include "statistics/widgets/chart_lines_filter_widget.h" +#include "statistics/widgets/point_details_widget.h" +#include "ui/abstract_button.h" +#include "ui/effects/animation_value_f.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/show_animation.h" +#include "ui/image/image_prepare.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/buttons.h" +#include "styles/style_layers.h" +#include "styles/style_statistics.h" + +namespace Statistic { + +namespace { + +constexpr auto kHeightLimitsUpdateTimeout = crl::time(320); + +inline float64 InterpolationRatio(float64 from, float64 to, float64 result) { + return (result - from) / (to - from); +}; + +void FillLineColorsByKey(Data::StatisticalChart &chartData) { + for (auto &line : chartData.lines) { + if (line.colorKey == u"BLUE"_q) { + line.color = st::statisticsChartLineBlue->c; + } else if (line.colorKey == u"GREEN"_q) { + line.color = st::statisticsChartLineGreen->c; + } else if (line.colorKey == u"RED"_q) { + line.color = st::statisticsChartLineRed->c; + } else if (line.colorKey == u"GOLDEN"_q) { + line.color = st::statisticsChartLineGolden->c; + } else if (line.colorKey == u"LIGHTBLUE"_q) { + line.color = st::statisticsChartLineLightblue->c; + } else if (line.colorKey == u"LIGHTGREEN"_q) { + line.color = st::statisticsChartLineLightgreen->c; + } else if (line.colorKey == u"ORANGE"_q) { + line.color = st::statisticsChartLineOrange->c; + } else if (line.colorKey == u"INDIGO"_q) { + line.color = st::statisticsChartLineIndigo->c; + } else if (line.colorKey == u"PURPLE"_q) { + line.color = st::statisticsChartLinePurple->c; + } else if (line.colorKey == u"CYAN"_q) { + line.color = st::statisticsChartLineCyan->c; + } + } +} + +[[nodiscard]] QString HeaderSubTitle( + const Data::StatisticalChart &chartData, + int xIndexMin, + int xIndexMax) { + constexpr auto kOneDay = 3600 * 24 * 1000; + const auto leftTimestamp = chartData.x[xIndexMin]; + if (leftTimestamp < kOneDay) { + return {}; + } + const auto formatter = u"d MMM yyyy"_q; + const auto leftDateTime = QDateTime::fromSecsSinceEpoch( + leftTimestamp / 1000); + const auto leftText = QLocale().toString(leftDateTime.date(), formatter); + if ((xIndexMin == xIndexMax) && !chartData.weekFormat) { + return leftText; + } else { + constexpr auto kSevenDays = 3600 * 24 * 7; + const auto rightDateTime = QDateTime::fromSecsSinceEpoch(0 + + (chartData.x[xIndexMax] / 1000) + + (chartData.weekFormat ? kSevenDays : 0)); + return leftText + + ' ' + + QChar(8212) + + ' ' + + QLocale().toString(rightDateTime.date(), formatter); + } +} + +void PaintBottomLine( + QPainter &p, + const std::vector &dates, + Data::StatisticalChart &chartData, + const Limits &xPercentageLimits, + int fullWidth, + int chartWidth, + int y, + int captionIndicesOffset) { + p.setFont(st::statisticsDetailsBottomCaptionStyle.font); + const auto opacity = p.opacity(); + + const auto startXIndex = chartData.findStartIndex( + xPercentageLimits.min); + const auto endXIndex = chartData.findEndIndex( + startXIndex, + xPercentageLimits.max); + + const auto edgeAlphaSize = st::statisticsChartBottomCaptionMaxWidth / 4.; + + for (auto k = 0; k < dates.size(); k++) { + const auto &date = dates[k]; + const auto isLast = (k == dates.size() - 1); + const auto resultAlpha = date.alpha; + const auto step = std::max(date.step, 1); + + auto start = startXIndex - captionIndicesOffset; + while (start % step != 0) { + start--; + } + + auto end = endXIndex - captionIndicesOffset; + while ((end % step != 0) || end < (chartData.x.size() - 1)) { + end++; + } + + start += captionIndicesOffset; + end += captionIndicesOffset; + + const auto offset = fullWidth * xPercentageLimits.min; + + // 30 ms / 200 ms = 0.15. + constexpr auto kFastAlphaSpeed = 0.85; + const auto hasFastAlpha = (date.stepRaw < dates.back().stepMinFast); + const auto fastAlpha = isLast + ? 1. + : std::max(resultAlpha - kFastAlphaSpeed, 0.); + + for (auto i = start; i < end; i += step) { + if ((i < 0) || (i >= (chartData.x.size() - 1))) { + continue; + } + const auto xPercentage = (chartData.x[i] - chartData.x.front()) + / float64(chartData.x.back() - chartData.x.front()); + const auto xPoint = xPercentage * fullWidth - offset; + const auto r = QRectF( + xPoint - st::statisticsChartBottomCaptionMaxWidth / 2., + y, + st::statisticsChartBottomCaptionMaxWidth, + st::statisticsChartBottomCaptionHeight); + const auto edgeAlpha = (r.x() < 0) + ? std::max( + 0., + 1. + (r.x() / edgeAlphaSize)) + : (rect::right(r) > chartWidth) + ? std::max( + 0., + 1. + ((chartWidth - rect::right(r)) / edgeAlphaSize)) + : 1.; + p.setOpacity(opacity + * edgeAlpha + * (hasFastAlpha ? fastAlpha : resultAlpha)); + p.drawText(r, chartData.getDayString(i), style::al_center); + } + } +} + +} // namespace + +class RpMouseWidget : public Ui::AbstractButton { +public: + using Ui::AbstractButton::AbstractButton; + + struct State { + QPoint point; + QEvent::Type mouseState; + }; + + [[nodiscard]] const QPoint &start() const; + [[nodiscard]] rpl::producer mouseStateChanged() const; + +protected: + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + +private: + QPoint _start = QPoint(-1, -1); + + rpl::event_stream _mouseStateChanged; + +}; + +const QPoint &RpMouseWidget::start() const { + return _start; +} + +rpl::producer RpMouseWidget::mouseStateChanged() const { + return _mouseStateChanged.events(); +} + +void RpMouseWidget::mousePressEvent(QMouseEvent *e) { + _start = e->pos(); + _mouseStateChanged.fire({ e->pos(), QEvent::MouseButtonPress }); +} + +void RpMouseWidget::mouseMoveEvent(QMouseEvent *e) { + if (_start.x() >= 0 || _start.y() >= 0) { + _mouseStateChanged.fire({ e->pos(), QEvent::MouseMove }); + } +} + +void RpMouseWidget::mouseReleaseEvent(QMouseEvent *e) { + _start = { -1, -1 }; + _mouseStateChanged.fire({ e->pos(), QEvent::MouseButtonRelease }); +} + +class ChartWidget::Footer final : public RpMouseWidget { +public: + using PaintCallback = Fn; + + explicit Footer(not_null parent); + + void setXPercentageLimits(const Limits &xLimits); + + [[nodiscard]] Limits xPercentageLimits() const; + [[nodiscard]] rpl::producer xPercentageLimitsChange() const; + + void setPaintChartCallback(PaintCallback paintChartCallback); + +protected: + void paintEvent(QPaintEvent *e) override; + + int resizeGetHeight(int newWidth) override; + +private: + void moveSide(bool left, float64 x); + void moveCenter( + bool isDirectionToLeft, + float64 x, + float64 diffBetweenStartAndLeft); + + void fire() const; + + enum class DragArea { + None, + Middle, + Left, + Right, + }; + DragArea _dragArea = DragArea::None; + float64 _diffBetweenStartAndSide = 0; + Ui::Animations::Simple _moveCenterAnimation; + bool _draggedAfterPress = false; + + const QPen _sidePen; + + float64 _width = 0.; + float64 _widthBetweenSides = 0.; + + PaintCallback _paintChartCallback; + + QImage _frame; + QImage _mask; + + Limits _leftSide; + Limits _rightSide; + + rpl::event_stream _xPercentageLimitsChange; + +}; + +ChartWidget::Footer::Footer(not_null parent) +: RpMouseWidget(parent) +, _sidePen( + st::premiumButtonFg, + st::statisticsChartLineWidth, + Qt::SolidLine, + Qt::RoundCap) { + sizeValue( + ) | rpl::take(2) | rpl::start_with_next([=](const QSize &s) { + moveSide(false, s.width()); + moveSide(true, 0); + update(); + }, lifetime()); + + mouseStateChanged( + ) | rpl::start_with_next([=](const RpMouseWidget::State &state) { + if (_moveCenterAnimation.animating()) { + return; + } + + const auto posX = state.point.x(); + const auto isLeftSide = (posX >= _leftSide.min) + && (posX <= _leftSide.max); + const auto isRightSide = !isLeftSide + && (posX >= _rightSide.min) + && (posX <= _rightSide.max); + switch (state.mouseState) { + case QEvent::MouseMove: { + _draggedAfterPress = true; + if (_dragArea == DragArea::None) { + return; + } + const auto resultX = posX - _diffBetweenStartAndSide; + if (_dragArea == DragArea::Right) { + moveSide(false, resultX); + } else if (_dragArea == DragArea::Left) { + moveSide(true, resultX); + } else if (_dragArea == DragArea::Middle) { + const auto toLeft = (posX + - _diffBetweenStartAndSide + - _leftSide.min) <= 0; + moveCenter(toLeft, posX, _diffBetweenStartAndSide); + } + fire(); + } break; + case QEvent::MouseButtonPress: { + _draggedAfterPress = false; + _dragArea = isLeftSide + ? DragArea::Left + : isRightSide + ? DragArea::Right + : ((posX < _leftSide.min) || (posX > _rightSide.max)) + ? DragArea::None + : DragArea::Middle; + _diffBetweenStartAndSide = isRightSide + ? (start().x() - _rightSide.min) + : (start().x() - _leftSide.min); + } break; + case QEvent::MouseButtonRelease: { + const auto finish = [=] { + _dragArea = DragArea::None; + fire(); + }; + if ((_dragArea == DragArea::None) && !_draggedAfterPress) { + const auto startX = _leftSide.min + + (_rightSide.max - _leftSide.min) / 2; + const auto finishX = posX; + const auto toLeft = (finishX <= startX); + const auto diffBetweenStartAndLeft = startX - _leftSide.min; + _moveCenterAnimation.stop(); + _moveCenterAnimation.start([=](float64 value) { + moveCenter(toLeft, value, diffBetweenStartAndLeft); + fire(); + update(); + if (value == finishX) { + finish(); + } + }, + startX, + finishX, + st::slideWrapDuration, + anim::sineInOut); + } else { + finish(); + } + } break; + } + update(); + }, lifetime()); +} + +int ChartWidget::Footer::resizeGetHeight(int newWidth) { + const auto h = st::statisticsChartFooterHeight; + if (!newWidth) { + return h; + } + const auto was = xPercentageLimits(); + const auto w = float64(st::statisticsChartFooterSideWidth); + _width = newWidth - w; + _widthBetweenSides = newWidth - w * 2.; + _mask = Ui::RippleAnimation::RoundRectMask( + QSize(newWidth, h - st::lineWidth * 2), + st::boxRadius); + _frame = _mask; + if (_widthBetweenSides && was.max) { + setXPercentageLimits(was); + } + return h; +} + +Limits ChartWidget::Footer::xPercentageLimits() const { + return { + .min = _widthBetweenSides ? _leftSide.min / _widthBetweenSides : 0., + .max = _widthBetweenSides + ? (_rightSide.min - st::statisticsChartFooterSideWidth) + / _widthBetweenSides + : 0., + }; +} + +void ChartWidget::Footer::fire() const { + _xPercentageLimitsChange.fire(xPercentageLimits()); +} + +void ChartWidget::Footer::moveCenter( + bool isDirectionToLeft, + float64 x, + float64 diffBetweenStartAndLeft) { + const auto resultX = x - diffBetweenStartAndLeft; + const auto diffBetweenSides = std::max( + _rightSide.min - _leftSide.min, + float64(st::statisticsChartFooterBetweenSide)); + if (isDirectionToLeft) { + moveSide(true, resultX); + moveSide(false, _leftSide.min + diffBetweenSides); + } else { + moveSide(false, resultX + diffBetweenSides); + moveSide(true, _rightSide.min - diffBetweenSides); + } +} + +void ChartWidget::Footer::moveSide(bool left, float64 x) { + const auto w = float64(st::statisticsChartFooterSideWidth); + const auto mid = float64(st::statisticsChartFooterBetweenSide); + if (_width < (2 * w + mid)) { + return; + } else if (left) { + const auto rightLimit = _rightSide.min - w - mid; + const auto min = std::clamp( + x, + 0., + (rightLimit <= 0) ? _widthBetweenSides : rightLimit); + _leftSide = Limits{ .min = min, .max = min + w }; + } else if (!left) { + const auto min = std::clamp(x, _leftSide.max + mid, _width); + _rightSide = Limits{ .min = min, .max = min + w }; + } +} + +void ChartWidget::Footer::setPaintChartCallback( + PaintCallback paintChartCallback) { + _paintChartCallback = std::move(paintChartCallback); +} + +void ChartWidget::Footer::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + auto hq = PainterHighQualityEnabler(p); + + const auto lineWidth = st::lineWidth; + const auto innerMargins = QMargins{ 0, lineWidth, 0, lineWidth }; + const auto r = rect(); + const auto innerRect = r - innerMargins; + const auto &inactiveColor = st::statisticsChartInactive; + + _frame.fill(Qt::transparent); + if (_paintChartCallback) { + auto q = QPainter(&_frame); + + { + const auto opacity = q.opacity(); + _paintChartCallback(q, Rect(innerRect.size())); + q.setOpacity(opacity); + } + + q.setCompositionMode(QPainter::CompositionMode_DestinationIn); + q.drawImage(0, 0, _mask); + } + + p.drawImage(0, lineWidth, _frame); + + auto inactivePath = QPainterPath(); + inactivePath.addRoundedRect( + innerRect, + st::statisticsChartFooterSideRadius, + st::statisticsChartFooterSideRadius); + + auto sidesPath = QPainterPath(); + sidesPath.addRoundedRect( + _leftSide.min, + 0, + _rightSide.max - _leftSide.min, + r.height(), + st::statisticsChartFooterSideRadius, + st::statisticsChartFooterSideRadius); + inactivePath = inactivePath.subtracted(sidesPath); + sidesPath.addRect( + _leftSide.max, + lineWidth, + _rightSide.min - _leftSide.max, + r.height() - lineWidth * 2); + + p.setBrush(st::statisticsChartActive); + p.setPen(Qt::NoPen); + p.drawPath(sidesPath); + p.setBrush(inactiveColor); + p.drawPath(inactivePath); + + { + p.setPen(_sidePen); + const auto halfWidth = st::statisticsChartLineWidth / 2.; + const auto left = _leftSide.min + + (_leftSide.max - _leftSide.min) / 2. + + halfWidth; + const auto right = _rightSide.min + + (_rightSide.max - _rightSide.min) / 2.; + const auto halfHeight = st::statisticsChartFooterArrowHeight / 2. + - halfWidth; + const auto center = r.height() / 2.; + const auto top = center - halfHeight; + const auto bottom = center + halfHeight; + p.drawLine(left, top, left, bottom); + p.drawLine(right, top, right, bottom); + } +} + +void ChartWidget::Footer::setXPercentageLimits(const Limits &xLimits) { + const auto left = xLimits.min * _widthBetweenSides; + const auto right = xLimits.max * _widthBetweenSides + + st::statisticsChartFooterSideWidth; + moveSide(true, left); + moveSide(false, right); + fire(); + update(); +} + +rpl::producer ChartWidget::Footer::xPercentageLimitsChange() const { + return _xPercentageLimitsChange.events(); +} + +ChartWidget::ChartAnimationController::ChartAnimationController( + Fn &&updateCallback) +: _animation(std::move(updateCallback)) { +} + +void ChartWidget::ChartAnimationController::setXPercentageLimits( + Data::StatisticalChart &chartData, + Limits xPercentageLimits, + const std::unique_ptr &chartView, + const std::shared_ptr &linesFilter, + crl::time now) { + if ((_animationValueXMin.to() == xPercentageLimits.min) + && (_animationValueXMax.to() == xPercentageLimits.max) + && linesFilter->isFinished()) { + return; + } + start(); + _animationValueXMin.start(xPercentageLimits.min); + _animationValueXMax.start(xPercentageLimits.max); + _lastUserInteracted = now; + + const auto startXIndex = chartData.findStartIndex( + _animationValueXMin.to()); + const auto endXIndex = chartData.findEndIndex( + startXIndex, + _animationValueXMax.to()); + _currentXIndices = { float64(startXIndex), float64(endXIndex) }; + + { + const auto heightLimits = chartView->heightLimits( + chartData, + _currentXIndices); + if (heightLimits.ranged.min == heightLimits.ranged.max) { + return; + } + _previousFullHeightLimits = _finalHeightLimits; + _finalHeightLimits = heightLimits.ranged; + if (!_previousFullHeightLimits.max) { + _previousFullHeightLimits = _finalHeightLimits; + } + if (!linesFilter->isFinished()) { + _animationValueFooterHeightMin = anim::value( + _animationValueFooterHeightMin.current(), + heightLimits.full.min); + _animationValueFooterHeightMax = anim::value( + _animationValueFooterHeightMax.current(), + heightLimits.full.max); + } else if (!_animationValueFooterHeightMax.to()) { + // Will be finished in setChartData. + _animationValueFooterHeightMin = anim::value( + 0, + heightLimits.full.min); + _animationValueFooterHeightMax = anim::value( + 0, + heightLimits.full.max); + } + } + + _animationValueHeightMin = anim::value( + _animationValueHeightMin.current(), + _finalHeightLimits.min); + _animationValueHeightMax = anim::value( + _animationValueHeightMax.current(), + _finalHeightLimits.max); + + { + const auto previousDelta = _previousFullHeightLimits.max + - _previousFullHeightLimits.min; + auto k = previousDelta + / float64(_finalHeightLimits.max - _finalHeightLimits.min); + if (k > 1.) { + k = 1. / k; + } + constexpr auto kDtHeightSpeed1 = 0.03 * 2; + constexpr auto kDtHeightSpeed2 = 0.03 * 2; + constexpr auto kDtHeightSpeed3 = 0.045 * 2; + constexpr auto kDtHeightSpeedFilter = kDtHeightSpeed1 / 1.2; + constexpr auto kDtHeightSpeedThreshold1 = 0.7; + constexpr auto kDtHeightSpeedThreshold2 = 0.1; + constexpr auto kDtHeightInstantThreshold = 0.97; + if (k < 1.) { + auto &alpha = _animationValueHeightAlpha; + alpha = anim::value( + (alpha.current() == alpha.to()) ? 0. : alpha.current(), + 1.); + _dtHeight.currentAlpha = 0.; + _addRulerRequests.fire({}); + } + _dtHeight.speed = (!linesFilter->isFinished()) + ? kDtHeightSpeedFilter + : (k > kDtHeightSpeedThreshold1) + ? kDtHeightSpeed1 + : (k < kDtHeightSpeedThreshold2) + ? kDtHeightSpeed2 + : kDtHeightSpeed3; + if (k < kDtHeightInstantThreshold) { + _dtHeight.current = { 0., 0. }; + } + } +} + +auto ChartWidget::ChartAnimationController::addRulerRequests() const +-> rpl::producer<> { + return _addRulerRequests.events(); +} + +void ChartWidget::ChartAnimationController::start() { + if (!_animation.animating()) { + _animation.start(); + } +} + +void ChartWidget::ChartAnimationController::finish() { + _animation.stop(); + _animationValueXMin.finish(); + _animationValueXMax.finish(); + _animationValueHeightMin.finish(); + _animationValueHeightMax.finish(); + _animationValueFooterHeightMin.finish(); + _animationValueFooterHeightMax.finish(); + _animationValueHeightAlpha.finish(); + _benchmark = {}; +} + +void ChartWidget::ChartAnimationController::restartBottomLineAlpha() { + _bottomLineAlphaAnimationStartedAt = crl::now(); + _animValueBottomLineAlpha = anim::value(0., 1.); + start(); +} + +void ChartWidget::ChartAnimationController::tick( + crl::time now, + ChartRulersView &rulersView, + std::vector &dateLines, + const std::unique_ptr &chartView, + const std::shared_ptr &linesFilter) { + if (!_animation.animating()) { + return; + } + constexpr auto kXExpandingDuration = 200.; + constexpr auto kAlphaExpandingDuration = 200.; + + { + constexpr auto kIdealFPS = float64(60); + const auto currentFPS = _benchmark.lastTickedAt + ? (1000. / (now - _benchmark.lastTickedAt)) + : kIdealFPS; + if (!_benchmark.lastFPSSlow) { + constexpr auto kAcceptableFPS = int(30); + _benchmark.lastFPSSlow = (currentFPS < kAcceptableFPS); + } + _benchmark.lastTickedAt = now; + + + const auto k = (kIdealFPS / currentFPS) + // Speed up to reduce ugly frames count. + * (_benchmark.lastFPSSlow ? 2. : 1.); + const auto speed = _dtHeight.speed * k; + linesFilter->tick(speed); + _dtHeight.current.min = std::min(_dtHeight.current.min + speed, 1.); + _dtHeight.current.max = std::min(_dtHeight.current.max + speed, 1.); + _dtHeight.currentAlpha = std::min(_dtHeight.currentAlpha + speed, 1.); + } + + const auto dtX = std::min( + (now - _animation.started()) / kXExpandingDuration, + 1.); + const auto dtBottomLineAlpha = std::min( + (now - _bottomLineAlphaAnimationStartedAt) / kAlphaExpandingDuration, + 1.); + + const auto isFinished = [](const anim::value &anim) { + return anim.current() == anim.to(); + }; + + const auto xFinished = isFinished(_animationValueXMin) + && isFinished(_animationValueXMax); + const auto yFinished = isFinished(_animationValueHeightMin) + && isFinished(_animationValueHeightMax); + const auto alphaFinished = isFinished(_animationValueHeightAlpha) + && isFinished(_animationValueHeightMax); + const auto bottomLineAlphaFinished = isFinished( + _animValueBottomLineAlpha); + + const auto footerMinFinished = isFinished(_animationValueFooterHeightMin); + const auto footerMaxFinished = isFinished(_animationValueFooterHeightMax); + + if (xFinished + && yFinished + && alphaFinished + && bottomLineAlphaFinished + && footerMinFinished + && footerMaxFinished + && linesFilter->isFinished()) { + if ((_finalHeightLimits.min == _animationValueHeightMin.to()) + && _finalHeightLimits.max == _animationValueHeightMax.to()) { + _animation.stop(); + _benchmark = {}; + } + } + if (xFinished) { + _animationValueXMin.finish(); + _animationValueXMax.finish(); + } else { + _animationValueXMin.update(dtX, anim::linear); + _animationValueXMax.update(dtX, anim::linear); + } + if (bottomLineAlphaFinished) { + _animValueBottomLineAlpha.finish(); + _bottomLineAlphaAnimationStartedAt = 0; + } else { + _animValueBottomLineAlpha.update( + dtBottomLineAlpha, + anim::easeInCubic); + } + if (!yFinished) { + _animationValueHeightMin.update( + _dtHeight.current.min, + anim::easeInCubic); + _animationValueHeightMax.update( + _dtHeight.current.max, + anim::easeInCubic); + + rulersView.computeRelative( + _animationValueHeightMax.current(), + _animationValueHeightMin.current()); + } + if (!footerMinFinished) { + _animationValueFooterHeightMin.update( + _dtHeight.current.min, + anim::easeInCubic); + } + if (!footerMaxFinished) { + _animationValueFooterHeightMax.update( + _dtHeight.current.max, + anim::easeInCubic); + } + + if (!alphaFinished) { + _animationValueHeightAlpha.update( + _dtHeight.currentAlpha, + anim::easeInCubic); + rulersView.setAlpha(_animationValueHeightAlpha.current()); + } + + if (!bottomLineAlphaFinished) { + const auto value = _animValueBottomLineAlpha.current(); + for (auto &date : dateLines) { + date.alpha = (1. - value) * date.fixedAlpha; + } + dateLines.back().alpha = value; + } else { + if (dateLines.size() > 1) { + const auto data = dateLines.back(); + dateLines.clear(); + dateLines.push_back(data); + } + } +} + +Limits ChartWidget::ChartAnimationController::currentXLimits() const { + return { _animationValueXMin.current(), _animationValueXMax.current() }; +} + +Limits ChartWidget::ChartAnimationController::currentXIndices() const { + return _currentXIndices; +} + +Limits ChartWidget::ChartAnimationController::finalXLimits() const { + return { _animationValueXMin.to(), _animationValueXMax.to() }; +} + +Limits ChartWidget::ChartAnimationController::currentHeightLimits() const { + return { + _animationValueHeightMin.current(), + _animationValueHeightMax.current(), + }; +} + +auto ChartWidget::ChartAnimationController::currentFooterHeightLimits() const +-> Limits { + return { + _animationValueFooterHeightMin.current(), + _animationValueFooterHeightMax.current(), + }; +} + +Limits ChartWidget::ChartAnimationController::finalHeightLimits() const { + return _finalHeightLimits; +} + +bool ChartWidget::ChartAnimationController::animating() const { + return _animation.animating(); +} + +bool ChartWidget::ChartAnimationController::footerAnimating() const { + return (_animationValueFooterHeightMin.current() + != _animationValueFooterHeightMin.to()) + || (_animationValueFooterHeightMax.current() + != _animationValueFooterHeightMax.to()); +} + +ChartWidget::ChartWidget(not_null parent) +: Ui::RpWidget(parent) +, _chartArea(base::make_unique_q(this)) +, _header(std::make_unique
(this)) +, _footer(std::make_unique