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
This commit is contained in:
ZavaruKitsu 2023-10-21 22:04:45 +03:00
commit eff273dd7d
212 changed files with 10767 additions and 690 deletions

View file

@ -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: |

View file

@ -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
View file

@ -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

View file

@ -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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -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...";

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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"

View 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

View 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

View file

@ -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;

View file

@ -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);

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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);

View file

@ -787,7 +787,7 @@ void MessageLinksParser::parse() {
}
offset = matchOffset = p - start;
}
processTagsBefore(QFIXED_MAX);
processTagsBefore(Ui::kQFixedMax);
apply(text, ranges);
}

View file

@ -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() {

View file

@ -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"
}
};
};

View file

@ -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 },
//
};

View file

@ -59,6 +59,8 @@ enum class Command {
ReadChat,
MediaViewerFullscreen,
SupportReloadTemplates,
SupportToggleMuted,
SupportScrollToCurrent,

View file

@ -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) {

View file

@ -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;

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 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;

View file

@ -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 {

View 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

View file

@ -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);

View file

@ -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>;

View file

@ -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));

View file

@ -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{}),
};

View file

@ -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 {

View file

@ -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)) {

View file

@ -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;

View 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

View 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

View 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

View file

@ -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,

View file

@ -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) {

View file

@ -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>;

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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()

View file

@ -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) {

View file

@ -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);

View file

@ -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

View file

@ -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;

View file

@ -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) {

View file

@ -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);
}

View file

@ -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 {

View file

@ -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;

View file

@ -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);
}

View file

@ -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.;

View file

@ -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);

View file

@ -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()) {

View file

@ -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,
});
}

View file

@ -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;

View file

@ -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),

View file

@ -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()

View file

@ -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);

View file

@ -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;

View file

@ -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),

View file

@ -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),

View file

@ -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;

View file

@ -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),

View file

@ -14,6 +14,10 @@ namespace Stickers {
struct LargeEmojiImage;
} // namespace Stickers
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace HistoryView {
using LargeEmojiMedia = std::variant<

View file

@ -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"

View file

@ -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),

View file

@ -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 {

View file

@ -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),

View file

@ -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)

View file

@ -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;

View file

@ -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

View 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

View 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

View 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

View 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

View file

@ -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);

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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()");
}

View file

@ -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().");

View file

@ -76,6 +76,8 @@ Badge::Badge(
}, _lifetime);
}
Badge::~Badge() = default;
Ui::RpWidget *Badge::widget() const {
return _view.data();
}

View file

@ -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);

View file

@ -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);

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