Merge tag 'v4.10.4' into dev
# Conflicts: # Telegram/Resources/winrc/Telegram.rc # Telegram/Resources/winrc/Updater.rc # Telegram/SourceFiles/core/version.h # Telegram/lib_ui # snap/snapcraft.yaml
2
.github/workflows/docker.yml
vendored
|
@ -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: |
|
||||
|
|
4
.github/workflows/master_updater.yml
vendored
|
@ -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'
|
||||
|
|
3
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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 $<TARGET_FILE_DIR:Telegram>/../Resources
|
||||
COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $<TARGET_FILE_DIR:Telegram>/../Resources
|
||||
COMMAND cp ${CMAKE_BINARY_DIR}/lib_spellcheck.rcc $<TARGET_FILE_DIR:Telegram>/../Resources
|
||||
)
|
||||
if (NOT build_macstore)
|
||||
add_custom_command(TARGET Telegram
|
||||
|
|
BIN
Telegram/Resources/animations/stats.tgs
Normal file
BIN
Telegram/Resources/icons/chat/mini_copy.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
Telegram/Resources/icons/chat/mini_copy@2x.png
Normal file
After Width: | Height: | Size: 484 B |
BIN
Telegram/Resources/icons/chat/mini_copy@3x.png
Normal file
After Width: | Height: | Size: 623 B |
BIN
Telegram/Resources/icons/chat/mini_quote.png
Normal file
After Width: | Height: | Size: 374 B |
BIN
Telegram/Resources/icons/chat/mini_quote@2x.png
Normal file
After Width: | Height: | Size: 570 B |
BIN
Telegram/Resources/icons/chat/mini_quote@3x.png
Normal file
After Width: | Height: | Size: 849 B |
BIN
Telegram/Resources/icons/menu/stats.png
Normal file
After Width: | Height: | Size: 548 B |
BIN
Telegram/Resources/icons/menu/stats@2x.png
Normal file
After Width: | Height: | Size: 938 B |
BIN
Telegram/Resources/icons/menu/stats@3x.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
|
@ -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...";
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
<file alias="cloud_password/email.tgs">../../animations/cloud_password/email.tgs</file>
|
||||
<file alias="ttl.tgs">../../animations/ttl.tgs</file>
|
||||
<file alias="discussion.tgs">../../animations/discussion.tgs</file>
|
||||
<file alias="stats.tgs">../../animations/stats.tgs</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="4.10.2.0" />
|
||||
Version="4.10.4.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
570
Telegram/SourceFiles/api/api_statistics.cpp
Normal file
|
@ -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<ApiWrap*> api)
|
||||
: _api(&api->instance()) {
|
||||
}
|
||||
|
||||
rpl::producer<rpl::no_value, QString> Statistics::request(
|
||||
not_null<PeerData*> 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<PeerData*> 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<PeerData*> 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<ChannelData*> channel,
|
||||
FullMsgId fullId)
|
||||
: _channel(channel)
|
||||
, _fullId(fullId)
|
||||
, _api(&channel->session().api().instance()) {
|
||||
}
|
||||
|
||||
void PublicForwards::request(
|
||||
const Data::PublicForwardsSlice::OffsetToken &token,
|
||||
Fn<void(Data::PublicForwardsSlice)> 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<FullMsgId>;
|
||||
_requestId = 0;
|
||||
|
||||
auto nextToken = Data::PublicForwardsSlice::OffsetToken();
|
||||
const auto process = [&](const MTPVector<MTPMessage> &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<ChannelData*> 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<void(Data::MessageStatistics)> 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<PeerData*> peer)
|
||||
: _peer(peer)
|
||||
, _api(&peer->session().api().instance()) {
|
||||
}
|
||||
|
||||
rpl::producer<rpl::no_value, QString> 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<void(Data::BoostsListSlice)> 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<Data::Boost>();
|
||||
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
|
110
Telegram/SourceFiles/api/api_statistics.h
Normal file
|
@ -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<ApiWrap*> api);
|
||||
|
||||
[[nodiscard]] rpl::producer<rpl::no_value, QString> request(
|
||||
not_null<PeerData*> peer);
|
||||
using GraphResult = rpl::producer<Data::StatisticalGraph, QString>;
|
||||
[[nodiscard]] GraphResult requestZoom(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &token,
|
||||
float64 x);
|
||||
[[nodiscard]] GraphResult requestMessage(
|
||||
not_null<PeerData*> 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<Fn<void()>> _zoomDeque;
|
||||
|
||||
};
|
||||
|
||||
class PublicForwards final {
|
||||
public:
|
||||
explicit PublicForwards(not_null<ChannelData*> channel, FullMsgId fullId);
|
||||
|
||||
void request(
|
||||
const Data::PublicForwardsSlice::OffsetToken &token,
|
||||
Fn<void(Data::PublicForwardsSlice)> done);
|
||||
|
||||
private:
|
||||
const not_null<ChannelData*> _channel;
|
||||
const FullMsgId _fullId;
|
||||
mtpRequestId _requestId = 0;
|
||||
int _lastTotal = 0;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
};
|
||||
|
||||
class MessageStatistics final {
|
||||
public:
|
||||
explicit MessageStatistics(
|
||||
not_null<ChannelData*> channel,
|
||||
FullMsgId fullId);
|
||||
|
||||
void request(Fn<void(Data::MessageStatistics)> done);
|
||||
|
||||
[[nodiscard]] Data::PublicForwardsSlice firstSlice() const;
|
||||
|
||||
private:
|
||||
PublicForwards _publicForwards;
|
||||
const not_null<ChannelData*> _channel;
|
||||
const FullMsgId _fullId;
|
||||
|
||||
Data::PublicForwardsSlice _firstSlice;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
MTP::Sender _api;
|
||||
|
||||
};
|
||||
|
||||
class Boosts final {
|
||||
public:
|
||||
explicit Boosts(not_null<PeerData*> peer);
|
||||
|
||||
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
|
||||
void requestBoosts(
|
||||
const Data::BoostsListSlice::OffsetToken &token,
|
||||
Fn<void(Data::BoostsListSlice)> done);
|
||||
|
||||
[[nodiscard]] Data::BoostStatus boostStatus() const;
|
||||
|
||||
static constexpr auto kFirstSlice = int(10);
|
||||
static constexpr auto kLimit = int(40);
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
Data::BoostStatus _boostStatus;
|
||||
|
||||
MTP::Sender _api;
|
||||
mtpRequestId _requestId = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Api
|
|
@ -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;
|
||||
|
|
|
@ -194,7 +194,7 @@ not_null<Ui::FlatLabel*> 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -787,7 +787,7 @@ void MessageLinksParser::parse() {
|
|||
}
|
||||
offset = matchOffset = p - start;
|
||||
}
|
||||
processTagsBefore(QFIXED_MAX);
|
||||
processTagsBefore(Ui::kQFixedMax);
|
||||
|
||||
apply(text, ranges);
|
||||
}
|
||||
|
|
|
@ -512,14 +512,16 @@ void Application::startMediaView() {
|
|||
InvokeQueued(this, [=] {
|
||||
_mediaView = std::make_unique<Media::View::OverlayWidget>();
|
||||
});
|
||||
#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<Media::View::OverlayWidget>();
|
||||
_lastActivePrimaryWindow->widget()->Ui::RpWidget::setGeometry(current);
|
||||
#endif // Q_OS_MAC
|
||||
#else
|
||||
_mediaView = std::make_unique<Media::View::OverlayWidget>();
|
||||
#endif // Q_OS_MAC || Q_OS_WIN
|
||||
}
|
||||
|
||||
void Application::startTray() {
|
||||
|
|
|
@ -30,6 +30,16 @@ std::map<int, const char*> 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"
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -94,9 +94,10 @@ const auto CommandByName = base::flat_map<QString, Command>{
|
|||
{ 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 },
|
||||
//
|
||||
};
|
||||
|
||||
|
|
|
@ -59,6 +59,8 @@ enum class Command {
|
|||
|
||||
ReadChat,
|
||||
|
||||
MediaViewerFullscreen,
|
||||
|
||||
SupportReloadTemplates,
|
||||
SupportToggleMuted,
|
||||
SupportScrollToCurrent,
|
||||
|
|
|
@ -243,6 +243,16 @@ bool UiIntegration::handleUrlClick(
|
|||
return true;
|
||||
}
|
||||
|
||||
bool UiIntegration::copyPreOnClick(const QVariant &context) {
|
||||
const auto my = context.value<ClickHandlerContext>();
|
||||
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<Ui::Text::CustomEmoji> UiIntegration::createCustomEmoji(
|
||||
const QString &data,
|
||||
const std::any &context) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
43
Telegram/SourceFiles/data/data_boosts.h
Normal file
|
@ -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<Boost> list;
|
||||
int total = 0;
|
||||
bool allLoaded = false;
|
||||
OffsetToken token;
|
||||
};
|
||||
|
||||
struct BoostStatus final {
|
||||
BoostsOverview overview;
|
||||
BoostsListSlice firstSlice;
|
||||
QString link;
|
||||
};
|
||||
|
||||
} // namespace Data
|
|
@ -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);
|
||||
|
|
|
@ -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<ChannelDataFlag>;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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{}),
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Main::Session*> 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<HistoryItem*> item) {
|
|||
}
|
||||
}
|
||||
|
||||
void Session::registerHighlightProcess(
|
||||
uint64 processId,
|
||||
not_null<HistoryItem*> 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<HistoryItem*> item) {
|
||||
enumerateItemViews(item, [&](not_null<ViewElement*> view) {
|
||||
view->animateUnreadReactions();
|
||||
|
@ -2496,6 +2523,13 @@ void Session::unregisterMessage(not_null<HistoryItem*> 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)) {
|
||||
|
|
|
@ -299,6 +299,10 @@ public:
|
|||
void notifyPinnedDialogsOrderUpdated();
|
||||
[[nodiscard]] rpl::producer<> pinnedDialogsOrderUpdated() const;
|
||||
|
||||
void registerHighlightProcess(
|
||||
uint64 processId,
|
||||
not_null<HistoryItem*> item);
|
||||
|
||||
void registerHeavyViewPart(not_null<ViewElement*> view);
|
||||
void unregisterHeavyViewPart(not_null<ViewElement*> view);
|
||||
void unloadHeavyViewParts(
|
||||
|
@ -845,6 +849,7 @@ private:
|
|||
TimeId date);
|
||||
|
||||
void setWallpapers(const QVector<MTPWallPaper> &data, uint64 hash);
|
||||
void highlightProcessDone(uint64 processId);
|
||||
|
||||
void checkPollsClosings();
|
||||
|
||||
|
@ -955,6 +960,7 @@ private:
|
|||
std::unordered_map<
|
||||
FullStoryId,
|
||||
base::flat_set<not_null<HistoryItem*>>> _storyItems;
|
||||
base::flat_map<uint64, not_null<HistoryItem*>> _highlightings;
|
||||
|
||||
base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
|
||||
base::flat_set<not_null<GameData*>> _gamesUpdated;
|
||||
|
|
133
Telegram/SourceFiles/data/data_statistics.h
Normal file
|
@ -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<StatisticsMessageInteractionInfo> 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<StatisticsMessageSenderInfo> topSenders;
|
||||
std::vector<StatisticsAdministratorActionsInfo> topAdministrators;
|
||||
std::vector<StatisticsInviterInfo> 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<FullMsgId> list;
|
||||
int total = 0;
|
||||
bool allLoaded = false;
|
||||
OffsetToken token;
|
||||
};
|
||||
|
||||
} // namespace Data
|
156
Telegram/SourceFiles/data/data_statistics_chart.cpp
Normal file
|
@ -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 <QtCore/QDateTime>
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
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
|
77
Telegram/SourceFiles/data/data_statistics_chart.h
Normal file
|
@ -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<int> y;
|
||||
|
||||
Statistic::SegmentTree segmentTree;
|
||||
int id = 0;
|
||||
QString idString;
|
||||
QString name;
|
||||
int maxValue = 0;
|
||||
int minValue = std::numeric_limits<int>::max();
|
||||
QString colorKey;
|
||||
QColor color;
|
||||
QColor colorDark;
|
||||
bool isHiddenOnStart = false;
|
||||
};
|
||||
|
||||
std::vector<float64> x;
|
||||
std::vector<float64> xPercentage;
|
||||
std::vector<QString> daysLookup;
|
||||
|
||||
std::vector<Line> lines;
|
||||
|
||||
struct {
|
||||
float64 min = 0.;
|
||||
float64 max = 0.;
|
||||
} defaultZoomXIndex;
|
||||
|
||||
int maxValue = 0;
|
||||
int minValue = std::numeric_limits<int>::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
|
|
@ -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,
|
||||
|
|
|
@ -102,7 +102,7 @@ void MessageCursor::fillFrom(not_null<const Ui::InputField*> 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<Ui::InputField*> field) {
|
||||
|
|
|
@ -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<MessageFlag>;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<lt_from_part>::FromString);
|
||||
|
|
|
@ -75,7 +75,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn<void()> 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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ClickHandlerPtr> &HistoryItem::customTextLinks() const {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<HistoryItem*> 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);
|
||||
}
|
||||
|
|
|
@ -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, HistoryItem> {
|
||||
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.;
|
||||
|
||||
|
|
|
@ -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<Data::Draft>(
|
||||
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<HistoryItem*> 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<HistoryItem*> 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);
|
||||
|
|
|
@ -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<HistoryItem*> 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()) {
|
||||
|
|
|
@ -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<void()> 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<ChatHelpers::Show> 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<ChatHelpers::Show> 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ public:
|
|||
[[nodiscard]] rpl::producer<> itemsUpdated() const;
|
||||
|
||||
void editOptions(std::shared_ptr<ChatHelpers::Show> show);
|
||||
void editToNextOption();
|
||||
|
||||
[[nodiscard]] const HistoryItemsList &items() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -358,15 +358,15 @@ void ServiceMessagePainter::PaintComplexBubble(
|
|||
}
|
||||
}
|
||||
|
||||
QVector<int> ServiceMessagePainter::CountLineWidths(
|
||||
std::vector<int> ServiceMessagePainter::CountLineWidths(
|
||||
const Ui::Text::String &text,
|
||||
const QRect &textRect) {
|
||||
const auto linesCount = qMax(
|
||||
textRect.height() / st::msgServiceFont->height,
|
||||
1);
|
||||
auto result = QVector<int>();
|
||||
result.reserve(linesCount);
|
||||
text.countLineWidths(textRect.width(), &result);
|
||||
auto result = text.countLineWidths(textRect.width(), {
|
||||
.reserve = linesCount,
|
||||
});
|
||||
|
||||
const auto minDelta = 2 * (Ui::HistoryServiceMsgRadius()
|
||||
+ Ui::HistoryServiceMsgInvertedRadius()
|
||||
|
|
|
@ -113,7 +113,7 @@ public:
|
|||
const QRect &textRect);
|
||||
|
||||
private:
|
||||
static QVector<int> CountLineWidths(
|
||||
static std::vector<int> CountLineWidths(
|
||||
const Ui::Text::String &text,
|
||||
const QRect &textRect);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -14,6 +14,10 @@ namespace Stickers {
|
|||
struct LargeEmojiImage;
|
||||
} // namespace Stickers
|
||||
|
||||
namespace Ui::Text {
|
||||
class CustomEmoji;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
using LargeEmojiMedia = std::variant<
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
305
Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp
Normal file
|
@ -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 <QtGui/QGuiApplication>
|
||||
|
||||
namespace Info::Boosts {
|
||||
namespace {
|
||||
|
||||
void AddHeader(
|
||||
not_null<Ui::VerticalLayout*> content,
|
||||
tr::phrase<> text) {
|
||||
const auto header = content->add(
|
||||
object_ptr<Statistic::Header>(content),
|
||||
st::statisticsLayerMargins + st::boostsChartHeaderPadding);
|
||||
header->resizeToWidth(header->width());
|
||||
header->setTitle(text(tr::now));
|
||||
header->setSubTitle({});
|
||||
}
|
||||
|
||||
void FillOverview(
|
||||
not_null<Ui::VerticalLayout*> 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<Ui::RpWidget>(content),
|
||||
st::statisticsLayerMargins);
|
||||
|
||||
const auto addPrimary = [&](float64 v) {
|
||||
return Ui::CreateChild<Ui::FlatLabel>(
|
||||
container,
|
||||
(v >= 0)
|
||||
? Lang::FormatCountToShort(v).string
|
||||
: QString(),
|
||||
st::statisticsOverviewValue);
|
||||
};
|
||||
const auto addSub = [&](
|
||||
not_null<Ui::RpWidget*> primary,
|
||||
float64 percentage,
|
||||
tr::phrase<> text) {
|
||||
const auto second = Ui::CreateChild<Ui::FlatLabel>(
|
||||
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<Ui::FlatLabel>(
|
||||
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<Ui::VerticalLayout*> content,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
const QString &link,
|
||||
not_null<PeerData*> 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<Ui::InviteLinkLabel>(
|
||||
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<Ui::VerticalLayout>(content));
|
||||
Ui::AddCopyShareLinkButtons(copyShareWrap, copyLink, shareLink);
|
||||
copyShareWrap->widgetAt(0)->showChildren();
|
||||
::Settings::AddSkip(content, st::boostsLinkFieldPadding.bottom());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
InnerWidget::InnerWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller,
|
||||
not_null<PeerData*> peer)
|
||||
: VerticalLayout(parent)
|
||||
, _controller(controller)
|
||||
, _peer(peer)
|
||||
, _show(controller->uiShow()) {
|
||||
}
|
||||
|
||||
void InnerWidget::load() {
|
||||
const auto api = lifetime().make_state<Api::Boosts>(_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<rpl::event_stream<>>();
|
||||
const auto &status = _state;
|
||||
const auto inner = this;
|
||||
|
||||
{
|
||||
auto dividerContent = object_ptr<Ui::VerticalLayout>(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<Ui::DividerLabel>(
|
||||
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<PeerData*>;
|
||||
const auto header = inner->add(
|
||||
object_ptr<Statistic::Header>(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) {
|
||||
memento->setState(base::take(_state));
|
||||
}
|
||||
|
||||
void InnerWidget::restoreState(not_null<Memento*> memento) {
|
||||
_state = memento->state();
|
||||
if (!_state.link.isEmpty()) {
|
||||
fill();
|
||||
} else {
|
||||
load();
|
||||
}
|
||||
Ui::RpWidget::resizeToWidth(width());
|
||||
}
|
||||
|
||||
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
auto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {
|
||||
return _showRequests.events();
|
||||
}
|
||||
|
||||
void InnerWidget::showFinished() {
|
||||
_showFinished.fire({});
|
||||
}
|
||||
|
||||
not_null<PeerData*> InnerWidget::peer() const {
|
||||
return _peer;
|
||||
}
|
||||
|
||||
} // namespace Info::Boosts
|
||||
|
63
Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.h
Normal file
|
@ -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*> controller,
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
|
||||
[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;
|
||||
|
||||
void showFinished();
|
||||
|
||||
void saveState(not_null<Memento*> memento);
|
||||
void restoreState(not_null<Memento*> memento);
|
||||
|
||||
private:
|
||||
void load();
|
||||
void fill();
|
||||
|
||||
not_null<Controller*> _controller;
|
||||
not_null<PeerData*> _peer;
|
||||
std::shared_ptr<Ui::Show> _show;
|
||||
|
||||
Data::BoostStatus _state;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
rpl::event_stream<ShowRequest> _showRequests;
|
||||
rpl::event_stream<> _showFinished;
|
||||
rpl::event_stream<bool> _loaded;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Boosts
|
121
Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp
Normal file
|
@ -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*> controller)
|
||||
: ContentMemento(Info::Statistics::Tag{
|
||||
controller->statisticsPeer(),
|
||||
{}
|
||||
}) {
|
||||
}
|
||||
|
||||
Memento::Memento(not_null<PeerData*> 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<ContentWidget> Memento::createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller,
|
||||
const QRect &geometry) {
|
||||
auto result = object_ptr<Widget>(parent, controller);
|
||||
result->setInternalState(geometry, this);
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget::Widget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller)
|
||||
: ContentWidget(parent, controller)
|
||||
, _inner(setInnerWidget(
|
||||
object_ptr<InnerWidget>(
|
||||
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<PeerData*> Widget::peer() const {
|
||||
return _inner->peer();
|
||||
}
|
||||
|
||||
bool Widget::showInternal(not_null<ContentMemento*> memento) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rpl::producer<QString> Widget::title() {
|
||||
return tr::lng_boosts_title();
|
||||
}
|
||||
|
||||
void Widget::setInternalState(
|
||||
const QRect &geometry,
|
||||
not_null<Memento*> memento) {
|
||||
setGeometry(geometry);
|
||||
Ui::SendPendingMoveResizeEvents(this);
|
||||
restoreState(memento);
|
||||
}
|
||||
|
||||
rpl::producer<bool> Widget::desiredShadowVisibility() const {
|
||||
return rpl::single<bool>(true);
|
||||
}
|
||||
|
||||
void Widget::showFinished() {
|
||||
_inner->showFinished();
|
||||
}
|
||||
|
||||
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
|
||||
auto result = std::make_shared<Memento>(controller());
|
||||
saveState(result.get());
|
||||
return result;
|
||||
}
|
||||
|
||||
void Widget::saveState(not_null<Memento*> memento) {
|
||||
memento->setScrollTop(scrollTopSave());
|
||||
_inner->saveState(memento);
|
||||
}
|
||||
|
||||
void Widget::restoreState(not_null<Memento*> memento) {
|
||||
_inner->restoreState(memento);
|
||||
scrollTopRestore(memento->scrollTop());
|
||||
}
|
||||
|
||||
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
|
||||
return std::make_shared<Info::Memento>(
|
||||
std::vector<std::shared_ptr<ContentMemento>>(
|
||||
1,
|
||||
std::make_shared<Memento>(peer)));
|
||||
}
|
||||
|
||||
} // namespace Info::Boosts
|
||||
|
68
Telegram/SourceFiles/info/boosts/info_boosts_widget.h
Normal file
|
@ -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*> controller);
|
||||
Memento(not_null<PeerData*> peer);
|
||||
~Memento();
|
||||
|
||||
object_ptr<ContentWidget> createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> 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*> controller);
|
||||
|
||||
bool showInternal(not_null<ContentMemento*> memento) override;
|
||||
rpl::producer<QString> title() override;
|
||||
rpl::producer<bool> desiredShadowVisibility() const override;
|
||||
void showFinished() override;
|
||||
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
[[nodiscard]] FullMsgId contextId() const;
|
||||
|
||||
void setInternalState(
|
||||
const QRect &geometry,
|
||||
not_null<Memento*> memento);
|
||||
|
||||
private:
|
||||
void saveState(not_null<Memento*> memento);
|
||||
void restoreState(not_null<Memento*> memento);
|
||||
|
||||
std::shared_ptr<ContentMemento> doCreateMemento() override;
|
||||
|
||||
const not_null<InnerWidget*> _inner;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
|
||||
|
||||
} // namespace Info::Boosts
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<PollData*> 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;
|
||||
|
||||
|
|
|
@ -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<PollData*> 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<Statistics::Tag>(&_value)) {
|
||||
return tag->peer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FullMsgId Key::statisticsContextId() const {
|
||||
if (const auto tag = std::get_if<Statistics::Tag>(&_value)) {
|
||||
return tag->contextId;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
PollData *Key::poll() const {
|
||||
if (const auto data = std::get_if<PollKey>(&_value)) {
|
||||
return data->poll;
|
||||
|
|
|
@ -55,6 +55,20 @@ struct Tag {
|
|||
|
||||
} // namespace Info::Stories
|
||||
|
||||
namespace Info::Statistics {
|
||||
|
||||
struct Tag {
|
||||
explicit Tag(not_null<PeerData*> peer, FullMsgId contextId)
|
||||
: peer(peer)
|
||||
, contextId(contextId) {
|
||||
}
|
||||
|
||||
not_null<PeerData*> 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<PollData*> 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();
|
||||
|
|
|
@ -152,11 +152,16 @@ std::shared_ptr<ContentMemento> Memento::DefaultContent(
|
|||
std::shared_ptr<ContentMemento> Memento::DefaultContent(
|
||||
not_null<Data::ForumTopic*> 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<Profile::Memento>(topic);
|
||||
case Section::Type::Media:
|
||||
return std::make_shared<Media::Memento>(topic, section.mediaType());
|
||||
case Section::Type::Members:
|
||||
return std::make_shared<Members::Memento>(peer, migratedPeerId);
|
||||
}
|
||||
Unexpected("Wrong section type in Info::Memento::DefaultContent()");
|
||||
}
|
||||
|
|
|
@ -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().");
|
||||
|
|
|
@ -76,6 +76,8 @@ Badge::Badge(
|
|||
}, _lifetime);
|
||||
}
|
||||
|
||||
Badge::~Badge() = default;
|
||||
|
||||
Ui::RpWidget *Badge::widget() const {
|
||||
return _view.data();
|
||||
}
|
||||
|
|
|
@ -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<BadgeType> allowed
|
||||
= base::flags<BadgeType>::from_raw(-1));
|
||||
|
||||
~Badge();
|
||||
|
||||
[[nodiscard]] Ui::RpWidget *widget() const;
|
||||
|
||||
void setPremiumClickCallback(Fn<void()> callback);
|
||||
|
|
|
@ -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);
|
||||
|
|