Merge tag 'v5.2.0' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/calls/calls_call.cpp
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/history/view/media/history_view_gif.cpp
#	Telegram/SourceFiles/window/notifications_manager_default.cpp
#	Telegram/lib_ui
#	snap/snapcraft.yaml
This commit is contained in:
AlexeyZavar 2024-07-01 03:55:36 +03:00
commit da797a2b07
273 changed files with 7124 additions and 2466 deletions

View file

@ -57,14 +57,6 @@ include(cmake/validate_d3d_compiler.cmake)
include(cmake/target_prepare_qrc.cmake)
include(cmake/options.cmake)
if (NOT DESKTOP_APP_USE_PACKAGED)
if (WIN32)
set(qt_version 5.15.13)
elseif (APPLE)
set(qt_version 6.2.8)
endif()
endif()
include(cmake/external/qt/package.cmake)
set(desktop_app_skip_libs

View file

@ -195,6 +195,7 @@ PRIVATE
api/api_earn.h
api/api_editing.cpp
api/api_editing.h
api/api_filter_updates.h
api/api_global_privacy.cpp
api/api_global_privacy.h
api/api_hash.cpp
@ -233,6 +234,10 @@ PRIVATE
api/api_single_message_search.h
api/api_statistics.cpp
api/api_statistics.h
api/api_statistics_data_deserialize.cpp
api/api_statistics_data_deserialize.h
api/api_statistics_sender.cpp
api/api_statistics_sender.h
api/api_text_entities.cpp
api/api_text_entities.h
api/api_toggling_media.cpp
@ -791,8 +796,6 @@ PRIVATE
history/view/media/history_view_dice.h
history/view/media/history_view_document.cpp
history/view/media/history_view_document.h
history/view/media/history_view_extended_preview.cpp
history/view/media/history_view_extended_preview.h
history/view/media/history_view_file.cpp
history/view/media/history_view_file.h
history/view/media/history_view_game.cpp
@ -956,6 +959,10 @@ PRIVATE
history/history_view_highlight_manager.h
history/history_widget.cpp
history/history_widget.h
info/bot/earn/info_earn_inner_widget.cpp
info/bot/earn/info_earn_inner_widget.h
info/bot/earn/info_earn_widget.cpp
info/bot/earn/info_earn_widget.h
info/channel_statistics/boosts/create_giveaway_box.cpp
info/channel_statistics/boosts/create_giveaway_box.h
info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp
@ -1608,6 +1615,8 @@ PRIVATE
window/window_peer_menu.cpp
window/window_peer_menu.h
window/window_section_common.h
window/window_separate_id.cpp
window/window_separate_id.h
window/window_session_controller.cpp
window/window_session_controller.h
window/window_session_controller_link_info.h
@ -1899,12 +1908,44 @@ if (WIN32)
/DELAYLOAD:uxtheme.dll
/DELAYLOAD:crypt32.dll
/DELAYLOAD:bcrypt.dll
/DELAYLOAD:imm32.dll
/DELAYLOAD:netapi32.dll
/DELAYLOAD:imm32.dll
/DELAYLOAD:userenv.dll
/DELAYLOAD:wtsapi32.dll
/DELAYLOAD:propsys.dll
)
if (QT_VERSION GREATER 6)
target_link_options(Telegram
PRIVATE
/DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll
/DELAYLOAD:API-MS-Win-Core-File-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-LibraryLoader-l1-2-0.dll
/DELAYLOAD:API-MS-Win-Core-Localization-l1-2-0.dll
/DELAYLOAD:API-MS-Win-Core-Memory-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Memory-l1-1-1.dll
/DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
/DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll
/DELAYLOAD:authz.dll # Authz.lib
/DELAYLOAD:comdlg32.dll
/DELAYLOAD:dwrite.dll # DWrite.lib
/DELAYLOAD:dxgi.dll # DXGI.lib
/DELAYLOAD:d3d9.dll # D3D9.lib
/DELAYLOAD:d3d11.dll # D3D11.lib
/DELAYLOAD:d3d12.dll # D3D12.lib
/DELAYLOAD:setupapi.dll # SetupAPI.lib
/DELAYLOAD:winhttp.dll
)
endif()
endif()
target_prepare_qrc(Telegram)

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -683,6 +683,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_privacy_premium_link" = "Telegram Premium";
"lng_settings_passcode_disable" = "Disable Passcode";
"lng_settings_passcode_disable_sure" = "Are you sure you want to disable passcode?";
"lng_settings_use_winhello" = "Unlock with Windows Hello";
"lng_settings_use_winhello_about" = "You need to enter your passcode once before unlocking Telegram with Windows Hello.";
"lng_settings_use_touchid" = "Unlock with Touch ID";
"lng_settings_use_touchid_about" = "You need to enter your passcode once before unlocking Telegram with Touch ID.";
"lng_settings_password_disable" = "Disable Cloud Password";
"lng_settings_password_abort" = "Abort two-step verification setup";
"lng_settings_about_bio" = "Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco";
@ -897,6 +901,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_gift_premium_users_confirm" = "Proceed";
"lng_settings_gift_premium_users_error#one" = "You can select maximum {count} user.";
"lng_settings_gift_premium_users_error#other" = "You can select maximum {count} users.";
"lng_settings_gift_premium_choose" = "Please choose at least one recipient.";
"lng_backgrounds_header" = "Choose Wallpaper";
"lng_theme_sure_keep" = "Keep this theme?";
@ -991,6 +996,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_passcode_ph" = "Your passcode";
"lng_passcode_submit" = "Submit";
"lng_passcode_logout" = "Log out";
"lng_passcode_winhello" = "You need to enter your passcode\nbefore you can use Windows Hello.";
"lng_passcode_touchid" = "You need to enter your passcode\nbefore you can use Touch ID.";
"lng_passcode_winhello_unlock" = "Telegram wants to unlock with Windows Hello.";
"lng_passcode_touchid_unlock" = "unlock";
"lng_passcode_create_button" = "Save Passcode";
"lng_passcode_check_button" = "Submit";
"lng_passcode_change_button" = "Save Passcode";
@ -1560,6 +1569,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_bot_public_link" = "Public Link";
"lng_manage_peer_bot_public_links" = "Public Links";
"lng_manage_peer_bot_balance" = "Balance";
"lng_manage_peer_bot_edit_intro" = "Edit Intro";
"lng_manage_peer_bot_edit_commands" = "Edit Commands";
"lng_manage_peer_bot_edit_settings" = "Change Bot Settings";
@ -2185,6 +2195,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_translation" = "Real-time translation of channels and chats into other languages.";
"lng_premium_summary_subtitle_business" = "Telegram Business";
"lng_premium_summary_about_business" = "Upgrade your account with business features such as location, opening hours and quick replies.";
"lng_premium_summary_subtitle_effects" = "Message Effects";
"lng_premium_summary_about_effects" = "Add over 500 animated effects to private messages.";
"lng_premium_summary_bottom_subtitle" = "About Telegram Premium";
"lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
"lng_premium_summary_button" = "Subscribe for {cost} per month";
@ -2325,15 +2337,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_out_title" = "Confirm Your Purchase";
"lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?";
"lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?";
"lng_credits_box_out_media#one" = "Do you want to unlock {media} in {chat} for **{count} Star**?";
"lng_credits_box_out_media#other" = "Do you want to unlock {media} in {chat} for **{count} Stars**?";
"lng_credits_box_out_photo" = "a photo";
"lng_credits_box_out_photos#one" = "{count} photo";
"lng_credits_box_out_photos#other" = "{count} photos";
"lng_credits_box_out_video" = "a video";
"lng_credits_box_out_videos#one" = "{count} video";
"lng_credits_box_out_videos#other" = "{count} videos";
"lng_credits_box_out_both" = "{photo} and {video}";
"lng_credits_box_out_confirm#one" = "Confirm and Pay {emoji} {count} Star";
"lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars";
"lng_credits_box_out_about" = "Review the {link} for Stars.";
"lng_credits_media_done_title" = "Media Unlocked";
"lng_credits_media_done_text#one" = "**{count} Star** transferred to {chat}.";
"lng_credits_media_done_text#other" = "**{count} Stars** transferred to {chat}.";
"lng_credits_summary_in_toast_title" = "Stars Acquired";
"lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance.";
"lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance.";
"lng_credits_box_history_entry_peer" = "Recipient";
"lng_credits_box_history_entry_via" = "Via";
"lng_credits_box_history_entry_play_market" = "Play Market";
"lng_credits_box_history_entry_app_store" = "App Store";
"lng_credits_box_history_entry_fragment" = "Fragment";
"lng_credits_box_history_entry_ads" = "Ads Platform";
"lng_credits_box_history_entry_id" = "Transaction ID";
"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard.";
"lng_credits_box_history_entry_success_date" = "Transaction date";
"lng_credits_box_history_entry_success_url" = "Transaction link";
"lng_credits_box_history_entry_media" = "Media";
"lng_credits_box_history_entry_about" = "You can dispute this transaction {link}.";
"lng_credits_box_history_entry_about_link" = "here";
"lng_credits_small_balance_title#one" = "{count} Star Needed";
@ -3305,6 +3337,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler";
"lng_context_make_paid" = "Make This Content Paid";
"lng_context_change_price" = "Change Price";
"lng_factcheck_title" = "Fact Check";
"lng_factcheck_placeholder" = "Add Facts or Context";
@ -3316,6 +3350,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_factcheck_bottom" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation.";
"lng_factcheck_links" = "Only **t.me/** links are allowed.";
"lng_paid_title" = "Paid Content";
"lng_paid_enter_cost" = "Enter Unlock Cost";
"lng_paid_cost_placeholder" = "Stars to Unlock";
"lng_paid_about" = "Users will have to transfer this amount of Stars to your channel in order to view this media. {link}";
"lng_paid_about_link" = "More about stars >";
"lng_paid_about_link_url" = "https://telegram.org/blog/telegram-stars";
"lng_paid_price" = "Unlock for {price}";
"lng_translate_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}";
"lng_translate_bar_to_other" = "Translate to {name}";
@ -3582,6 +3624,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_formatting_link_text" = "Text";
"lng_formatting_link_url" = "URL";
"lng_formatting_link_create" = "Create";
"lng_formatting_code_title" = "Code Language";
"lng_formatting_code_language" = "Language for syntax highlighting.";
"lng_formatting_code_auto" = "Auto-Detect";
"lng_text_copied" = "Text copied to clipboard.";
"lng_code_copied" = "Block copied to clipboard.";
@ -4132,6 +4177,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_restricted_send_polls_all" = "Posting polls isn't allowed in this group.";
"lng_restricted_send_public_polls" = "Sorry, public polls can't be forwarded to channels.";
"lng_restricted_send_paid_media" = "Sorry, paid media can't be sent to this channel.";
"lng_restricted_send_voice_messages" = "{user} restricted sending of voice messages to them.";
"lng_restricted_send_video_messages" = "{user} restricted sending of video messages to them.";
@ -5159,6 +5205,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channel_earn_history_return" = "Refund";
"lng_channel_earn_history_return_about" = "Refunded back";
"lng_channel_earn_history_pending" = "Pending";
"lng_channel_earn_history_failed" = "Failed";
"lng_channel_earn_history_show_more#one" = "Show {count} More Transaction";
"lng_channel_earn_history_show_more#other" = "Show {count} More Transactions";
"lng_channel_earn_off" = "Switch Off Ads";
@ -5181,6 +5228,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channel_earn_chart_revenue" = "Ad revenue";
"lng_channel_earn_chart_overriden_detail_currency" = "Revenue in TON";
"lng_channel_earn_chart_overriden_detail_usd" = "Revenue in USD";
"lng_channel_earn_currency_history" = "TON Transactions";
"lng_channel_earn_credits_history" = "Stars Transactions";
"lng_channel_earn_out_check_password_about" = "You can withdraw only if you have:";
"lng_bot_earn_title" = "Stars Balance";
"lng_bot_earn_chart_revenue" = "Revenue";
"lng_bot_earn_overview_title" = "Proceeds overview";
"lng_bot_earn_available" = "Available balance";
"lng_bot_earn_total" = "Total lifetime proceeds";
"lng_bot_earn_balance_title" = "Available balance";
"lng_bot_earn_balance_about" = "Stars from your total balance become available for spending on ads and rewards 21 days after they are earned.";
"lng_bot_earn_balance_about_url" = "https://telegram.org/tos/stars";
"lng_bot_earn_balance_button#one" = "Withdraw {emoji} {count}";
"lng_bot_earn_balance_button#other" = "Withdraw {emoji} {count}";
"lng_bot_earn_balance_button_all" = "Withdraw all stars";
"lng_bot_earn_balance_button_locked" = "Withdraw";
"lng_bot_earn_balance_button_buy_ads" = "Buy Ads";
"lng_bot_earn_learn_credits_out_about" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. {link}";
"lng_bot_earn_out_ph" = "Enter amount to withdraw";
"lng_bot_earn_balance_password_title" = "Two-step verification";
"lng_bot_earn_balance_password_description" = "Please enter your password to collect.";
"lng_bot_earn_credits_out_minimal" = "You cannot withdraw less then {link}.";
"lng_bot_earn_credits_out_minimal_link#one" = "{count} star";
"lng_bot_earn_credits_out_minimal_link#other" = "{count} stars";
"lng_contact_add" = "Add";
"lng_contact_send_message" = "message";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.1.7.0" />
Version="5.2.0.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 5,1,7,0
PRODUCTVERSION 5,1,7,0
FILEVERSION 5,2,0,0
PRODUCTVERSION 5,2,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.1.7.0"
VALUE "FileVersion", "5.2.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.1.7.0"
VALUE "ProductVersion", "5.2.0.0"
END
END
BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,1,7,0
PRODUCTVERSION 5,1,7,0
FILEVERSION 5,2,0,0
PRODUCTVERSION 5,2,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "5.1.7.0"
VALUE "FileVersion", "5.2.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.1.7.0"
VALUE "ProductVersion", "5.2.0.0"
END
END
BLOCK "VarFileInfo"

View file

@ -20,6 +20,7 @@ namespace Api {
inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
struct SendOptions {
uint64 price = 0;
PeerData *sendAs = nullptr;
TimeId scheduled = 0;
BusinessShortcutId shortcutId = 0;

View file

@ -7,9 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_credits.h"
#include "apiwrap.h"
#include "api/api_statistics_data_deserialize.h"
#include "api/api_updates.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
@ -20,25 +23,62 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Api {
namespace {
constexpr auto kTransactionsLimit = 100;
[[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL(
const MTPStarsTransaction &tl,
not_null<PeerData*> peer) {
using HistoryPeerTL = MTPDstarsTransactionPeer;
using namespace Data;
const auto owner = &peer->owner();
const auto photo = tl.data().vphoto()
? peer->owner().photoFromWeb(*tl.data().vphoto(), ImageLocation())
? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())
: nullptr;
auto extended = std::vector<CreditsHistoryMedia>();
if (const auto list = tl.data().vextended_media()) {
extended.reserve(list->v.size());
for (const auto &media : list->v) {
media.match([&](const MTPDmessageMediaPhoto &photo) {
if (const auto inner = photo.vphoto()) {
const auto photo = owner->processPhoto(*inner);
if (!photo->isNull()) {
extended.push_back(CreditsHistoryMedia{
.type = CreditsHistoryMediaType::Photo,
.id = photo->id,
});
}
}
}, [&](const MTPDmessageMediaDocument &document) {
if (const auto inner = document.vdocument()) {
const auto document = owner->processDocument(*inner);
if (document->isAnimation()
|| document->isVideoFile()
|| document->isGifv()) {
extended.push_back(CreditsHistoryMedia{
.type = CreditsHistoryMediaType::Video,
.id = document->id,
});
}
}
}, [&](const auto &) {});
}
}
const auto barePeerId = tl.data().vpeer().match([](
const HistoryPeerTL &p) {
return peerFromMTP(p.vpeer());
}, [](const auto &) {
return PeerId(0);
}).value;
return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()),
.description = qs(tl.data().vdescription().value_or_empty()),
.date = base::unixtime::parse(tl.data().vdate().v),
.photoId = photo ? photo->id : 0,
.extended = std::move(extended),
.credits = tl.data().vstars().v,
.bareId = tl.data().vpeer().match([](const HistoryPeerTL &p) {
return peerFromMTP(p.vpeer());
}, [](const auto &) {
return PeerId(0);
}).value,
.bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),
.barePeerId = barePeerId,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
@ -51,8 +91,17 @@ namespace {
return Data::CreditsHistoryEntry::PeerType::Unsupported;
}, [](const MTPDstarsTransactionPeerPremiumBot &) {
return Data::CreditsHistoryEntry::PeerType::PremiumBot;
}, [](const MTPDstarsTransactionPeerAds &) {
return Data::CreditsHistoryEntry::PeerType::Ads;
}),
.refunded = tl.data().is_refund(),
.pending = tl.data().is_pending(),
.failed = tl.data().is_failed(),
.successDate = tl.data().vtransaction_date()
? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(),
.successLink = qs(tl.data().vtransaction_url().value_or_empty()),
.in = (int64(tl.data().vstars().v) >= 0),
};
}
@ -152,7 +201,8 @@ void CreditsHistory::request(
_requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token)
MTP_string(token),
MTP_int(kTransactionsLimit)
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
@ -199,4 +249,58 @@ rpl::producer<not_null<PeerData*>> PremiumPeerBot(
};
}
CreditsEarnStatistics::CreditsEarnStatistics(not_null<PeerData*> peer)
: StatisticsRequestSender(peer)
, _isUser(peer->isUser()) {
}
rpl::producer<rpl::no_value, QString> CreditsEarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto finish = [=](const QString &url) {
makeRequest(MTPpayments_GetStarsRevenueStats(
MTP_flags(0),
(_isUser ? user()->input : channel()->input)
)).done([=](const MTPpayments_StarsRevenueStats &result) {
const auto &data = result.data();
const auto &status = data.vstatus().data();
_data = Data::CreditsEarnStatistics{
.revenueGraph = StatisticalGraphFromTL(
data.vrevenue_graph()),
.currentBalance = status.vcurrent_balance().v,
.availableBalance = status.vavailable_balance().v,
.overallRevenue = status.voverall_revenue().v,
.usdRate = data.vusd_rate().v,
.isWithdrawalEnabled = status.is_withdrawal_enabled(),
.nextWithdrawalAt = status.vnext_withdrawal_at()
? base::unixtime::parse(
status.vnext_withdrawal_at()->v)
: QDateTime(),
.buyAdsUrl = url,
};
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
};
makeRequest(
MTPpayments_GetStarsRevenueAdsAccountUrl(
(_isUser ? user()->input : channel()->input))
).done([=](const MTPpayments_StarsRevenueAdsAccountUrl &result) {
finish(qs(result.data().vurl()));
}).fail([=](const MTP::Error &error) {
finish({});
}).send();
return lifetime;
};
}
Data::CreditsEarnStatistics CreditsEarnStatistics::data() const {
return _data;
}
} // namespace Api

View file

@ -7,13 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "api/api_statistics_sender.h"
#include "data/data_credits.h"
#include "data/data_credits_earn.h"
#include "mtproto/sender.h"
namespace Main {
class Session;
} // namespace Main
class UserData;
namespace Api {
class CreditsTopupOptions final {
@ -68,6 +72,21 @@ private:
};
class CreditsEarnStatistics final : public StatisticsRequestSender {
public:
explicit CreditsEarnStatistics(not_null<PeerData*>);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::CreditsEarnStatistics data() const;
private:
Data::CreditsEarnStatistics _data;
bool _isUser = false;
mtpRequestId _requestId = 0;
};
[[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(
not_null<Main::Session*> session);

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_cloud_password.h"
#include "apiwrap.h"
#include "ui/layers/generic_box.h"
#include "boxes/passcode_box.h"
#include "data/data_channel.h"
#include "data/data_session.h"
@ -34,22 +35,33 @@ void RestrictSponsored(
}
void HandleWithdrawalButton(
not_null<ChannelData*> channel,
RewardReceiver receiver,
not_null<Ui::RippleButton*> button,
std::shared_ptr<Ui::Show> show) {
Expects(receiver.currencyReceiver
|| (receiver.creditsReceiver && receiver.creditsAmount));
struct State {
rpl::lifetime lifetime;
bool loading = false;
};
const auto channel = receiver.currencyReceiver;
const auto peer = receiver.creditsReceiver;
const auto state = button->lifetime().make_state<State>();
const auto session = &channel->session();
const auto session = (channel ? &channel->session() : &peer->session());
using ChannelOutUrl = MTPstats_BroadcastRevenueWithdrawalUrl;
using CreditsOutUrl = MTPpayments_StarsRevenueWithdrawalUrl;
session->api().cloudPassword().reload();
button->setClickedCallback([=] {
const auto processOut = [=] {
if (state->loading) {
return;
}
if (peer && !receiver.creditsAmount()) {
return;
}
state->loading = true;
state->lifetime = session->api().cloudPassword().state(
) | rpl::take(
@ -58,10 +70,12 @@ void HandleWithdrawalButton(
state->loading = false;
auto fields = PasscodeBox::CloudFields::From(pass);
fields.customTitle
= tr::lng_channel_earn_balance_password_title();
fields.customDescription
= tr::lng_channel_earn_balance_password_description(tr::now);
fields.customTitle = channel
? tr::lng_channel_earn_balance_password_title()
: tr::lng_bot_earn_balance_password_title();
fields.customDescription = channel
? tr::lng_channel_earn_balance_password_description(tr::now)
: tr::lng_bot_earn_balance_password_description(tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = crl::guard(button, [=](
const Core::CloudPasswordResult &result,
@ -74,22 +88,63 @@ void HandleWithdrawalButton(
}
}
};
const auto fail = [=](const QString &error) {
show->showToast(error);
const auto fail = [=](const MTP::Error &error) {
show->showToast(error.type());
};
session->api().request(
MTPstats_GetBroadcastRevenueWithdrawalUrl(
channel->inputChannel,
result.result
)).done([=](const MTPstats_BroadcastRevenueWithdrawalUrl &r) {
done(qs(r.data().vurl()));
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
if (channel) {
session->api().request(
MTPstats_GetBroadcastRevenueWithdrawalUrl(
channel->inputChannel,
result.result
)).done([=](const ChannelOutUrl &r) {
done(qs(r.data().vurl()));
}).fail(fail).send();
} else if (peer) {
session->api().request(
MTPpayments_GetStarsRevenueWithdrawalUrl(
peer->input,
MTP_long(receiver.creditsAmount()),
result.result
)).done([=](const CreditsOutUrl &r) {
done(qs(r.data().vurl()));
}).fail(fail).send();
}
});
show->show(Box<PasscodeBox>(session, fields));
});
};
button->setClickedCallback([=] {
if (state->loading) {
return;
}
const auto fail = [=](const MTP::Error &error) {
auto box = PrePasswordErrorBox(
error.type(),
session,
TextWithEntities{
tr::lng_channel_earn_out_check_password_about(tr::now),
});
if (box) {
show->show(std::move(box));
state->loading = false;
} else {
processOut();
}
};
if (channel) {
session->api().request(
MTPstats_GetBroadcastRevenueWithdrawalUrl(
channel->inputChannel,
MTP_inputCheckPasswordEmpty()
)).fail(fail).send();
} else if (peer) {
session->api().request(
MTPpayments_GetStarsRevenueWithdrawalUrl(
peer->input,
MTP_long(std::numeric_limits<int64_t>::max()),
MTP_inputCheckPasswordEmpty()
)).fail(fail).send();
}
});
}

View file

@ -21,8 +21,14 @@ void RestrictSponsored(
bool restricted,
Fn<void(QString)> failed);
struct RewardReceiver final {
ChannelData *currencyReceiver = nullptr;
PeerData *creditsReceiver = nullptr;
Fn<uint64()> creditsAmount;
};
void HandleWithdrawalButton(
not_null<ChannelData*> channel,
RewardReceiver receiver,
not_null<Ui::RippleButton*> button,
std::shared_ptr<Ui::Show> show);

View file

@ -0,0 +1,27 @@
/*
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 Api {
template <typename Type>
void PerformForUpdate(
const MTPUpdates &updates,
Fn<void(const Type &)> callback) {
updates.match([&](const MTPDupdates &updates) {
for (const auto &update : updates.vupdates().v) {
update.match([&](const Type &d) {
callback(d);
}, [](const auto &) {
});
}
}, [](const auto &) {
});
}
} // namespace Api

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_statistics.h"
#include "api/api_statistics_data_deserialize.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
@ -15,33 +16,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_story.h"
#include "history/history.h"
#include "main/main_session.h"
#include "statistics/statistics_data_deserialize.h"
namespace Api {
namespace {
constexpr auto kCheckRequestsTimer = 10 * crl::time(1000);
[[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;
@ -223,61 +201,6 @@ Statistics::Statistics(not_null<ChannelData*> channel)
: StatisticsRequestSender(channel) {
}
StatisticsRequestSender::StatisticsRequestSender(not_null<ChannelData *> channel)
: _channel(channel)
, _api(&_channel->session().api().instance())
, _timer([=] { checkRequests(); }) {
}
StatisticsRequestSender::~StatisticsRequestSender() {
for (const auto &[dcId, ids] : _requests) {
for (const auto id : ids) {
_channel->session().api().unregisterStatsRequest(dcId, id);
}
}
}
void StatisticsRequestSender::checkRequests() {
for (auto i = begin(_requests); i != end(_requests);) {
for (auto j = begin(i->second); j != end(i->second);) {
if (_api.pending(*j)) {
++j;
} else {
_channel->session().api().unregisterStatsRequest(
i->first,
*j);
j = i->second.erase(j);
}
}
if (i->second.empty()) {
i = _requests.erase(i);
} else {
++i;
}
}
if (_requests.empty()) {
_timer.cancel();
}
}
template <typename Request, typename, typename>
auto StatisticsRequestSender::makeRequest(Request &&request) {
const auto id = _api.allocateRequestId();
const auto dcId = _channel->owner().statsDcId(_channel);
if (dcId) {
_channel->session().api().registerStatsRequest(dcId, id);
_requests[dcId].emplace(id);
if (!_timer.isActive()) {
_timer.callEach(kCheckRequestsTimer);
}
}
return std::move(_api.request(
std::forward<Request>(request)
).toDC(
dcId ? MTP::ShiftDcId(dcId, MTP::kStatsDcShift) : 0
).overrideId(id));
}
rpl::producer<rpl::no_value, QString> Statistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
@ -747,11 +670,11 @@ Data::BoostStatus Boosts::boostStatus() const {
return _boostStatus;
}
EarnStatistics::EarnStatistics(not_null<ChannelData*> channel)
ChannelEarnStatistics::ChannelEarnStatistics(not_null<ChannelData*> channel)
: StatisticsRequestSender(channel) {
}
rpl::producer<rpl::no_value, QString> EarnStatistics::request() {
rpl::producer<rpl::no_value, QString> ChannelEarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
@ -795,7 +718,7 @@ rpl::producer<rpl::no_value, QString> EarnStatistics::request() {
};
}
void EarnStatistics::requestHistory(
void ChannelEarnStatistics::requestHistory(
const Data::EarnHistorySlice::OffsetToken &token,
Fn<void(Data::EarnHistorySlice)> done) {
if (_requestId) {
@ -865,7 +788,7 @@ void EarnStatistics::requestHistory(
}).send();
}
Data::EarnStatistics EarnStatistics::data() const {
Data::EarnStatistics ChannelEarnStatistics::data() const {
return _data;
}

View file

@ -7,45 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "api/api_statistics_sender.h"
#include "data/data_boosts.h"
#include "data/data_channel_earn.h"
#include "data/data_statistics.h"
#include "mtproto/sender.h"
class ChannelData;
class PeerData;
namespace Api {
class StatisticsRequestSender {
protected:
explicit StatisticsRequestSender(not_null<ChannelData*> channel);
~StatisticsRequestSender();
template <
typename Request,
typename = std::enable_if_t<!std::is_reference_v<Request>>,
typename = typename Request::Unboxed>
[[nodiscard]] auto makeRequest(Request &&request);
[[nodiscard]] MTP::Sender &api() {
return _api;
}
[[nodiscard]] not_null<ChannelData*> channel() {
return _channel;
}
private:
void checkRequests();
const not_null<ChannelData*> _channel;
MTP::Sender _api;
base::Timer _timer;
base::flat_map<MTP::DcId, base::flat_set<mtpRequestId>> _requests;
};
class Statistics final : public StatisticsRequestSender {
public:
explicit Statistics(not_null<ChannelData*> channel);
@ -108,9 +79,9 @@ private:
};
class EarnStatistics final : public StatisticsRequestSender {
class ChannelEarnStatistics final : public StatisticsRequestSender {
public:
explicit EarnStatistics(not_null<ChannelData*> channel);
explicit ChannelEarnStatistics(not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
void requestHistory(

View file

@ -0,0 +1,35 @@
/*
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_data_deserialize.h"
#include "data/data_statistics_chart.h"
#include "statistics/statistics_data_deserialize.h"
namespace Api {
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()) };
});
}
} // namespace Api

View file

@ -0,0 +1,19 @@
/*
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 StatisticalGraph;
} // namespace Data
namespace Api {
[[nodiscard]] Data::StatisticalGraph StatisticalGraphFromTL(
const MTPStatsGraph &tl);
} // namespace Api

View file

@ -0,0 +1,86 @@
/*
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_sender.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Api {
StatisticsRequestSender::StatisticsRequestSender(
not_null<PeerData*> peer)
: _peer(peer)
, _channel(peer->asChannel())
, _user(peer->asUser())
, _api(&_peer->session().api().instance())
, _timer([=] { checkRequests(); }) {
}
MTP::Sender &StatisticsRequestSender::api() {
return _api;
}
not_null<ChannelData*> StatisticsRequestSender::channel() {
Expects(_channel);
return _channel;
}
not_null<UserData*> StatisticsRequestSender::user() {
Expects(_user);
return _user;
}
void StatisticsRequestSender::checkRequests() {
for (auto i = begin(_requests); i != end(_requests);) {
for (auto j = begin(i->second); j != end(i->second);) {
if (_api.pending(*j)) {
++j;
} else {
_peer->session().api().unregisterStatsRequest(
i->first,
*j);
j = i->second.erase(j);
}
}
if (i->second.empty()) {
i = _requests.erase(i);
} else {
++i;
}
}
if (_requests.empty()) {
_timer.cancel();
}
}
auto StatisticsRequestSender::ensureRequestIsRegistered()
-> StatisticsRequestSender::Registered {
const auto id = _api.allocateRequestId();
const auto dcId = _peer->owner().statsDcId(_peer);
if (dcId) {
_peer->session().api().registerStatsRequest(dcId, id);
_requests[dcId].emplace(id);
if (!_timer.isActive()) {
constexpr auto kCheckRequestsTimer = 10 * crl::time(1000);
_timer.callEach(kCheckRequestsTimer);
}
}
return StatisticsRequestSender::Registered{ id, dcId };
}
StatisticsRequestSender::~StatisticsRequestSender() {
for (const auto &[dcId, ids] : _requests) {
for (const auto id : ids) {
_peer->session().api().unregisterStatsRequest(dcId, id);
}
}
}
} // namespace Api

View file

@ -0,0 +1,58 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "mtproto/sender.h"
class ChannelData;
class PeerData;
class UserData;
namespace Api {
class StatisticsRequestSender {
protected:
explicit StatisticsRequestSender(not_null<PeerData*> peer);
~StatisticsRequestSender();
template <
typename Request,
typename = std::enable_if_t<!std::is_reference_v<Request>>,
typename = typename Request::Unboxed>
[[nodiscard]] auto makeRequest(Request &&request) {
const auto [id, dcId] = ensureRequestIsRegistered();
return std::move(_api.request(
std::forward<Request>(request)
).toDC(
dcId ? MTP::ShiftDcId(dcId, MTP::kStatsDcShift) : 0
).overrideId(id));
}
[[nodiscard]] MTP::Sender &api();
[[nodiscard]] not_null<ChannelData*> channel();
[[nodiscard]] not_null<UserData*> user();
private:
struct Registered final {
mtpRequestId id;
MTP::DcId dcId;
};
[[nodiscard]] Registered ensureRequestIsRegistered();
void checkRequests();
const not_null<PeerData*> _peer;
ChannelData * const _channel;
UserData * const _user;
MTP::Sender _api;
base::Timer _timer;
base::flat_map<MTP::DcId, base::flat_set<mtpRequestId>> _requests;
};
} // namespace Api

View file

@ -1704,7 +1704,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
const auto peerId = peerFromMTP(d.vpeer());
const auto msgId = d.vmsg_id().v;
if (const auto item = session().data().message(peerId, msgId)) {
item->applyEdition(d.vextended_media());
item->applyEdition(d.vextended_media().v);
}
} break;
@ -2129,6 +2129,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
};
if (IsForceLogoutNotification(d)) {
Core::App().forceLogOut(&session().account(), text);
} else if (IsWithdrawalNotification(d)) {
return;
} else if (d.is_popup()) {
const auto &windows = session().windows();
if (!windows.empty()) {
@ -2630,4 +2632,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
}
}
bool IsWithdrawalNotification(const MTPDupdateServiceNotification &data) {
return qs(data.vtype()).startsWith(u"API_WITHDRAWAL_FEATURE_DISABLED_"_q);
}
} // namespace Api

View file

@ -211,4 +211,7 @@ private:
};
[[nodiscard]] bool IsWithdrawalNotification(
const MTPDupdateServiceNotification &);
} // namespace Api

View file

@ -55,7 +55,9 @@ void ViewsManager::removeIncremented(not_null<PeerData*> peer) {
_incremented.remove(peer);
}
void ViewsManager::pollExtendedMedia(not_null<HistoryItem*> item) {
void ViewsManager::pollExtendedMedia(
not_null<HistoryItem*> item,
bool force) {
if (!item->isRegular()) {
return;
}
@ -63,14 +65,20 @@ void ViewsManager::pollExtendedMedia(not_null<HistoryItem*> item) {
const auto peer = item->history()->peer;
auto &request = _pollRequests[peer];
if (request.ids.contains(id) || request.sent.contains(id)) {
return;
if (!force || request.forced) {
return;
}
}
request.ids.emplace(id);
if (!request.id && !request.when) {
request.when = crl::now() + kPollExtendedMediaPeriod;
if (force) {
request.forced = true;
}
if (!_pollTimer.isActive()) {
_pollTimer.callOnce(kPollExtendedMediaPeriod);
const auto delay = force ? 1 : kPollExtendedMediaPeriod;
if (!request.id && (!request.when || force)) {
request.when = crl::now() + delay;
}
if (!_pollTimer.isActive() || force) {
_pollTimer.callOnce(delay);
}
}
@ -160,9 +168,12 @@ void ViewsManager::sendPollRequests(
if (i->second.ids.empty()) {
i = _pollRequests.erase(i);
} else {
i->second.when = now + kPollExtendedMediaPeriod;
if (!_pollTimer.isActive()) {
_pollTimer.callOnce(kPollExtendedMediaPeriod);
const auto delay = i->second.forced
? 1
: kPollExtendedMediaPeriod;
i->second.when = now + delay;
if (!_pollTimer.isActive() || i->second.forced) {
_pollTimer.callOnce(delay);
}
++i;
}

View file

@ -26,7 +26,7 @@ public:
void scheduleIncrement(not_null<HistoryItem*> item);
void removeIncremented(not_null<PeerData*> peer);
void pollExtendedMedia(not_null<HistoryItem*> item);
void pollExtendedMedia(not_null<HistoryItem*> item, bool force = false);
private:
struct PollExtendedMediaRequest {
@ -34,6 +34,7 @@ private:
mtpRequestId id = 0;
base::flat_set<MsgId> ids;
base::flat_set<MsgId> sent;
bool forced = false;
};
void viewsIncrement();

View file

@ -2204,7 +2204,8 @@ void ApiWrap::saveDraftsToCloud() {
entities,
Data::WebPageForMTP(
cloudDraft->webpage,
textWithTags.text.isEmpty())
textWithTags.text.isEmpty()),
MTP_long(0) // effect
)).done([=](const MTPBool &result, const MTP::Response &response) {
const auto requestId = response.requestId;
history->finishSavingCloudDraft(
@ -4259,7 +4260,11 @@ void ApiWrap::sendMediaWithRandomId(
MTP_flags(flags),
peer->input,
Data::Histories::ReplyToPlaceholder(),
media,
(options.price
? MTPInputMedia(MTP_inputMediaPaidMedia(
MTP_long(options.price),
MTP_vector<MTPInputMedia>(1, media)))
: media),
MTP_string(caption.text),
MTP_long(randomId),
MTPReplyMarkup(),
@ -4281,6 +4286,82 @@ void ApiWrap::sendMediaWithRandomId(
});
}
void ApiWrap::sendMultiPaidMedia(
not_null<HistoryItem*> item,
not_null<SendingAlbum*> album,
Fn<void(bool)> done) {
Expects(album->options.price > 0);
const auto groupId = album->groupId;
const auto &options = album->options;
const auto randomId = album->items.front().randomId;
auto medias = album->items | ranges::view::transform([](
const SendingAlbum::Item &part) {
Assert(part.media.has_value());
return MTPInputMedia(part.media->data().vmedia());
}) | ranges::to<QVector<MTPInputMedia>>();
const auto history = item->history();
const auto replyTo = item->replyTo();
auto caption = item->originalText();
TextUtilities::Trim(caption);
auto sentEntities = Api::EntitiesToMTP(
_session,
caption.entities,
Api::ConvertOption::SkipLocal);
using Flag = MTPmessages_SendMedia::Flag;
const auto flags = Flag(0)
| (replyTo ? Flag::f_reply_to : Flag(0))
| (ShouldSendSilent(history->peer, options)
? Flag::f_silent
: Flag(0))
| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))
| (options.scheduled ? Flag::f_schedule_date : Flag(0))
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0));
auto &histories = history->owner().histories();
const auto peer = history->peer;
const auto itemId = item->fullId();
histories.sendPreparedMessage(
history,
replyTo,
randomId,
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
MTP_flags(flags),
peer->input,
Data::Histories::ReplyToPlaceholder(),
MTP_inputMediaPaidMedia(
MTP_long(options.price),
MTP_vector<MTPInputMedia>(std::move(medias))),
MTP_string(caption.text),
MTP_long(randomId),
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (const auto album = _sendingAlbums.take(groupId)) {
const auto copy = (*album)->items;
for (const auto &part : copy) {
if (const auto item = history->owner().message(part.msgId)) {
item->destroy();
}
}
}
if (done) done(true);
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (done) done(false);
sendMessageFail(error, peer, randomId, itemId);
});
}
void ApiWrap::sendAlbumWithUploaded(
not_null<HistoryItem*> item,
const MessageGroupId &groupId,
@ -4334,8 +4415,11 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
if (!sample) {
_sendingAlbums.remove(groupId);
return;
} else if (album->options.price > 0) {
sendMultiPaidMedia(sample, album);
return;
} else if (medias.size() < 2) {
const auto &single = medias.front().c_inputSingleMedia();
const auto &single = medias.front().data();
sendMediaWithRandomId(
sample,
single.vmedia(),

View file

@ -545,6 +545,10 @@ private:
Api::SendOptions options,
uint64 randomId,
Fn<void(bool)> done = nullptr);
void sendMultiPaidMedia(
not_null<HistoryItem*> item,
not_null<SendingAlbum*> album,
Fn<void(bool)> done = nullptr);
void getTopPromotionDelayed(TimeId now, TimeId next);
void topPromotionDone(const MTPhelp_PromoData &proxy);

View file

@ -237,7 +237,7 @@ shareColumnSkip: 6px;
shareActivateDuration: 150;
shareScrollDuration: 300;
shareComment: InputField(defaultInputField) {
font: normalFont;
style: defaultTextStyle;
textMargins: margins(8px, 8px, 8px, 6px);
heightMin: 36px;
heightMax: 72px;
@ -290,6 +290,29 @@ passcodeTextLine: 28px;
passcodeLittleSkip: 5px;
passcodeAboutSkip: 7px;
passcodeSkip: 23px;
passcodeSystemUnlock: IconButton(defaultIconButton) {
width: 32px;
height: 36px;
iconPosition: point(4px, 4px);
rippleAreaSize: 32px;
rippleAreaPosition: point(0px, 0px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgOver;
}
}
passcodeSystemWinHello: IconButton(passcodeSystemUnlock) {
icon: icon{{ "menu/passcode_winhello", lightButtonFg }};
iconOver: icon{{ "menu/passcode_winhello", lightButtonFg }};
}
passcodeSystemTouchID: IconButton(passcodeSystemUnlock) {
icon: icon{{ "menu/passcode_finger", lightButtonFg }};
iconOver: icon{{ "menu/passcode_finger", lightButtonFg }};
}
passcodeSystemUnlockLater: FlatLabel(defaultFlatLabel) {
align: align(top);
textFg: windowSubTextFg;
}
passcodeSystemUnlockSkip: 12px;
newGroupAboutFg: windowSubTextFg;
newGroupPadding: margins(4px, 6px, 4px, 3px);
@ -585,7 +608,7 @@ groupStickersRemovePosition: point(6px, 6px);
groupStickersFieldPadding: margins(8px, 6px, 8px, 6px);
groupStickersField: InputField(defaultMultiSelectSearchField) {
placeholderFont: boxTextFont;
font: boxTextFont;
style: boxTextStyle;
placeholderMargins: margins(0px, 0px, 0px, 0px);
textMargins: margins(0px, 7px, 0px, 0px);
textBg: boxBg;
@ -672,7 +695,6 @@ themesMenuToggle: IconButton(defaultIconButton) {
themesMenuPosition: point(-2px, 25px);
createPollField: InputField(defaultInputField) {
font: boxTextFont;
textMargins: margins(0px, 4px, 0px, 4px);
textAlign: align(left);
heightMin: 36px;
@ -877,7 +899,6 @@ scheduleDateField: InputField(defaultInputField) {
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(top);
font: font(14px);
}
scheduleTimeField: InputField(scheduleDateField) {
border: 0px;
@ -905,7 +926,6 @@ muteBoxTimeField: InputField(scheduleDateField) {
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(left);
font: font(14px);
}
muteBoxTimeFieldPadding: margins(5px, 0px, 5px, 0px);

View file

@ -81,7 +81,7 @@ void ChangeFilterById(
MTP_int(filter.id()),
filter.tl()
)).done([=, chat = history->peer->name(), name = filter.title()] {
const auto account = &history->session().account();
const auto account = not_null(&history->session().account());
if (const auto controller = Core::App().windowFor(account)) {
controller->showToast((add
? tr::lng_filters_toast_add

View file

@ -1044,7 +1044,16 @@ not_null<Ui::InputField*> CreatePollBox::setupSolution(
solution->setInstantReplaces(Ui::InstantReplaces::Default());
solution->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
solution->setMarkdownReplacesEnabled(true);
solution->setMarkdownReplacesEnabled(rpl::single(
Ui::MarkdownEnabledState{ Ui::MarkdownEnabled{ {
Ui::InputField::kTagBold,
Ui::InputField::kTagItalic,
Ui::InputField::kTagUnderline,
Ui::InputField::kTagStrikeOut,
Ui::InputField::kTagCode,
Ui::InputField::kTagSpoiler,
} } }
));
solution->setEditLinkCallback(
DefaultEditLinkCallback(_controller->uiShow(), solution));
solution->customTab(true);

View file

@ -463,6 +463,7 @@ void EditCaptionBox::rebuildPreview() {
st::defaultComposeControls,
gifPaused,
file,
[] { return true; },
Ui::AttachControls::Type::EditOnly);
_isPhoto = (media && media->isPhoto());
const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);

View file

@ -1011,14 +1011,16 @@ void GiftPremiumValidator::showChoosePeerBox(const QString &ref) {
}) | ranges::views::filter([](UserData *u) -> bool {
return u;
}) | ranges::to<std::vector<not_null<UserData*>>>();
if (!users.empty()) {
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
if (users.empty()) {
show->showToast(
tr::lng_settings_gift_premium_choose(tr::now));
}
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
(*ignoreClose) = true;
peersBox->closeBox();
};
@ -1644,12 +1646,60 @@ void AddCreditsHistoryEntryTable(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
if (entry.bareId) {
const auto peerId = PeerId(entry.barePeerId);
if (peerId) {
auto text = tr::lng_credits_box_history_entry_peer();
AddTableRow(table, std::move(text), controller, peerId);
}
if (const auto msgId = MsgId(peerId ? entry.bareMsgId : 0)) {
const auto session = &controller->session();
const auto peer = session->data().peer(peerId);
if (const auto channel = peer->asBroadcast()) {
const auto username = channel->username();
const auto base = username.isEmpty()
? u"c/%1"_q.arg(peerToChannel(channel->id).bare)
: username;
const auto query = base + '/' + QString::number(msgId.bare);
const auto link = session->createInternalLink(query);
auto label = object_ptr<Ui::FlatLabel>(
table,
rpl::single(Ui::Text::Link(link)),
st::giveawayGiftCodeValue);
label->setClickHandlerFilter([=](const auto &...) {
controller->showPeerHistory(channel, {}, msgId);
return false;
});
AddTableRow(
table,
tr::lng_credits_box_history_entry_media(),
std::move(label),
st::giveawayGiftCodeValueMargin);
}
}
using Type = Data::CreditsHistoryEntry::PeerType;
if (entry.peerType == Type::AppStore) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer(),
controller,
PeerId(entry.bareId));
tr::lng_credits_box_history_entry_via(),
tr::lng_credits_box_history_entry_app_store(
Ui::Text::RichLangValue));
} else if (entry.peerType == Type::PlayMarket) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_via(),
tr::lng_credits_box_history_entry_play_market(
Ui::Text::RichLangValue));
} else if (entry.peerType == Type::Fragment) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_via(),
tr::lng_credits_box_history_entry_fragment(
Ui::Text::RichLangValue));
} else if (entry.peerType == Type::Ads) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_via(),
tr::lng_credits_box_history_entry_ads(Ui::Text::RichLangValue));
}
if (!entry.id.isEmpty()) {
constexpr auto kOneLineCount = 18;
@ -1680,4 +1730,17 @@ void AddCreditsHistoryEntryTable(
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
}
if (!entry.successDate.isNull()) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_success_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
}
if (!entry.successLink.isEmpty()) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_success_url(),
rpl::single(
Ui::Text::Link(entry.successLink, entry.successLink)));
}
}

View file

@ -133,8 +133,8 @@ void MaxInviteBox::paintEvent(QPaintEvent *e) {
auto option = QTextOption(style::al_left);
option.setWrapMode(QTextOption::WrapAnywhere);
p.setFont(_linkOver
? st::defaultInputField.font->underline()
: st::defaultInputField.font);
? st::defaultInputField.style.font->underline()
: st::defaultInputField.style.font);
p.setPen(st::defaultLinkButton.color);
const auto inviteLinkText = _channel->inviteLink().isEmpty()
? tr::lng_group_invite_create(tr::now)

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_peer_info_box.h"
#include "apiwrap.h"
#include "api/api_credits.h"
#include "api/api_peer_photo.h"
#include "api/api_user_names.h"
#include "main/main_session.h"
@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_premium_limits.h"
#include "data/data_user.h"
#include "history/admin_log/history_admin_log_section.h"
#include "info/bot/earn/info_earn_widget.h"
#include "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/info_memento.h"
@ -52,6 +54,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/boost_box.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/premium_graphics.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/vertical_list.h"
#include "ui/toast/toast.h"
@ -71,6 +75,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include <QtSvg/QSvgRenderer>
namespace {
constexpr auto kBotManagerUsername = "BotFather"_cs;
@ -343,6 +349,7 @@ private:
void fillPendingRequestsButton();
void fillBotUsernamesButton();
void fillBotBalanceButton();
void fillBotEditIntroButton();
void fillBotEditCommandsButton();
void fillBotEditSettingsButton();
@ -1126,6 +1133,7 @@ void Controller::fillManageSection() {
::AddSkip(container, 0);
fillBotUsernamesButton();
fillBotBalanceButton();
fillBotEditIntroButton();
fillBotEditCommandsButton();
fillBotEditSettingsButton();
@ -1536,6 +1544,84 @@ void Controller::fillBotUsernamesButton() {
{ &st::menuIconLinks });
}
void Controller::fillBotBalanceButton() {
Expects(_isBot);
struct State final {
rpl::variable<QString> balance;
};
auto &lifetime = _controls.buttonsLayout->lifetime();
const auto state = lifetime.make_state<State>();
const auto wrap = _controls.buttonsLayout->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
_controls.buttonsLayout,
EditPeerInfoBox::CreateButton(
_controls.buttonsLayout,
tr::lng_manage_peer_bot_balance(),
state->balance.value(),
[controller = _navigation->parentController(), peer = _peer] {
controller->showSection(Info::BotEarn::Make(peer));
},
st::manageGroupButton,
{})));
wrap->toggle(false, anim::type::instant);
const auto button = wrap->entity();
{
const auto api = button->lifetime().make_state<Api::CreditsStatus>(
_peer);
api->request({}, [=](Data::CreditsStatusSlice data) {
if (data.balance) {
wrap->toggle(true, anim::type::normal);
}
state->balance = QString::number(data.balance);
});
}
{
constexpr auto kSizeShift = 3;
constexpr auto kStrokeWidth = 5;
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
icon->resize(Size(st::menuIconLinks.width() - kSizeShift));
auto colorized = [&] {
auto f = QFile(Ui::Premium::Svg());
if (!f.open(QIODevice::ReadOnly)) {
return QString();
}
return QString::fromUtf8(
f.readAll()).replace(u"#fff"_q, u"#ffffff00"_q);
}();
colorized.replace(
u"stroke=\"none\""_q,
u"stroke=\"%1\""_q.arg(st::menuIconColor->c.name()));
colorized.replace(
u"stroke-width=\"1\""_q,
u"stroke-width=\"%1\""_q.arg(kStrokeWidth));
const auto svg = icon->lifetime().make_state<QSvgRenderer>(
colorized.toUtf8());
svg->setViewBox(svg->viewBox() + Margins(kStrokeWidth));
const auto starSize = Size(icon->height());
icon->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(icon);
svg->render(&p, Rect(starSize));
}, icon->lifetime());
button->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
icon->moveToLeft(
button->st().iconLeft + kSizeShift / 2.,
(size.height() - icon->height()) / 2);
}, icon->lifetime());
}
}
void Controller::fillBotEditIntroButton() {
Expects(_isBot);

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/event_filter.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h"
@ -351,8 +352,8 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
};
raw->setCustomEmojiFactory([=](QStringView data, Fn<void()> update)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
auto factory = [=](QStringView data, Fn<void()> update)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto id = Data::ParseCustomEmojiData(data);
auto result = owner->customEmojiManager().create(
data,
@ -364,7 +365,13 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
}
using namespace Ui::Text;
return std::make_unique<FirstFrameEmoji>(std::move(result));
}, std::move(customEmojiPaused));
};
raw->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
});
}, customEmojiPaused, customEmojiPaused, std::move(factory));
const auto callback = args.callback;
const auto isCustom = [=](DocumentId id) {

View file

@ -131,6 +131,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_subtitle_translation();
case PremiumFeature::Business:
return tr::lng_premium_summary_subtitle_business();
case PremiumFeature::Effects:
return tr::lng_premium_summary_subtitle_effects();
case PremiumFeature::BusinessLocation:
return tr::lng_business_subtitle_location();
@ -192,6 +194,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_about_translation();
case PremiumFeature::Business:
return tr::lng_premium_summary_about_business();
case PremiumFeature::Effects:
return tr::lng_premium_summary_about_effects();
case PremiumFeature::BusinessLocation:
return tr::lng_business_about_location();
@ -529,6 +533,7 @@ struct VideoPreviewDocument {
case PremiumFeature::Wallpapers: return "wallpapers";
case PremiumFeature::LastSeen: return "last_seen";
case PremiumFeature::MessagePrivacy: return "message_privacy";
case PremiumFeature::Effects: return "effects";
case PremiumFeature::BusinessLocation: return "business_location";
case PremiumFeature::BusinessHours: return "business_hours";

View file

@ -70,6 +70,7 @@ enum class PremiumFeature {
LastSeen,
MessagePrivacy,
Business,
Effects,
// Business features.
BusinessLocation,

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/data_credits.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
@ -39,6 +40,147 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_settings.h"
namespace Ui {
namespace {
struct PaidMediaData {
const Data::Invoice *invoice = nullptr;
HistoryItem *item = nullptr;
PeerData *peer = nullptr;
int photos = 0;
int videos = 0;
explicit operator bool() const {
return invoice && item && peer && (photos || videos);
}
};
[[nodiscard]] PaidMediaData LookupPaidMediaData(
not_null<Main::Session*> session,
not_null<Payments::CreditsFormData*> form) {
using namespace Payments;
const auto message = std::get_if<InvoiceMessage>(&form->id.value);
const auto item = message
? session->data().message(message->peer, message->itemId)
: nullptr;
const auto media = item ? item->media() : nullptr;
const auto invoice = media ? media->invoice() : nullptr;
if (!invoice || !invoice->isPaidMedia) {
return {};
}
auto photos = 0;
auto videos = 0;
for (const auto &media : invoice->extendedMedia) {
const auto photo = media->photo();
if (photo && !photo->extendedMediaVideoDuration().has_value()) {
++photos;
} else {
++videos;
}
}
const auto sender = item->originalSender();
const auto broadcast = (sender && sender->isBroadcast())
? sender
: message->peer.get();
return {
.invoice = invoice,
.item = item,
.peer = broadcast,
.photos = photos,
.videos = videos,
};
}
[[nodiscard]] rpl::producer<TextWithEntities> SendCreditsConfirmText(
not_null<Main::Session*> session,
not_null<Payments::CreditsFormData*> form) {
if (const auto data = LookupPaidMediaData(session, form)) {
auto photos = 0;
auto videos = 0;
for (const auto &media : data.invoice->extendedMedia) {
const auto photo = media->photo();
if (photo && !photo->extendedMediaVideoDuration().has_value()) {
++photos;
} else {
++videos;
}
}
auto photosBold = tr::lng_credits_box_out_photos(
lt_count,
rpl::single(photos) | tr::to_count(),
Ui::Text::Bold);
auto videosBold = tr::lng_credits_box_out_videos(
lt_count,
rpl::single(videos) | tr::to_count(),
Ui::Text::Bold);
auto media = (!videos)
? ((photos > 1)
? std::move(photosBold)
: tr::lng_credits_box_out_photo(Ui::Text::WithEntities))
: (!photos)
? ((videos > 1)
? std::move(videosBold)
: tr::lng_credits_box_out_video(Ui::Text::WithEntities))
: tr::lng_credits_box_out_both(
lt_photo,
std::move(photosBold),
lt_video,
std::move(videosBold),
Ui::Text::WithEntities);
return tr::lng_credits_box_out_media(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_media,
std::move(media),
lt_chat,
rpl::single(Ui::Text::Bold(data.peer->name())),
Ui::Text::RichLangValue);
}
const auto bot = session->data().user(form->botId);
return tr::lng_credits_box_out_sure(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_text,
rpl::single(TextWithEntities{ form->title }),
lt_bot,
rpl::single(TextWithEntities{ bot->name() }),
Ui::Text::RichLangValue);
}
[[nodiscard]] object_ptr<Ui::RpWidget> SendCreditsThumbnail(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
not_null<Payments::CreditsFormData*> form,
int photoSize) {
if (const auto data = LookupPaidMediaData(session, form)) {
const auto first = data.invoice->extendedMedia[0]->photo();
const auto second = (data.photos > 1)
? data.invoice->extendedMedia[1]->photo()
: nullptr;
const auto totalCount = int(data.invoice->extendedMedia.size());
if (first && first->extendedMediaPreview()) {
return Settings::PaidMediaThumbnail(
parent,
first,
second,
totalCount,
photoSize);
}
}
if (form->photo) {
return Settings::HistoryEntryPhoto(parent, form->photo, photoSize);
}
const auto bot = session->data().user(form->botId);
return object_ptr<Ui::UserpicButton>(
parent,
bot,
st::defaultUserpicButton);
}
} // namespace
void SendCreditsBox(
not_null<Ui::GenericBox*> box,
@ -89,22 +231,10 @@ void SendCreditsBox(
}, ministarsContainer->lifetime());
}
const auto bot = session->data().user(form->botId);
if (form->photo) {
box->addRow(object_ptr<Ui::CenterWrap<>>(
content,
Settings::HistoryEntryPhoto(content, form->photo, photoSize)));
} else {
const auto widget = box->addRow(
object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::UserpicButton>(
content,
bot,
st::defaultUserpicButton)));
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
}
const auto thumb = box->addRow(object_ptr<Ui::CenterWrap<>>(
content,
SendCreditsThumbnail(content, session, form.get(), photoSize)));
thumb->setAttribute(Qt::WA_TransparentForMouseEvents);
Ui::AddSkip(content);
box->addRow(object_ptr<Ui::CenterWrap<>>(
@ -118,14 +248,7 @@ void SendCreditsBox(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_credits_box_out_sure(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_text,
rpl::single(TextWithEntities{ form->title }),
lt_bot,
rpl::single(TextWithEntities{ bot->name() }),
Ui::Text::RichLangValue),
SendCreditsConfirmText(session, form.get()),
st::creditsBoxAbout)));
Ui::AddSkip(content);
Ui::AddSkip(content);
@ -158,26 +281,16 @@ void SendCreditsBox(
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
{
const auto emojiMargin = QMargins(
0,
-st::moderateBoxExpandInnerSkip,
0,
0);
const auto buttonEmoji = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::settingsPremiumIconStar,
emojiMargin,
true));
auto buttonText = tr::lng_credits_box_out_confirm(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_emoji,
rpl::single(buttonEmoji),
rpl::single(CreditsEmojiSmall(session)),
Ui::Text::RichLangValue);
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st::defaultFlatLabel);
st::creditsBoxButtonLabel);
std::move(
buttonText
) | rpl::start_with_next([=](const TextWithEntities &text) {
@ -247,4 +360,22 @@ void SendCreditsBox(
}
}
TextWithEntities CreditsEmoji(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::settingsPremiumIconStar,
QMargins{ 0, -st::moderateBoxExpandInnerSkip, 0, 0 },
true),
QString(QChar(0x2B50)));
}
TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::starIconSmall,
st::starIconSmallPadding,
true),
QString(QChar(0x2B50)));
}
} // namespace Ui

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
namespace Main {
class Session;
} // namespace Main
namespace Payments {
struct CreditsFormData;
} // namespace Payments
@ -22,4 +26,10 @@ void SendCreditsBox(
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void()> sent);
[[nodiscard]] TextWithEntities CreditsEmoji(
not_null<Main::Session*> session);
[[nodiscard]] TextWithEntities CreditsEmojiSmall(
not_null<Main::Session*> session);
} // namespace Ui

View file

@ -10,7 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "storage/storage_media_prepare.h"
#include "iv/iv_instance.h"
#include "mainwidget.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "mtproto/mtproto_config.h"
@ -24,11 +26,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/controls/history_view_characters_limit.h"
#include "history/view/history_view_schedule_box.h"
#include "core/mime_type.h"
#include "core/ui_integration.h"
#include "base/event_filter.h"
#include "base/call_delayed.h"
#include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/send_credits_box.h"
#include "ui/effects/scroll_content_shadow.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/popup_menu.h"
@ -36,10 +41,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/attach/attach_single_file_preview.h"
#include "ui/chat/attach/attach_single_media_preview.h"
#include "ui/grouped_layout.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/controls/emoji_button.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "lottie/lottie_single_player.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_user.h"
#include "data/data_peer_values.h" // Data::AmPremiumValue.
@ -111,6 +119,84 @@ rpl::producer<QString> FieldPlaceholder(
: tr::lng_photos_comment();
}
void EditPriceBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
uint64 price,
Fn<void(uint64)> apply) {
box->setTitle(tr::lng_paid_title());
AddSubsectionTitle(
box->verticalLayout(),
tr::lng_paid_enter_cost(),
(st::boxRowPadding - QMargins(
st::defaultSubsectionTitlePadding.left(),
0,
st::defaultSubsectionTitlePadding.right(),
0)));
const auto limit = session->appConfig().get<int>(
u"stars_paid_post_amount_max"_q,
10'000);
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::editTagField.heightMin));
auto owned = object_ptr<Ui::NumberInput>(
wrap,
st::editTagField,
tr::lng_paid_cost_placeholder(),
price ? QString::number(price) : QString(),
limit);
const auto field = owned.data();
wrap->widthValue() | rpl::start_with_next([=](int width) {
field->move(0, 0);
field->resize(width, field->height());
wrap->resize(width, field->height());
}, wrap->lifetime());
field->selectAll();
box->setFocusCallback([=] {
field->setFocusFast();
});
const auto about = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_paid_about(
lt_link,
tr::lng_paid_about_link() | Ui::Text::ToLink(),
Ui::Text::WithEntities),
st::paidAmountAbout),
st::boxRowPadding + QMargins(0, st::sendMediaRowSkip, 0, 0));
about->setClickHandlerFilter([=](const auto &...) {
Core::App().iv().openWithIvPreferred(
session,
tr::lng_paid_about_link_url(tr::now));
return false;
});
field->paintRequest() | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(field);
st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width());
}, field->lifetime());
const auto save = [=] {
const auto now = field->getLastText().toULongLong();
if (now > limit) {
field->showError();
return;
}
const auto weak = Ui::MakeWeak(box);
apply(now);
if (const auto strong = weak.data()) {
strong->closeBox();
}
};
QObject::connect(field, &Ui::NumberInput::submitted, box, save);
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace
SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) {
@ -161,7 +247,8 @@ SendFilesBox::Block::Block(
int from,
int till,
Fn<bool()> gifPaused,
SendFilesWay way)
SendFilesWay way,
Fn<bool()> canToggleSpoiler)
: _items(items)
, _from(from)
, _till(till) {
@ -178,14 +265,16 @@ SendFilesBox::Block::Block(
parent.get(),
st,
my,
way);
way,
std::move(canToggleSpoiler));
_preview.reset(preview);
} else {
const auto media = Ui::SingleMediaPreview::Create(
parent,
st,
gifPaused,
first);
first,
std::move(canToggleSpoiler));
if (media) {
_isSingleMedia = true;
_preview.reset(media);
@ -261,6 +350,14 @@ rpl::producer<int> SendFilesBox::Block::itemModifyRequest() const {
}
}
rpl::producer<> SendFilesBox::Block::orderUpdated() const {
if (_isAlbum) {
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
return album->orderUpdated();
}
return rpl::never<>();
}
void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {
if (!_isAlbum) {
if (_isSingleMedia) {
@ -329,6 +426,18 @@ void SendFilesBox::Block::applyChanges() {
}
}
QImage SendFilesBox::Block::generatePriceTagBackground() const {
const auto preview = _preview.get();
if (_isAlbum) {
const auto album = static_cast<Ui::AlbumPreview*>(preview);
return album->generatePriceTagBackground();
} else if (_isSingleMedia) {
const auto media = static_cast<Ui::SingleMediaPreview*>(preview);
return media->generatePriceTagBackground();
}
return QImage();
}
SendFilesBox::SendFilesBox(
QWidget*,
not_null<Window::SessionController*> controller,
@ -393,6 +502,9 @@ Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
: _invertCaption
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below;
result.price = canChangePrice()
? _price.current()
: std::optional<uint64>();
return result;
});
}
@ -406,6 +518,7 @@ auto SendFilesBox::prepareSendMenuCallback()
case Type::CaptionUp: _invertCaption = true; break;
case Type::SpoilerOn: toggleSpoilers(true); break;
case Type::SpoilerOff: toggleSpoilers(false); break;
case Type::ChangePrice: changePrice(); break;
default:
SendMenu::DefaultCallback(
_show,
@ -596,14 +709,27 @@ void SendFilesBox::refreshButtons() {
addMenuButton();
}
bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const {
bool SendFilesBox::hasSendMenu(const MenuDetails &details) const {
return (details.type != SendMenu::Type::Disabled)
|| (details.spoiler != SendMenu::SpoilerState::None)
|| (details.caption != SendMenu::CaptionState::None);
}
bool SendFilesBox::hasSpoilerMenu() const {
return _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos());
return !hasPrice()
&& _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos());
}
bool SendFilesBox::canChangePrice() const {
const auto way = _sendWay.current();
const auto broadcast = _captionToPeer
? _captionToPeer->asBroadcast()
: nullptr;
return broadcast
&& broadcast->canPostPaidMedia()
&& _list.canChangePrice(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos());
}
void SendFilesBox::applyBlockChanges() {
@ -626,6 +752,118 @@ void SendFilesBox::toggleSpoilers(bool enabled) {
}
}
void SendFilesBox::changePrice() {
const auto weak = Ui::MakeWeak(this);
const auto session = &_show->session();
const auto now = _price.current();
_show->show(Box(EditPriceBox, session, now, [=](uint64 price) {
if (weak && price != now) {
_price = price;
refreshPriceTag();
}
}));
}
bool SendFilesBox::hasPrice() const {
return canChangePrice() && _price.current() > 0;
}
void SendFilesBox::refreshPriceTag() {
const auto resetSpoilers = hasPrice() || _priceTag;
if (resetSpoilers) {
for (auto &file : _list.files) {
file.spoiler = false;
}
for (auto &block : _blocks) {
block.toggleSpoilers(hasPrice());
}
}
if (!hasPrice()) {
_priceTag = nullptr;
_priceTagBg = QImage();
} else if (!_priceTag) {
_priceTag = std::make_unique<Ui::RpWidget>(_inner.data());
const auto raw = _priceTag.get();
raw->show();
raw->paintRequest() | rpl::start_with_next([=] {
if (_priceTagBg.isNull()) {
_priceTagBg = preparePriceTagBg(raw->size());
}
QPainter(raw).drawImage(0, 0, _priceTagBg);
}, raw->lifetime());
const auto session = &_show->session();
auto price = _price.value() | rpl::map([=](uint64 amount) {
auto result = Ui::Text::Colorized(Ui::CreditsEmoji(session));
result.append(Lang::FormatCountDecimal(amount));
return result;
});
auto text = tr::lng_paid_price(
lt_price,
std::move(price),
Ui::Text::WithEntities);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString(),
st::paidTagLabel);
std::move(text) | rpl::start_with_next([=](TextWithEntities &&text) {
label->setMarkedText(text, Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { label->update(); },
});
}, label->lifetime());
label->show();
label->sizeValue() | rpl::start_with_next([=](QSize size) {
const auto inner = QRect(QPoint(), size);
const auto rect = inner.marginsAdded(st::paidTagPadding);
raw->resize(rect.size());
label->move(-rect.topLeft());
}, label->lifetime());
_inner->sizeValue() | rpl::start_with_next([=](QSize size) {
raw->move(
(size.width() - raw->width()) / 2,
(size.height() - raw->height()) / 2);
}, raw->lifetime());
} else {
_priceTag->raise();
_priceTag->update();
_priceTagBg = QImage();
}
}
QImage SendFilesBox::preparePriceTagBg(QSize size) const {
const auto ratio = style::DevicePixelRatio();
const auto outer = _blocks.empty()
? size
: _inner->widgetAt(0)->geometry().size();
auto bg = _blocks.empty()
? QImage()
: _blocks.front().generatePriceTagBackground();
if (bg.isNull()) {
bg = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);
bg.fill(Qt::black);
}
auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::black);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.drawImage(
QRect(
(size.width() - outer.width()) / 2,
(size.height() - outer.height()) / 2,
outer.width(),
outer.height()),
bg);
p.fillRect(QRect(QPoint(), size), st::msgDateImgBg);
p.end();
const auto radius = std::min(size.width(), size.height()) / 2;
return Images::Round(std::move(result), Images::CornersMask(radius));
}
void SendFilesBox::addMenuButton() {
const auto details = _sendMenuDetails();
if (!hasSendMenu(details)) {
@ -719,6 +957,7 @@ void SendFilesBox::initSendWay() {
block.setSendWay(value);
}
refreshButtons();
refreshPriceTag();
if (was != hidden()) {
updateBoxSize();
updateControlsGeometry();
@ -804,7 +1043,8 @@ void SendFilesBox::pushBlock(int from, int till) {
from,
till,
gifPaused,
_sendWay.current());
_sendWay.current(),
[=] { return !hasPrice(); });
auto &block = _blocks.back();
const auto widget = _inner->add(
block.takeWidget(),
@ -927,10 +1167,18 @@ void SendFilesBox::pushBlock(int from, int till) {
st::sendMediaPreviewSize,
[=] { refreshAllAfterChanges(from); });
}, widget->lifetime());
block.orderUpdated() | rpl::start_with_next([=]{
if (_priceTag) {
_priceTagBg = QImage();
_priceTag->update();
}
}, widget->lifetime());
}
void SendFilesBox::refreshControls(bool initial) {
refreshButtons();
refreshPriceTag();
refreshTitleText();
updateSendWayControls();
updateCaptionPlaceholder();
@ -1499,6 +1747,7 @@ void SendFilesBox::send(
auto child = _sendMenuDetails();
child.spoiler = SendMenu::SpoilerState::None;
child.caption = SendMenu::CaptionState::None;
child.price = std::nullopt;
return SendMenu::DefaultCallback(_show, sendCallback())(
{ .type = SendMenu::ActionType::Schedule },
child);
@ -1526,10 +1775,16 @@ void SendFilesBox::send(
auto caption = (_caption && !_caption->isHidden())
? _caption->getTextWithAppliedMarkdown()
: TextWithTags();
options.invertCaption = _invertCaption;
if (!validateLength(caption.text)) {
return;
}
options.invertCaption = _invertCaption;
options.price = hasPrice() ? _price.current() : 0;
if (options.price > 0) {
for (auto &file : _list.files) {
file.spoiler = false;
}
}
_confirmedCallback(
std::move(_list),
_sendWay.current(),

View file

@ -149,7 +149,8 @@ private:
int from,
int till,
Fn<bool()> gifPaused,
Ui::SendFilesWay way);
Ui::SendFilesWay way,
Fn<bool()> canToggleSpoiler);
Block(Block &&other) = default;
Block &operator=(Block &&other) = default;
@ -160,11 +161,14 @@ private:
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
[[nodiscard]] rpl::producer<int> itemReplaceRequest() const;
[[nodiscard]] rpl::producer<int> itemModifyRequest() const;
[[nodiscard]] rpl::producer<> orderUpdated() const;
void setSendWay(Ui::SendFilesWay way);
void toggleSpoilers(bool enabled);
void applyChanges();
[[nodiscard]] QImage generatePriceTagBackground() const;
private:
base::unique_qptr<Ui::RpWidget> _preview;
not_null<std::vector<Ui::PreparedFile>*> _items;
@ -190,6 +194,12 @@ private:
void addMenuButton();
void applyBlockChanges();
void toggleSpoilers(bool enabled);
void changePrice();
[[nodiscard]] bool canChangePrice() const;
[[nodiscard]] bool hasPrice() const;
void refreshPriceTag();
[[nodiscard]] QImage preparePriceTagBg(QSize size) const;
bool validateLength(const QString &text) const;
void refreshButtons();
@ -251,6 +261,9 @@ private:
SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback;
Fn<void()> _cancelledCallback;
rpl::variable<uint64> _price = 0;
std::unique_ptr<Ui::RpWidget> _priceTag;
QImage _priceTagBg;
bool _confirmed = false;
bool _invertCaption = false;

View file

@ -1393,7 +1393,6 @@ groupCallScheduleDateField: InputField(groupCallField) {
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(top);
font: font(14px);
}
groupCallScheduleTimeField: InputField(groupCallScheduleDateField) {
textBg: groupCallMembersBg;

View file

@ -706,7 +706,8 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
}
}
if (false && data.is_need_rating() && _id && _accessHash) {
const auto window = Core::App().windowFor(_user);
const auto window = Core::App().windowFor(
Window::SeparateId(_user));
const auto session = &_user->session();
const auto callId = _id;
const auto callAccessHash = _accessHash;
@ -1402,7 +1403,8 @@ void Call::handleRequestError(const QString &error) {
_user->name())
: QString();
if (!inform.isEmpty()) {
if (const auto window = Core::App().windowFor(_user)) {
if (const auto window = Core::App().windowFor(
Window::SeparateId(_user))) {
window->show(Ui::MakeInformBox(inform));
} else {
Ui::show(Ui::MakeInformBox(inform));
@ -1420,7 +1422,8 @@ void Call::handleControllerError(const QString &error) {
? tr::lng_call_error_audio_io(tr::now)
: QString();
if (!inform.isEmpty()) {
if (const auto window = Core::App().windowFor(_user)) {
if (const auto window = Core::App().windowFor(
Window::SeparateId(_user))) {
window->show(Ui::MakeInformBox(inform));
} else {
Ui::show(Ui::MakeInformBox(inform));

View file

@ -383,20 +383,14 @@ void Panel::initWindow() {
&& _fullScreenOrMaximized.current()) {
toggleFullScreen();
}
} else if (e->type() == QEvent::WindowStateChange && _call->rtmp()) {
const auto state = window()->windowState();
_fullScreenOrMaximized = (state & Qt::WindowFullScreen)
|| (state & Qt::WindowMaximized);
}
return base::EventFilterResult::Continue;
});
if (_call->rtmp()) {
QObject::connect(
window()->windowHandle(),
&QWindow::windowStateChanged,
[=](Qt::WindowState state) {
_fullScreenOrMaximized = (state == Qt::WindowFullScreen)
|| (state == Qt::WindowMaximized);
});
}
window()->setBodyTitleArea([=](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag;
const auto titleRect = QRect(

View file

@ -585,7 +585,6 @@ void ChooseSourceProcess::setupSourcesGeometry() {
void ChooseSourceProcess::setupGeometryWithParent(
not_null<QWidget*> parent) {
_window->createWinId();
const auto parentScreen = [&] {
if (const auto screen = QGuiApplication::screenAt(
parent->geometry().center())) {
@ -595,7 +594,12 @@ void ChooseSourceProcess::setupGeometryWithParent(
}();
const auto myScreen = _window->screen();
if (parentScreen && myScreen != parentScreen) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
_window->setScreen(parentScreen);
#else // Qt >= 6.0.0
_window->createWinId();
_window->windowHandle()->setScreen(parentScreen);
#endif // Qt < 6.0.0
}
_window->setFixedSize(_fixedSize);
_window->move(

View file

@ -71,6 +71,7 @@ ComposeIcons {
menuSpoilerOff: icon;
menuBelow: icon;
menuAbove: icon;
menuPrice: icon;
stripBubble: icon;
stripExpandPanel: icon;
@ -610,6 +611,7 @@ defaultComposeIcons: ComposeIcons {
menuSpoilerOff: menuIconSpoilerOff;
menuBelow: menuIconBelow;
menuAbove: menuIconAbove;
menuPrice: menuIconEarn;
stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg },
@ -989,8 +991,33 @@ historyUnreadReactions: TwoIconButton(historyToDown) {
}
historyUnreadThingsSkip: 4px;
historyQuoteStyle: QuoteStyle(defaultQuoteStyle) {
padding: margins(10px, 2px, 4px, 2px);
verticalSkip: 4px;
outline: 3px;
outlineShift: 2px;
radius: 5px;
}
historyTextStyle: TextStyle(defaultTextStyle) {
blockquote: QuoteStyle(historyQuoteStyle) {
padding: margins(10px, 2px, 20px, 2px);
icon: icon{{ "chat/mini_quote", windowFg }};
iconPosition: point(4px, 4px);
expand: icon{{ "intro_country_dropdown", windowFg }};
expandPosition: point(6px, 4px);
collapse: icon{{ "intro_country_dropdown-flip_vertical", windowFg }};
collapsePosition: point(6px, 4px);
}
pre: QuoteStyle(historyQuoteStyle) {
header: 20px;
headerPosition: point(10px, 2px);
scrollable: true;
icon: icon{{ "chat/mini_copy", windowFg }};
iconPosition: point(4px, 2px);
}
}
historyComposeField: InputField(defaultInputField) {
font: normalFont;
style: historyTextStyle;
textMargins: margins(0px, 0px, 0px, 0px);
textAlign: align(left);
textFg: historyComposeAreaFg;
@ -1381,3 +1408,18 @@ editTagField: InputField(defaultInputField) {
editTagLimit: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
paidStarIcon: icon {{ "settings/premium/star", creditsBg1 }};
paidStarIconTop: 7px;
paidAmountAbout: FlatLabel(defaultFlatLabel) {
minWidth: 256px;
textFg: windowSubTextFg;
}
paidTagLabel: FlatLabel(defaultFlatLabel) {
textFg: radialFg;
palette: TextPalette(defaultTextPalette) {
linkFg: creditsBg1;
}
style: semiboldTextStyle;
}
paidTagPadding: margins(16px, 6px, 16px, 6px);

View file

@ -30,15 +30,15 @@ ResolveWindow ResolveWindowDefault() {
return (Window::SessionController*)nullptr;
};
auto &app = Core::App();
const auto account = not_null(&session->account());
if (const auto a = check(app.activeWindow())) {
return a;
} else if (const auto b = check(app.activePrimaryWindow())) {
return b;
} else if (const auto c = check(app.windowFor(&session->account()))) {
} else if (const auto c = check(app.windowFor(account))) {
return c;
} else if (const auto d = check(
app.ensureSeparateWindowForAccount(
&session->account()))) {
} else if (const auto d = check(app.ensureSeparateWindowFor(
account))) {
return d;
}
return nullptr;

View file

@ -14,11 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "base/event_filter.h"
#include "ui/chat/chat_style.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "core/shortcuts.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/ui_integration.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h"
@ -40,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h"
#include <QtCore/QMimeData>
@ -58,6 +61,7 @@ using EditLinkSelection = Ui::InputField::EditLinkSelection;
constexpr auto kParseLinksTimeout = crl::time(500);
constexpr auto kTypesDuration = 4 * crl::time(1000);
constexpr auto kCodeLanguageLimit = 32;
// For mention / custom emoji tags save and validate selfId,
// ignore tags for different users.
@ -222,6 +226,51 @@ void EditLinkBox(
}, text->lifetime());
}
void EditCodeLanguageBox(
not_null<Ui::GenericBox*> box,
QString now,
Fn<void(QString)> save) {
Expects(save != nullptr);
box->setTitle(tr::lng_formatting_code_title());
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_formatting_code_language(),
st::settingsAddReplyLabel));
const auto field = box->addRow(object_ptr<Ui::InputField>(
box,
st::settingsAddReplyField,
tr::lng_formatting_code_auto(),
now.trimmed()));
box->setFocusCallback([=] {
field->setFocusFast();
});
field->selectAll();
field->setMaxLength(kCodeLanguageLimit);
Ui::AddLengthLimitLabel(field, kCodeLanguageLimit);
const auto callback = [=] {
const auto name = field->getLastText().trimmed();
const auto check = QRegularExpression("^[a-zA-Z0-9\\+\\-]*$");
if (check.match(name).hasMatch()) {
auto weak = Ui::MakeWeak(box);
save(name);
if (const auto strong = weak.data()) {
strong->closeBox();
}
} else {
field->showError();
}
};
field->submits(
) | rpl::start_with_next(callback, field->lifetime());
box->addButton(tr::lng_settings_save(), callback);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
TextWithEntities StripSupportHashtag(TextWithEntities text) {
static const auto expression = QRegularExpression(
u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q,
@ -274,15 +323,24 @@ TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
bool EditTextChanged(
not_null<HistoryItem*> item,
const TextWithTags &updated) {
TextWithTags updated) {
const auto original = PrepareEditText(item);
auto originalWithEntities = TextWithEntities{
std::move(original.text),
TextUtilities::ConvertTextTagsToEntities(original.tags)
};
auto updatedWithEntities = TextWithEntities{
std::move(updated.text),
TextUtilities::ConvertTextTagsToEntities(updated.tags)
};
TextUtilities::PrepareForSending(originalWithEntities, 0);
TextUtilities::PrepareForSending(updatedWithEntities, 0);
// Tags can be different for the same entities, because for
// animated emoji each tag contains a different random number.
// So we compare entities instead of tags.
return (original.text != updated.text)
|| (TextUtilities::ConvertTextTagsToEntities(original.tags)
!= TextUtilities::ConvertTextTagsToEntities(updated.tags));
return originalWithEntities != updatedWithEntities;
}
Fn<bool(
@ -320,6 +378,13 @@ Fn<bool(
};
}
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show) {
return [=](QString now, Fn<void(QString)> save) {
show->showBox(Box(EditCodeLanguageBox, now, save));
};
}
void InitMessageFieldHandlers(
not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show,
@ -329,12 +394,16 @@ void InitMessageFieldHandlers(
const style::InputField *fieldStyle) {
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji));
const auto paused = [customEmojiPaused] {
field->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
});
}, [customEmojiPaused] {
return On(PowerSaving::kEmojiChat) || customEmojiPaused();
};
field->setCustomEmojiFactory(
session->data().customEmojiManager().factory(),
std::move(customEmojiPaused));
}, [customEmojiPaused] {
return On(PowerSaving::kChatSpoiler) || customEmojiPaused();
});
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
@ -342,8 +411,18 @@ void InitMessageFieldHandlers(
if (show) {
field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle));
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, fieldStyle != nullptr);
}
const auto style = field->lifetime().make_state<Ui::ChatStyle>(
session->colorIndicesValue());
field->setPreCache([=] {
return style->messageStyle(false, false).preCache.get();
});
field->setBlockquoteCache([=] {
const auto colorIndex = session->user()->colorIndex();
return style->coloredQuoteCache(false, colorIndex).get();
});
}
[[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {
@ -569,12 +648,26 @@ void InitMessageFieldFade(
Ui::DestroyChild(b.data());
}, topFade->lifetime());
topFade->show();
bottomFade->showOn(
field->scrollTop().value(
) | rpl::map([field, descent = field->st().font->descent](int scroll) {
return (scroll + descent) < field->scrollTopMax();
}) | rpl::distinct_until_changed());
const auto descent = field->st().style.font->descent;
rpl::merge(
field->changes(),
field->scrollTop().changes() | rpl::to_empty,
field->sizeValue() | rpl::to_empty
) | rpl::start_with_next([=] {
// InputField::changes fires before the auto-resize is being applied,
// so for the scroll values to be accurate we enqueue the check.
InvokeQueued(field, [=] {
const auto topHidden = !field->scrollTop().current();
if (topFade->isHidden() != topHidden) {
topFade->setVisible(!topHidden);
}
const auto adjusted = field->scrollTop().current() + descent;
const auto bottomHidden = (adjusted >= field->scrollTopMax());
if (bottomFade->isHidden() != bottomHidden) {
bottomFade->setVisible(!bottomHidden);
}
});
}, topFade->lifetime());
}
InlineBotQuery ParseInlineBotQuery(
@ -766,11 +859,7 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
const auto text = static_cast<QKeyEvent*>(event)->text();
if (!text.isEmpty() && text.size() < 3) {
const auto ch = text[0];
if (false
|| ch == '\n'
|| ch == '\r'
|| ch.isSpace()
|| ch == QChar::LineSeparator) {
if (IsSpace(ch)) {
_timer.callOnce(0);
}
}
@ -1097,7 +1186,9 @@ base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
const auto margins = (st.textMargins + st.placeholderMargins);
const auto available = width - margins.left() - margins.right();
label->resizeToWidth(available);
label->moveToLeft(margins.left(), margins.top(), width);
const auto height = label->height() + link->height();
const auto top = (raw->height() - height) / 2;
label->moveToLeft(margins.left(), top, width);
link->move(
(width - link->width()) / 2,
label->y() + label->height());
@ -1117,8 +1208,8 @@ void SelectTextInFieldWithMargins(
auto textCursor = field->textCursor();
// Try to set equal margins for top and bottom sides.
const auto charsCountInLine = field->width()
/ field->st().font->width('W');
const auto linesCount = (field->height() / field->st().font->height);
/ field->st().style.font->width('W');
const auto linesCount = field->height() / field->st().style.font->height;
const auto selectedLines = (selection.to - selection.from)
/ charsCountInLine;
constexpr auto kMinDiff = ushort(3);

View file

@ -35,13 +35,14 @@ class Show;
namespace Ui {
class PopupMenu;
class Show;
} // namespace Ui
[[nodiscard]] QString PrepareMentionTag(not_null<UserData*> user);
[[nodiscard]] TextWithTags PrepareEditText(not_null<HistoryItem*> item);
[[nodiscard]] bool EditTextChanged(
not_null<HistoryItem*> item,
const TextWithTags &updated);
TextWithTags updated);
Fn<bool(
Ui::InputField::EditLinkSelection selection,
@ -51,6 +52,8 @@ Fn<bool(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
const style::InputField *fieldStyle = nullptr);
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show);
void InitMessageFieldHandlers(
not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show, // may be null

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "data/data_abstract_structure.h"
#include "data/data_forum.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
@ -91,6 +92,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/payments_checkout_process.h"
#include "export/export_manager.h"
#include "webrtc/webrtc_environment.h"
#include "window/window_separate_id.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "boxes/abstract_box.h"
@ -118,7 +120,7 @@ constexpr auto kFileOpenTimeoutMs = crl::time(1000);
LaunchState GlobalLaunchState/* = LaunchState::Running*/;
void SetCrashAnnotationsGL() {
#ifdef Q_OS_WIN
#ifdef DESKTOP_APP_USE_ANGLE
CrashReports::SetAnnotation("OpenGL ANGLE", [] {
if (Core::App().settings().disableOpenGL()) {
return "Disabled";
@ -131,11 +133,11 @@ void SetCrashAnnotationsGL() {
}
Unexpected("Ui::GL::CurrentANGLE value in SetupANGLE.");
}());
#else // Q_OS_WIN
#else // DESKTOP_APP_USE_ANGLE
CrashReports::SetAnnotation(
"OpenGL",
Core::App().settings().disableOpenGL() ? "Disabled" : "Enabled");
#endif // Q_OS_WIN
#endif // DESKTOP_APP_USE_ANGLE
}
} // namespace
@ -209,8 +211,7 @@ Application::~Application() {
setLastActiveWindow(nullptr);
_windowInSettings = _lastActivePrimaryWindow = nullptr;
_closingAsyncWindows.clear();
_secondaryWindows.clear();
_primaryWindows.clear();
_windows.clear();
_mediaView = nullptr;
_notifications->clearAllFast();
@ -315,8 +316,8 @@ void Application::run() {
// Create mime database, so it won't be slow later.
QMimeDatabase().mimeTypeForName(u"text/plain"_q);
_primaryWindows.emplace(nullptr, std::make_unique<Window::Controller>());
setLastActiveWindow(_primaryWindows.front().second.get());
_windows.emplace(nullptr, std::make_unique<Window::Controller>());
setLastActiveWindow(_windows.front().second.get());
_windowInSettings = _lastActivePrimaryWindow = _lastActiveWindow;
_domain->activeChanges(
@ -405,7 +406,7 @@ void Application::run() {
}
void Application::showAccount(not_null<Main::Account*> account) {
if (const auto separate = separateWindowForAccount(account)) {
if (const auto separate = separateWindowFor(account)) {
_lastActivePrimaryWindow = separate;
separate->activate();
} else if (const auto last = activePrimaryWindow()) {
@ -413,13 +414,13 @@ void Application::showAccount(not_null<Main::Account*> account) {
}
}
void Application::checkWindowAccount(not_null<Window::Controller*> window) {
const auto account = window->maybeAccount();
for (auto &[key, existing] : _primaryWindows) {
if (existing.get() == window && key != account) {
void Application::checkWindowId(not_null<Window::Controller*> window) {
const auto id = window->id();
for (auto &[existingId, existing] : _windows) {
if (existing.get() == window && existingId != id) {
auto found = std::move(existing);
_primaryWindows.remove(key);
_primaryWindows.emplace(account, std::move(found));
_windows.remove(existingId);
_windows.emplace(id, std::move(found));
break;
}
}
@ -495,10 +496,7 @@ void Application::startSystemDarkModeViewer() {
void Application::enumerateWindows(Fn<void(
not_null<Window::Controller*>)> callback) const {
for (const auto &window : ranges::views::values(_primaryWindows)) {
callback(window.get());
}
for (const auto &window : ranges::views::values(_secondaryWindows)) {
for (const auto &window : ranges::views::values(_windows)) {
callback(window.get());
}
}
@ -607,10 +605,7 @@ void Application::clearEmojiSourceImages() {
}
bool Application::isActiveForTrayMenu() const {
return ranges::any_of(ranges::views::values(_primaryWindows), [=](
const std::unique_ptr<Window::Controller> &controller) {
return controller->widget()->isActiveForTrayMenu();
}) || ranges::any_of(ranges::views::values(_secondaryWindows), [=](
return ranges::any_of(ranges::views::values(_windows), [=](
const std::unique_ptr<Window::Controller> &controller) {
return controller->widget()->isActiveForTrayMenu();
});
@ -1287,44 +1282,36 @@ Window::Controller *Application::activePrimaryWindow() const {
return _lastActivePrimaryWindow;
}
Window::Controller *Application::separateWindowForAccount(
not_null<Main::Account*> account) const {
for (const auto &[openedAccount, window] : _primaryWindows) {
if (openedAccount == account.get()) {
Window::Controller *Application::separateWindowFor(
Window::SeparateId id) const {
for (const auto &[existingId, window] : _windows) {
if (existingId == id) {
return window.get();
}
}
return nullptr;
}
Window::Controller *Application::separateWindowForPeer(
not_null<PeerData*> peer) const {
for (const auto &[history, window] : _secondaryWindows) {
if (history->peer == peer) {
return window.get();
}
}
return nullptr;
}
Window::Controller *Application::ensureSeparateWindowForPeer(
not_null<PeerData*> peer,
Window::Controller *Application::ensureSeparateWindowFor(
Window::SeparateId id,
MsgId showAtMsgId) {
const auto activate = [&](not_null<Window::Controller*> window) {
window->activate();
return window;
};
if (const auto existing = separateWindowForPeer(peer)) {
existing->sessionController()->showPeerHistory(
peer,
Window::SectionShow::Way::ClearStack,
showAtMsgId);
if (const auto existing = separateWindowFor(id)) {
if (id.thread && id.type == Window::SeparateType::Chat) {
existing->sessionController()->showThread(
id.thread,
showAtMsgId,
Window::SectionShow::Way::ClearStack);
}
return activate(existing);
}
const auto result = _secondaryWindows.emplace(
peer->owner().history(peer),
std::make_unique<Window::Controller>(peer, showAtMsgId)
const auto result = _windows.emplace(
id,
std::make_unique<Window::Controller>(id, showAtMsgId)
).first->second.get();
processCreatedWindow(result);
result->firstShow();
@ -1332,55 +1319,63 @@ Window::Controller *Application::ensureSeparateWindowForPeer(
return activate(result);
}
Window::Controller *Application::ensureSeparateWindowForAccount(
not_null<Main::Account*> account) {
const auto activate = [&](not_null<Window::Controller*> window) {
window->activate();
return window;
};
if (const auto existing = separateWindowForAccount(account)) {
return activate(existing);
}
const auto result = _primaryWindows.emplace(
account,
std::make_unique<Window::Controller>(account)
).first->second.get();
processCreatedWindow(result);
result->firstShow();
result->finishFirstShow();
return activate(result);
}
Window::Controller *Application::windowFor(not_null<PeerData*> peer) const {
if (const auto separate = separateWindowForPeer(peer)) {
return separate;
}
return windowFor(&peer->account());
}
Window::Controller *Application::windowFor(
not_null<Main::Account*> account) const {
if (const auto separate = separateWindowForAccount(account)) {
Window::Controller *Application::windowFor(Window::SeparateId id) const {
if (const auto separate = separateWindowFor(id)) {
return separate;
} else if (id && id.primary()) {
return windowFor(not_null(id.account));
}
return activePrimaryWindow();
}
Window::Controller *Application::windowForShowingHistory(
not_null<PeerData*> peer) const {
if (const auto separate = separateWindowFor(peer)) {
return separate;
}
auto result = (Window::Controller*)nullptr;
enumerateWindows([&](not_null<Window::Controller*> window) {
if (const auto controller = window->sessionController()) {
const auto current = controller->activeChatCurrent();
if (const auto history = current.history()) {
if (history->peer == peer) {
result = window;
}
}
}
});
return result;
}
Window::Controller *Application::windowForShowingForum(
not_null<Data::Forum*> forum) const {
const auto id = Window::SeparateId(
Window::SeparateType::Forum,
forum->history());
if (const auto separate = separateWindowFor(id)) {
return separate;
}
auto result = (Window::Controller*)nullptr;
enumerateWindows([&](not_null<Window::Controller*> window) {
if (const auto controller = window->sessionController()) {
const auto current = controller->shownForum().current();
if (forum == current) {
result = window;
}
}
});
return result;
}
Window::Controller *Application::findWindow(
not_null<QWidget*> widget) const {
const auto window = widget->window();
if (_lastActiveWindow && _lastActiveWindow->widget() == window) {
return _lastActiveWindow;
}
for (const auto &[account, primary] : _primaryWindows) {
if (primary->widget() == window) {
return primary.get();
}
}
for (const auto &[history, secondary] : _secondaryWindows) {
if (secondary->widget() == window) {
return secondary.get();
for (const auto &[id, controller] : _windows) {
if (controller->widget() == window) {
return controller.get();
}
}
return nullptr;
@ -1392,10 +1387,11 @@ Window::Controller *Application::activeWindow() const {
bool Application::closeNonLastAsync(not_null<Window::Controller*> window) {
const auto hasOther = [&] {
for (const auto &[account, primary] : _primaryWindows) {
if (!_closingAsyncWindows.contains(primary.get())
&& primary.get() != window
&& primary->maybeSession()) {
for (const auto &[id, controller] : _windows) {
if (id.primary()
&& !_closingAsyncWindows.contains(controller.get())
&& controller.get() != window
&& controller->maybeSession()) {
return true;
}
}
@ -1457,10 +1453,10 @@ void Application::closeWindow(not_null<Window::Controller*> window) {
: nullptr;
const auto next = nextFromStack
? nextFromStack
: (_primaryWindows.front().second.get() != window)
? _primaryWindows.front().second.get()
: (_primaryWindows.back().second.get() != window)
? _primaryWindows.back().second.get()
: (_windows.front().second.get() != window)
? _windows.front().second.get()
: (_windows.back().second.get() != window)
? _windows.back().second.get()
: nullptr;
Assert(next != window);
@ -1481,20 +1477,12 @@ void Application::closeWindow(not_null<Window::Controller*> window) {
}
}
_closingAsyncWindows.remove(window);
for (auto i = begin(_primaryWindows); i != end(_primaryWindows);) {
for (auto i = begin(_windows); i != end(_windows);) {
if (i->second.get() == window) {
Assert(_lastActiveWindow != window);
Assert(_lastActivePrimaryWindow != window);
Assert(_windowInSettings != window);
i = _primaryWindows.erase(i);
} else {
++i;
}
}
for (auto i = begin(_secondaryWindows); i != end(_secondaryWindows);) {
if (i->second.get() == window) {
Assert(_lastActiveWindow != window);
i = _secondaryWindows.erase(i);
i = _windows.erase(i);
} else {
++i;
}
@ -1502,36 +1490,34 @@ void Application::closeWindow(not_null<Window::Controller*> window) {
const auto account = domain().started()
? &domain().active()
: nullptr;
if (account && !_primaryWindows.contains(account) && _lastActiveWindow) {
if (account
&& !_windows.contains(Window::SeparateId(account))
&& _lastActiveWindow) {
domain().activate(&_lastActiveWindow->account());
}
}
void Application::closeChatFromWindows(not_null<PeerData*> peer) {
if (const auto window = windowFor(peer)
; window && !window->isPrimary()) {
closeWindow(window);
}
for (const auto &[history, window] : _secondaryWindows) {
if (const auto session = window->sessionController()) {
if (session->activeChatCurrent().peer() == peer) {
session->showPeerHistory(
window->singlePeer()->id,
Window::SectionShow::Way::ClearStack);
}
}
}
if (const auto window = windowFor(&peer->account())) {
if (const auto primary = window->sessionController()) {
if (primary->activeChatCurrent().peer() == peer) {
primary->clearSectionStack();
}
if (const auto forum = primary->shownForum().current()) {
if (peer->forum() == forum) {
primary->closeForum();
const auto closeOne = [&] {
for (const auto &[id, window] : _windows) {
if (id.thread && id.thread->peer() == peer) {
closeWindow(window.get());
return true;
} else if (const auto controller = window->sessionController()) {
if (controller->activeChatCurrent().peer() == peer) {
controller->showByInitialId();
}
if (const auto forum = controller->shownForum().current()) {
if (peer->forum() == forum) {
controller->closeForum();
}
}
}
}
return false;
};
while (closeOne()) {
}
}
@ -1737,11 +1723,8 @@ void Application::quitPreventFinished() {
}
void Application::quitDelayed() {
for (const auto &[account, window] : _primaryWindows) {
window->widget()->hide();
}
for (const auto &[history, window] : _secondaryWindows) {
window->widget()->hide();
for (const auto &[id, controller] : _windows) {
controller->widget()->hide();
}
if (!_private->quitTimer.isActive()) {
_private->quitTimer.setCallback([] { Sandbox::QuitWhenStarted(); });

View file

@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "mtproto/mtproto_auth_key.h"
#include "mtproto/mtproto_proxy_data.h"
#include "base/timer.h"
#include "window/window_separate_id.h"
class History;
@ -29,11 +30,9 @@ namespace Window {
class Controller;
} // namespace Window
namespace Window {
namespace Notifications {
namespace Window::Notifications {
class System;
} // namespace Notifications
} // namespace Window
} // namespace Window::Notifications
namespace ChatHelpers {
class EmojiKeywords;
@ -170,19 +169,17 @@ public:
not_null<QWidget*> widget) const;
[[nodiscard]] Window::Controller *activeWindow() const;
[[nodiscard]] Window::Controller *activePrimaryWindow() const;
[[nodiscard]] Window::Controller *separateWindowForAccount(
not_null<Main::Account*> account) const;
[[nodiscard]] Window::Controller *separateWindowForPeer(
not_null<PeerData*> peer) const;
Window::Controller *ensureSeparateWindowForPeer(
not_null<PeerData*> peer,
MsgId showAtMsgId);
Window::Controller *ensureSeparateWindowForAccount(
not_null<Main::Account*> account);
[[nodiscard]] Window::Controller *separateWindowFor(
Window::SeparateId id) const;
Window::Controller *ensureSeparateWindowFor(
Window::SeparateId id,
MsgId showAtMsgId = 0);
[[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch.
Window::SeparateId id) const;
[[nodiscard]] Window::Controller *windowForShowingHistory(
not_null<PeerData*> peer) const;
[[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch.
not_null<Main::Account*> account) const;
[[nodiscard]] Window::Controller *windowForShowingForum(
not_null<Data::Forum*> forum) const;
[[nodiscard]] bool closeNonLastAsync(
not_null<Window::Controller*> window);
void closeWindow(not_null<Window::Controller*> window);
@ -195,7 +192,7 @@ public:
void checkSystemDarkMode();
[[nodiscard]] bool isActiveForTrayMenu() const;
void closeChatFromWindows(not_null<PeerData*> peer);
void checkWindowAccount(not_null<Window::Controller*> window);
void checkWindowId(not_null<Window::Controller*> window);
void activate();
// Media view interface.
@ -423,12 +420,9 @@ private:
const std::unique_ptr<Calls::Instance> _calls;
const std::unique_ptr<Iv::Instance> _iv;
base::flat_map<
Main::Account*,
std::unique_ptr<Window::Controller>> _primaryWindows;
Window::SeparateId,
std::unique_ptr<Window::Controller>> _windows;
base::flat_set<not_null<Window::Controller*>> _closingAsyncWindows;
base::flat_map<
not_null<History*>,
std::unique_ptr<Window::Controller>> _secondaryWindows;
std::vector<not_null<Window::Controller*>> _windowStack;
Window::Controller *_lastActiveWindow = nullptr;
Window::Controller *_lastActivePrimaryWindow = nullptr;

View file

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h"
#include "styles/style_calls.h" // groupCallBoxLabel
#include "styles/style_layers.h"
namespace {
@ -121,7 +122,10 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
} else {
const auto parsedUrl = QUrl::fromUserInput(url);
if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) {
Core::App().hideMediaView();
const auto my = context.value<ClickHandlerContext>();
if (!my.show) {
Core::App().hideMediaView();
}
const auto displayed = parsedUrl.isValid()
? parsedUrl.toDisplayString()
: url;
@ -130,7 +134,6 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
: parsedUrl.isValid()
? QString::fromUtf8(parsedUrl.toEncoded())
: ShowEncoded(displayed);
const auto my = context.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
const auto use = controller
? &controller->window()
@ -140,8 +143,11 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
.text = (tr::lng_open_this_link(tr::now)),
.confirmed = [=](Fn<void()> hide) { hide(); open(); },
.confirmText = tr::lng_open_link(),
.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,
});
const auto &st = st::boxLabel;
const auto &st = my.dark
? st::groupCallBoxLabel
: st::boxLabel;
box->addSkip(st.style.lineHeight - st::boxPadding.bottom());
const auto url = box->addRow(
object_ptr<Ui::FlatLabel>(box, displayUrl, st));
@ -190,6 +196,7 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const {
_bot->name()),
.confirmed = callback,
.confirmText = tr::lng_allow_bot(),
.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,
}));
}
}

View file

@ -47,6 +47,7 @@ struct ClickHandlerContext {
bool skipBotAutoLogin = false;
bool botStartAutoSubmit = false;
bool ignoreIv = false;
bool dark = false;
// Is filled from peer info.
PeerData *peer = nullptr;
};

View file

@ -224,7 +224,7 @@ QByteArray Settings::serialize() const {
+ Serialize::bytearraySize(ivPosition)
+ Serialize::stringSize(noWarningExtensions)
+ Serialize::stringSize(_customFontFamily)
+ sizeof(qint32);
+ sizeof(qint32) * 2;
auto result = QByteArray();
result.reserve(size);
@ -375,7 +375,8 @@ QByteArray Settings::serialize() const {
<< qint32(std::clamp(
qRound(_dialogsNoChatWidthRatio.current() * 1000000),
0,
1000000));
1000000))
<< qint32(_systemUnlockEnabled ? 1 : 0);
}
Ensures(result.size() == size);
@ -497,6 +498,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0;
QByteArray ivPosition;
QString customFontFamily = _customFontFamily;
qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
stream >> themesAccentColors;
if (!stream.atEnd()) {
@ -794,6 +796,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
0.,
1.);
}
if (!stream.atEnd()) {
stream >> systemUnlockEnabled;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@ -1002,6 +1007,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_ivPosition = Deserialize(ivPosition);
}
_customFontFamily = customFontFamily;
_systemUnlockEnabled = (systemUnlockEnabled == 1);
}
QString Settings::getSoundPath(const QString &key) const {

View file

@ -890,6 +890,13 @@ public:
_customFontFamily = value;
}
[[nodiscard]] bool systemUnlockEnabled() const {
return _systemUnlockEnabled;
}
void setSystemUnlockEnabled(bool enabled) {
_systemUnlockEnabled = enabled;
}
[[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] static float64 DefaultDialogsWidthRatio();
@ -1020,6 +1027,7 @@ private:
rpl::variable<bool> _ttlVoiceClickTooltipHidden = false;
WindowPosition _ivPosition;
QString _customFontFamily;
bool _systemUnlockEnabled = false;
bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View file

@ -620,7 +620,7 @@ void Sandbox::processPostponedCalls(int level) {
bool Sandbox::nativeEventFilter(
const QByteArray &eventType,
void *message,
base::NativeEventResult *result) {
native_event_filter_result *result) {
registerEnterFromEventLoop();
return false;
}

View file

@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "mtproto/mtproto_proxy_data.h"
#include "base/qt/qt_common_adapters.h"
#include <QtWidgets/QApplication>
#include <QtNetwork/QLocalServer>
@ -87,7 +86,7 @@ private:
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
base::NativeEventResult *result) override;
native_event_filter_result *result) override;
void processPostponedCalls(int level);
void singleInstanceChecked();
void launchApplication();

View file

@ -279,7 +279,7 @@ bool UiIntegration::copyPreOnClick(const QVariant &context) {
}
std::unique_ptr<Ui::Text::CustomEmoji> UiIntegration::createCustomEmoji(
const QString &data,
QStringView data,
const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
if (!my || !my->session) {

View file

@ -58,7 +58,7 @@ public:
const Ui::Emoji::One *defaultEmojiVariant(
const Ui::Emoji::One *emoji) override;
std::unique_ptr<Ui::Text::CustomEmoji> createCustomEmoji(
const QString &data,
QStringView data,
const std::any &context) override;
Fn<void()> createSpoilerRepaint(const std::any &context) override;
bool allowClickHandlerActivation(

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 = 5001007;
constexpr auto AppVersionStr = "5.1.7";
constexpr auto AppVersion = 5002000;
constexpr auto AppVersionStr = "5.2";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -583,6 +583,10 @@ bool ChannelData::canDeleteStories() const {
|| (adminRights() & AdminRight::DeleteStories);
}
bool ChannelData::canPostPaidMedia() const {
return canPostMessages() && (flags() & Flag::PaidMediaAllowed);
}
bool ChannelData::anyoneCanAddMembers() const {
return !(defaultRestrictions() & Restriction::AddParticipants);
}
@ -1084,7 +1088,8 @@ void ApplyChannelUpdate(
| Flag::ParticipantsHidden
| Flag::CanGetStatistics
| Flag::ViewAsMessages
| Flag::CanViewRevenue;
| Flag::CanViewRevenue
| Flag::PaidMediaAllowed;
channel->setFlags((channel->flags() & ~mask)
| (update.is_can_set_username() ? Flag::CanSetUsername : Flag())
| (update.is_can_view_participants()
@ -1101,6 +1106,7 @@ void ApplyChannelUpdate(
| (update.is_view_forum_as_messages()
? Flag::ViewAsMessages
: Flag())
| (update.is_paid_media_allowed() ? Flag::PaidMediaAllowed : Flag())
| (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag()));
channel->setUserpicPhoto(update.vchat_photo());
if (const auto migratedFrom = update.vmigrated_from_chat_id()) {

View file

@ -66,6 +66,7 @@ enum class ChannelDataFlag : uint64 {
ViewAsMessages = (1ULL << 30),
SimilarExpanded = (1ULL << 31),
CanViewRevenue = (1ULL << 32),
PaidMediaAllowed = (1ULL << 33),
};
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>;
@ -357,6 +358,7 @@ public:
[[nodiscard]] bool canPostStories() const;
[[nodiscard]] bool canEditStories() const;
[[nodiscard]] bool canDeleteStories() const;
[[nodiscard]] bool canPostPaidMedia() const;
[[nodiscard]] bool hiddenPreHistory() const;
[[nodiscard]] bool canViewMembers() const;
[[nodiscard]] bool canViewAdmins() const;

View file

@ -170,6 +170,12 @@ void UpdateCloudFile(
if (data.progressivePartSize && !file.location.valid()) {
file.progressivePartSize = data.progressivePartSize;
}
if (data.location.width()
&& data.location.height()
&& !file.location.valid()
&& !file.location.width()) {
file.location = data.location;
}
return;
}

View file

@ -19,6 +19,16 @@ struct CreditTopupOption final {
using CreditTopupOptions = std::vector<CreditTopupOption>;
enum class CreditsHistoryMediaType {
Photo,
Video,
};
struct CreditsHistoryMedia {
CreditsHistoryMediaType type = CreditsHistoryMediaType::Photo;
uint64 id = 0;
};
struct CreditsHistoryEntry final {
using PhotoId = uint64;
enum class PeerType {
@ -28,16 +38,26 @@ struct CreditsHistoryEntry final {
Fragment,
Unsupported,
PremiumBot,
Ads,
};
QString id;
QString title;
QString description;
QDateTime date;
PhotoId photoId = 0;
std::vector<CreditsHistoryMedia> extended;
uint64 credits = 0;
uint64 bareId = 0;
uint64 bareMsgId = 0;
uint64 barePeerId = 0;
PeerType peerType;
bool refunded = false;
bool pending = false;
bool failed = false;
QDateTime successDate;
QString successLink;
bool in = false;
};
struct CreditsStatusSlice final {

View file

@ -0,0 +1,32 @@
/*
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"
#include <QtCore/QDateTime>
namespace Data {
using CreditsEarnInt = uint64;
struct CreditsEarnStatistics final {
explicit operator bool() const {
return !!usdRate;
}
Data::StatisticalGraph revenueGraph;
CreditsEarnInt currentBalance = 0;
CreditsEarnInt availableBalance = 0;
CreditsEarnInt overallRevenue = 0;
float64 usdRate = 0.;
bool isWithdrawalEnabled = false;
QDateTime nextWithdrawalAt;
QString buyAdsUrl;
};
} // namespace Data

View file

@ -531,7 +531,7 @@ void DownloadManager::loadingStopWithConfirmation(
return;
}
const auto window = Core::App().windowFor(
&item->history()->session().account());
not_null(&item->history()->session().account()));
if (!window) {
return;
}

View file

@ -76,6 +76,12 @@ struct FileReferenceAccumulator {
}, [](const auto &data) {
});
}
void push(const MTPMessageExtendedMedia &data) {
data.match([&](const MTPDmessageExtendedMediaPreview &data) {
}, [&](const MTPDmessageExtendedMedia &data) {
push(data.vmedia());
});
}
void push(const MTPMessageMedia &data) {
data.match([&](const MTPDmessageMediaPhoto &data) {
push(data.vphoto());
@ -85,6 +91,10 @@ struct FileReferenceAccumulator {
push(data.vwebpage());
}, [&](const MTPDmessageMediaGame &data) {
push(data.vgame());
}, [&](const MTPDmessageMediaInvoice &data) {
push(data.vextended_media());
}, [&](const MTPDmessageMediaPaidMedia &data) {
push(data.vextended_media());
}, [](const auto &data) {
});
}

View file

@ -81,6 +81,7 @@ void Groups::refreshMessage(
bool justRefreshViews) {
if (!isGrouped(item)) {
unregisterMessage(item);
_data->requestItemViewRefresh(item);
return;
}
if (!item->isRegular() && !item->isScheduled()) {

View file

@ -7,12 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_media_types.h"
#include "base/random.h"
#include "boxes/send_credits_box.h" // CreditsEmoji.
#include "history/history.h"
#include "history/history_item.h" // CreateMedia.
#include "history/history_location_manager.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_item_preview.h"
#include "history/view/media/history_view_extended_preview.h"
#include "history/view/media/history_view_photo.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_gif.h"
@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_giveaway.h"
#include "history/view/media/history_view_invoice.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_media_grouped.h"
#include "history/view/media/history_view_call.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/media/history_view_poll.h"
@ -84,6 +86,13 @@ constexpr auto kLoadingStoryPhotoId = PhotoId(0x7FFF'DEAD'FFFF'FFFFULL);
using ItemPreview = HistoryView::ItemPreview;
using ItemPreviewImage = HistoryView::ItemPreviewImage;
struct AlbumCounts {
int photos = 0;
int videos = 0;
int audios = 0;
int files = 0;
};
[[nodiscard]] TextWithEntities WithCaptionNotificationText(
const QString &attachType,
const TextWithEntities &caption,
@ -165,7 +174,7 @@ template <typename MediaType>
return (reinterpret_cast<uint64>(data.get()) & ~1) | (spoiler ? 1 : 0);
}
[[nodiscard]] ItemPreviewImage PreparePhotoPreview(
[[nodiscard]] ItemPreviewImage PreparePhotoPreviewImage(
not_null<const HistoryItem*> item,
const std::shared_ptr<PhotoMedia> &media,
ImageRoundRadius radius,
@ -181,14 +190,15 @@ template <typename MediaType>
}
const auto allowedToDownload = media->autoLoadThumbnailAllowed(
item->history()->peer);
const auto cacheKey = allowedToDownload ? 0 : counted;
const auto spoilered = uint64(spoiler ? 1 : 0);
const auto cacheKey = allowedToDownload ? spoilered : counted;
if (allowedToDownload) {
media->owner()->load(PhotoSize::Small, item->fullId());
}
if (const auto blurred = media->thumbnailInline()) {
return { PreparePreviewImage(blurred, radius, spoiler), cacheKey };
}
return { QImage(), allowedToDownload ? 0 : cacheKey };
return { QImage(), allowedToDownload ? spoilered : cacheKey };
}
[[nodiscard]] ItemPreviewImage PrepareFilePreviewImage(
@ -207,10 +217,11 @@ template <typename MediaType>
};
}
document->loadThumbnail(item->fullId());
const auto spoilered = uint64(spoiler ? 1 : 0);
if (const auto blurred = media->thumbnailInline()) {
return { PreparePreviewImage(blurred, radius, spoiler), 0 };
return { PreparePreviewImage(blurred, radius, spoiler), spoilered };
}
return { QImage(), 0 };
return { QImage(), spoilered };
}
[[nodiscard]] QImage PutPlayIcon(QImage preview) {
@ -225,6 +236,18 @@ template <typename MediaType>
return preview;
}
[[nodiscard]] ItemPreviewImage PreparePhotoPreview(
not_null<const HistoryItem*> item,
const std::shared_ptr<PhotoMedia> &media,
ImageRoundRadius radius,
bool spoiler) {
auto result = PreparePhotoPreviewImage(item, media, radius, spoiler);
if (media->owner()->extendedMediaVideoDuration().has_value()) {
result.data = PutPlayIcon(std::move(result.data));
}
return result;
}
[[nodiscard]] ItemPreviewImage PrepareFilePreview(
not_null<const HistoryItem*> item,
const std::shared_ptr<DocumentMedia> &media,
@ -261,48 +284,82 @@ template <typename MediaType>
}
bool UpdateExtendedMedia(
Invoice &invoice,
std::unique_ptr<Media> &media,
not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) {
return media.match([&](const MTPDmessageExtendedMediaPreview &data) {
if (invoice.extendedMedia) {
return false;
const MTPMessageExtendedMedia &extended) {
return extended.match([&](const MTPDmessageExtendedMediaPreview &data) {
auto photo = (PhotoData*)nullptr;
if (!media) {
const auto id = base::RandomValue<PhotoId>();
photo = item->history()->owner().photo(id);
} else {
photo = media->photo();
if (!photo || !photo->extendedMediaPreview()) {
return false;
}
}
auto changed = false;
auto &preview = invoice.extendedPreview;
auto size = QSize();
auto thumbnail = QByteArray();
auto videoDuration = std::optional<TimeId>();
if (const auto &w = data.vw()) {
const auto &h = data.vh();
Assert(h.has_value());
const auto dimensions = QSize(w->v, h->v);
if (preview.dimensions != dimensions) {
preview.dimensions = dimensions;
size = QSize(w->v, h->v);
if (!changed && photo->size(PhotoSize::Large) != size) {
changed = true;
}
}
if (const auto &thumb = data.vthumb()) {
if (thumb->type() == mtpc_photoStrippedSize) {
const auto bytes = thumb->c_photoStrippedSize().vbytes().v;
if (preview.inlineThumbnailBytes != bytes) {
preview.inlineThumbnailBytes = bytes;
thumbnail = thumb->c_photoStrippedSize().vbytes().v;
if (!changed && photo->inlineThumbnailBytes() != thumbnail) {
changed = true;
}
}
}
if (const auto &duration = data.vvideo_duration()) {
if (preview.videoDuration != duration->v) {
preview.videoDuration = duration->v;
videoDuration = duration->v;
if (photo->extendedMediaVideoDuration() != videoDuration) {
changed = true;
}
} else if (photo->extendedMediaVideoDuration().has_value()) {
changed = true;
}
if (changed) {
photo->setExtendedMediaPreview(size, thumbnail, videoDuration);
}
if (!media) {
media = std::make_unique<MediaPhoto>(item, photo, true);
}
return changed;
}, [&](const MTPDmessageExtendedMedia &data) {
invoice.extendedMedia = HistoryItem::CreateMedia(
item,
data.vmedia());
media = HistoryItem::CreateMedia(item, data.vmedia());
return true;
});
}
bool UpdateExtendedMedia(
Invoice &invoice,
not_null<HistoryItem*> item,
const QVector<MTPMessageExtendedMedia> &media) {
auto changed = false;
const auto count = int(media.size());
for (auto i = 0; i != count; ++i) {
if (i <= invoice.extendedMedia.size()) {
invoice.extendedMedia.emplace_back();
changed = true;
}
UpdateExtendedMedia(invoice.extendedMedia[i], item, media[i]);
}
if (count < invoice.extendedMedia.size()) {
invoice.extendedMedia.resize(count);
changed = true;
}
return changed;
}
TextForMimeData WithCaptionClipboardText(
const QString &attachType,
TextForMimeData &&caption) {
@ -322,6 +379,29 @@ TextForMimeData WithCaptionClipboardText(
return result;
}
[[nodiscard]] QString ComputeAlbumCountsString(AlbumCounts counts) {
const auto medias = counts.photos + counts.videos;
return (counts.photos && counts.videos)
? tr::lng_in_dlg_media_count(tr::now, lt_count, medias)
: (counts.photos > 1)
? tr::lng_in_dlg_photo_count(tr::now, lt_count, counts.photos)
: counts.photos
? tr::lng_in_dlg_photo(tr::now)
: (counts.videos > 1)
? tr::lng_in_dlg_video_count(tr::now, lt_count, counts.videos)
: counts.videos
? tr::lng_in_dlg_video(tr::now)
: (counts.audios > 1)
? tr::lng_in_dlg_audio_count(tr::now, lt_count, counts.audios)
: counts.audios
? tr::lng_in_dlg_audio(tr::now)
: (counts.files > 1)
? tr::lng_in_dlg_file_count(tr::now, lt_count, counts.files)
: counts.files
? tr::lng_in_dlg_file(tr::now)
: tr::lng_in_dlg_album(tr::now);
}
} // namespace
Invoice ComputeInvoiceData(
@ -344,11 +424,23 @@ Invoice ComputeInvoiceData(
.isTest = data.is_test(),
};
if (const auto &media = data.vextended_media()) {
UpdateExtendedMedia(result, item, *media);
UpdateExtendedMedia(result, item, { *media });
}
return result;
}
Invoice ComputeInvoiceData(
not_null<HistoryItem*> item,
const MTPDmessageMediaPaidMedia &data) {
auto result = Invoice{
.amount = data.vstars_amount().v,
.currency = Ui::kCreditsCurrency,
.isPaidMedia = true,
};
UpdateExtendedMedia(result, item, data.vextended_media().v);
return result;
}
Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
auto result = Call();
result.finishReason = [&] {
@ -424,6 +516,27 @@ GiveawayResults ComputeGiveawayResultsData(
return result;
}
bool HasExtendedMedia(const Invoice &invoice) {
return !invoice.extendedMedia.empty();
}
bool HasUnpaidMedia(const Invoice &invoice) {
for (const auto &media : invoice.extendedMedia) {
const auto photo = media->photo();
return photo && photo->extendedMediaPreview();
}
return false;
}
bool IsFirstVideo(const Invoice &invoice) {
if (invoice.extendedMedia.empty()) {
return false;
} else if (const auto photo = invoice.extendedMedia.front()->photo()) {
return photo->extendedMediaVideoDuration().has_value();
}
return true;
}
Media::Media(not_null<HistoryItem*> parent) : _parent(parent) {
}
@ -586,21 +699,18 @@ ItemPreview Media::toGroupPreview(
ToPreviewOptions options) const {
auto result = ItemPreview();
auto loadingContext = std::vector<std::any>();
auto photoCount = 0;
auto videoCount = 0;
auto audioCount = 0;
auto fileCount = 0;
auto counts = AlbumCounts();
auto manyCaptions = false;
for (const auto &item : items) {
if (const auto media = item->media()) {
if (media->photo()) {
photoCount++;
counts.photos++;
} else if (const auto document = media->document()) {
(document->isVideoFile()
? videoCount
? counts.videos
: document->isAudioFile()
? audioCount
: fileCount)++;
? counts.audios
: counts.files)++;
}
auto copy = options;
copy.ignoreGroup = true;
@ -630,19 +740,7 @@ ItemPreview Media::toGroupPreview(
}
}
if (manyCaptions || result.text.text.isEmpty()) {
const auto mediaCount = photoCount + videoCount;
auto genericText = (photoCount && videoCount)
? tr::lng_in_dlg_media_count(tr::now, lt_count, mediaCount)
: photoCount
? tr::lng_in_dlg_photo_count(tr::now, lt_count, photoCount)
: videoCount
? tr::lng_in_dlg_video_count(tr::now, lt_count, videoCount)
: audioCount
? tr::lng_in_dlg_audio_count(tr::now, lt_count, audioCount)
: fileCount
? tr::lng_in_dlg_file_count(tr::now, lt_count, fileCount)
: tr::lng_in_dlg_album(tr::now);
result.text = Ui::Text::Colorized(genericText);
result.text = Ui::Text::Colorized(ComputeAlbumCountsString(counts));
}
if (!loadingContext.empty()) {
result.loadingContext = std::move(loadingContext);
@ -1851,14 +1949,15 @@ MediaInvoice::MediaInvoice(
.currency = data.currency,
.title = data.title,
.description = data.description,
.extendedPreview = data.extendedPreview,
.extendedMedia = (data.extendedMedia
? data.extendedMedia->clone(parent)
: nullptr),
.photo = data.photo,
.isPaidMedia = data.isPaidMedia,
.isTest = data.isTest,
} {
if (_invoice.extendedPreview && !_invoice.extendedMedia) {
_invoice.extendedMedia.reserve(data.extendedMedia.size());
for (auto &item : data.extendedMedia) {
_invoice.extendedMedia.push_back(item->clone(parent));
}
if (HasUnpaidMedia(_invoice)) {
Ui::PreloadImageSpoiler();
}
}
@ -1894,9 +1993,89 @@ bool MediaInvoice::replyPreviewLoaded() const {
}
TextWithEntities MediaInvoice::notificationText() const {
if (_invoice.isPaidMedia && !_invoice.extendedMedia.empty()) {
return WithCaptionNotificationText(
(IsFirstVideo(_invoice)
? tr::lng_in_dlg_video
: tr::lng_in_dlg_photo)(tr::now),
parent()->originalText());
}
return { .text = _invoice.title };
}
ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const {
if (!_invoice.isPaidMedia || _invoice.extendedMedia.empty()) {
return Media::toPreview(options);
}
auto counts = AlbumCounts();
auto images = std::vector<ItemPreviewImage>();
auto context = std::vector<std::any>();
const auto existing = options.existing;
const auto spoiler = HasUnpaidMedia(_invoice);
for (const auto &media : _invoice.extendedMedia) {
const auto raw = media.get();
const auto photo = raw->photo();
const auto document = raw->document();
if (!photo && !document) {
continue;
} else if (images.size() < kMaxPreviewImages) {
auto found = photo
? FindCachedPreview(existing, not_null(photo), spoiler)
: FindCachedPreview(existing, not_null(document), spoiler);
const auto radius = ImageRoundRadius::Small;
if (found) {
images.push_back(std::move(found));
} else if (photo) {
const auto media = photo->createMediaView();
if (auto prepared = PreparePhotoPreview(
parent(),
media,
radius,
spoiler)
; prepared || !prepared.cacheKey) {
images.push_back(std::move(prepared));
if (!prepared.cacheKey) {
context.push_back(media);
}
}
} else if (TryFilePreview(document)) {
const auto media = document->createMediaView();
if (auto prepared = PrepareFilePreview(
parent(),
media,
radius,
spoiler)
; prepared || !prepared.cacheKey) {
images.push_back(std::move(prepared));
if (!prepared.cacheKey) {
context.push_back(media);
}
}
}
}
if (photo && !photo->extendedMediaVideoDuration().has_value()) {
++counts.photos;
} else {
++counts.videos;
}
}
const auto type = ComputeAlbumCountsString(counts);
const auto caption = (options.hideCaption || options.ignoreMessageText)
? TextWithEntities()
: options.translated
? parent()->translatedText()
: parent()->originalText();
const auto hasMiniImages = !images.empty();
auto nice = Ui::Text::Colorized(
Ui::CreditsEmojiSmall(&parent()->history()->session()));
nice.append(WithCaptionNotificationText(type, caption, hasMiniImages));
return {
.text = std::move(nice),
.images = std::move(images),
.loadingContext = std::move(context),
};
}
QString MediaInvoice::pinnedTextSubstring() const {
return QString::fromUtf8("\xC2\xAB")
+ _invoice.title
@ -1917,7 +2096,7 @@ bool MediaInvoice::updateSentMedia(const MTPMessageMedia &media) {
bool MediaInvoice::updateExtendedMedia(
not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) {
const QVector<MTPMessageExtendedMedia> &media) {
Expects(item == parent());
return UpdateExtendedMedia(_invoice, item, media);
@ -1927,15 +2106,15 @@ std::unique_ptr<HistoryView::Media> MediaInvoice::createView(
not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent,
HistoryView::Element *replacing) {
if (_invoice.extendedMedia) {
return _invoice.extendedMedia->createView(
if (_invoice.extendedMedia.size() == 1) {
return _invoice.extendedMedia.front()->createView(
message,
realParent,
replacing);
} else if (_invoice.extendedPreview) {
return std::make_unique<HistoryView::ExtendedPreview>(
} else if (!_invoice.extendedMedia.empty()) {
return std::make_unique<HistoryView::GroupedMedia>(
message,
&_invoice);
_invoice.extendedMedia);
}
return std::make_unique<HistoryView::Invoice>(message, &_invoice);
}

View file

@ -84,19 +84,6 @@ struct Call {
};
struct ExtendedPreview {
QByteArray inlineThumbnailBytes;
QSize dimensions;
TimeId videoDuration = -1;
[[nodiscard]] bool empty() const {
return dimensions.isEmpty();
}
explicit operator bool() const {
return !empty();
}
};
class Media;
struct Invoice {
@ -105,11 +92,14 @@ struct Invoice {
QString currency;
QString title;
TextWithEntities description;
ExtendedPreview extendedPreview;
std::unique_ptr<Media> extendedMedia;
std::vector<std::unique_ptr<Media>> extendedMedia;
PhotoData *photo = nullptr;
bool isPaidMedia = false;
bool isTest = false;
};
[[nodiscard]] bool HasExtendedMedia(const Invoice &invoice);
[[nodiscard]] bool HasUnpaidMedia(const Invoice &invoice);
[[nodiscard]] bool IsFirstVideo(const Invoice &invoice);
struct GiveawayStart {
std::vector<not_null<ChannelData*>> channels;
@ -207,7 +197,7 @@ public:
virtual bool updateSentMedia(const MTPMessageMedia &media) = 0;
virtual bool updateExtendedMedia(
not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) {
const QVector<MTPMessageExtendedMedia> &media) {
return false;
}
virtual std::unique_ptr<HistoryView::Media> createView(
@ -517,6 +507,7 @@ public:
Image *replyPreview() const override;
bool replyPreviewLoaded() const override;
TextWithEntities notificationText() const override;
ItemPreview toPreview(ToPreviewOptions way) const override;
QString pinnedTextSubstring() const override;
TextForMimeData clipboardText() const override;
@ -524,7 +515,7 @@ public:
bool updateSentMedia(const MTPMessageMedia &media) override;
bool updateExtendedMedia(
not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) override;
const QVector<MTPMessageExtendedMedia> &media) override;
std::unique_ptr<HistoryView::Media> createView(
not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent,
@ -750,6 +741,9 @@ private:
[[nodiscard]] Invoice ComputeInvoiceData(
not_null<HistoryItem*> item,
const MTPDmessageMediaInvoice &data);
[[nodiscard]] Invoice ComputeInvoiceData(
not_null<HistoryItem*> item,
const MTPDmessageMediaPaidMedia &data);
[[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call);

View file

@ -50,6 +50,38 @@ PhotoData::~PhotoData() {
base::take(_videoSizes);
}
void PhotoData::setFields(TimeId date, bool hasAttachedStickers) {
_dateOrExtendedVideoDuration = date;
_hasStickers = hasAttachedStickers;
_extendedMediaPreview = false;
}
void PhotoData::setExtendedMediaPreview(
QSize dimensions,
const QByteArray &inlineThumbnailBytes,
std::optional<TimeId> videoDuration) {
_extendedMediaPreview = true;
updateImages(
inlineThumbnailBytes,
{},
{},
{ .location = { {}, dimensions.width(), dimensions.height() } },
{},
{},
{});
_dateOrExtendedVideoDuration = videoDuration ? (*videoDuration + 1) : 0;
}
bool PhotoData::extendedMediaPreview() const {
return _extendedMediaPreview;
}
std::optional<TimeId> PhotoData::extendedMediaVideoDuration() const {
return (_extendedMediaPreview && _dateOrExtendedVideoDuration)
? TimeId(_dateOrExtendedVideoDuration - 1)
: std::optional<TimeId>();
}
Data::Session &PhotoData::owner() const {
return *_owner;
}
@ -74,6 +106,10 @@ void PhotoData::load(
load(PhotoSize::Large, origin, fromCloud, autoLoading);
}
TimeId PhotoData::date() const {
return _extendedMediaPreview ? 0 : _dateOrExtendedVideoDuration;
}
bool PhotoData::loading() const {
return loading(PhotoSize::Large);
}

View file

@ -53,6 +53,7 @@ public:
void automaticLoadSettingsChanged();
[[nodiscard]] TimeId date() const;
[[nodiscard]] bool loading() const;
[[nodiscard]] bool displayLoading() const;
void cancel();
@ -89,6 +90,14 @@ public:
[[nodiscard]] auto activeMediaView() const
-> std::shared_ptr<Data::PhotoMedia>;
void setFields(TimeId date, bool hasAttachedStickers);
void setExtendedMediaPreview(
QSize dimensions,
const QByteArray &inlineThumbnailBytes,
std::optional<TimeId> videoDuration);
[[nodiscard]] bool extendedMediaPreview() const;
[[nodiscard]] std::optional<TimeId> extendedMediaVideoDuration() const;
void updateImages(
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
@ -148,11 +157,10 @@ public:
void setHasAttachedStickers(bool value);
// For now they return size of the 'large' image.
int width() const;
int height() const;
[[nodiscard]] int width() const;
[[nodiscard]] int height() const;
PhotoId id = 0;
TimeId date = 0;
PeerData *peer = nullptr; // for chat and channel photos connection
// geo, caption
@ -168,6 +176,8 @@ private:
[[nodiscard]] const Data::CloudFile &videoFile(
Data::PhotoSize size) const;
TimeId _dateOrExtendedVideoDuration = 0;
struct VideoSizes {
Data::CloudFile small;
Data::CloudFile large;
@ -181,6 +191,8 @@ private:
int32 _dc = 0;
uint64 _access = 0;
bool _hasStickers = false;
bool _extendedMediaPreview = false;
QByteArray _fileReference;
std::unique_ptr<Data::ReplyPreview> _replyPreview;
std::weak_ptr<Data::PhotoMedia> _media;

View file

@ -3139,8 +3139,7 @@ void Session::photoApplyFields(
return;
}
photo->setRemoteLocation(dc, access, fileReference);
photo->date = date;
photo->setHasAttachedStickers(hasStickers);
photo->setFields(date, hasStickers);
photo->updateImages(
inlineThumbnailBytes,
small,
@ -4748,16 +4747,16 @@ uint64 Session::wallpapersHash() const {
return _wallpapersHash;
}
MTP::DcId Session::statsDcId(not_null<ChannelData*> channel) {
const auto it = _channelStatsDcIds.find(channel);
return (it == end(_channelStatsDcIds)) ? MTP::DcId(0) : it->second;
MTP::DcId Session::statsDcId(not_null<PeerData*> peer) {
const auto it = _peerStatsDcIds.find(peer);
return (it == end(_peerStatsDcIds)) ? MTP::DcId(0) : it->second;
}
void Session::applyStatsDcId(
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
MTP::DcId dcId) {
if (dcId != channel->session().mainDcId()) {
_channelStatsDcIds[channel] = dcId;
if (dcId != peer->session().mainDcId()) {
_peerStatsDcIds[peer] = dcId;
}
}

View file

@ -753,8 +753,8 @@ public:
[[nodiscard]] auto peerDecorationsUpdated() const
-> rpl::producer<not_null<PeerData*>>;
void applyStatsDcId(not_null<ChannelData*>, MTP::DcId);
[[nodiscard]] MTP::DcId statsDcId(not_null<ChannelData*>);
void applyStatsDcId(not_null<PeerData*>, MTP::DcId);
[[nodiscard]] MTP::DcId statsDcId(not_null<PeerData*>);
void viewTagsChanged(
not_null<ViewElement*> view,
@ -1053,7 +1053,7 @@ private:
base::flat_map<not_null<UserData*>, TimeId> _watchingForOffline;
base::Timer _watchForOfflineTimer;
base::flat_map<not_null<ChannelData*>, MTP::DcId> _channelStatsDcIds;
base::flat_map<not_null<PeerData*>, MTP::DcId> _peerStatsDcIds;
rpl::event_stream<WebViewResultSent> _webViewResultSent;

View file

@ -11,6 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
enum class StatisticalCurrency {
None,
Ton,
Credits,
};
struct StatisticalChart {
StatisticalChart() = default;
@ -67,6 +73,8 @@ struct StatisticalChart {
bool isFooterHidden = false;
bool hasPercentages = false;
bool weekFormat = false;
StatisticalCurrency currency = StatisticalCurrency::None;
float64 currencyRate = 0.;
// View data.

View file

@ -82,6 +82,7 @@ using UpdateFlag = StoryUpdate::Flag;
});
}, [&](const MTPDmediaAreaSuggestedReaction &data) {
}, [&](const MTPDmediaAreaChannelPost &data) {
}, [&](const MTPDmediaAreaUrl &data) {
}, [&](const MTPDinputMediaAreaChannelPost &data) {
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
}, [&](const MTPDinputMediaAreaVenue &data) {
@ -103,6 +104,7 @@ using UpdateFlag = StoryUpdate::Flag;
.dark = data.is_dark(),
});
}, [&](const MTPDmediaAreaChannelPost &data) {
}, [&](const MTPDmediaAreaUrl &data) {
}, [&](const MTPDinputMediaAreaChannelPost &data) {
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
}, [&](const MTPDinputMediaAreaVenue &data) {
@ -124,6 +126,27 @@ using UpdateFlag = StoryUpdate::Flag;
peerFromChannel(data.vchannel_id()),
data.vmsg_id().v),
});
}, [&](const MTPDmediaAreaUrl &data) {
}, [&](const MTPDinputMediaAreaChannelPost &data) {
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
}, [&](const MTPDinputMediaAreaVenue &data) {
LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
});
return result;
}
[[nodiscard]] auto ParseUrlArea(const MTPMediaArea &area)
-> std::optional<UrlArea> {
auto result = std::optional<UrlArea>();
area.match([&](const MTPDmediaAreaVenue &data) {
}, [&](const MTPDmediaAreaGeoPoint &data) {
}, [&](const MTPDmediaAreaSuggestedReaction &data) {
}, [&](const MTPDmediaAreaChannelPost &data) {
}, [&](const MTPDmediaAreaUrl &data) {
result.emplace(UrlArea{
.area = ParseArea(data.vcoordinates()),
.url = qs(data.vurl()),
});
}, [&](const MTPDinputMediaAreaChannelPost &data) {
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
}, [&](const MTPDinputMediaAreaVenue &data) {
@ -662,6 +685,10 @@ const std::vector<ChannelPost> &Story::channelPosts() const {
return _channelPosts;
}
const std::vector<UrlArea> &Story::urlAreas() const {
return _urlAreas;
}
void Story::applyChanges(
StoryMedia media,
const MTPDstoryItem &data,
@ -765,6 +792,7 @@ void Story::applyFields(
auto locations = std::vector<StoryLocation>();
auto suggestedReactions = std::vector<SuggestedReaction>();
auto channelPosts = std::vector<ChannelPost>();
auto urlAreas = std::vector<UrlArea>();
if (const auto areas = data.vmedia_areas()) {
for (const auto &area : areas->v) {
if (const auto location = ParseLocation(area)) {
@ -778,6 +806,8 @@ void Story::applyFields(
suggestedReactions.push_back(*reaction);
} else if (auto post = ParseChannelPost(area)) {
channelPosts.push_back(*post);
} else if (auto url = ParseUrlArea(area)) {
urlAreas.push_back(*url);
}
}
}
@ -790,6 +820,7 @@ void Story::applyFields(
const auto suggestedReactionsChanged
= (_suggestedReactions != suggestedReactions);
const auto channelPostsChanged = (_channelPosts != channelPosts);
const auto urlAreasChanged = (_urlAreas != urlAreas);
const auto reactionChanged = (_sentReactionId != reaction);
_out = out;
@ -815,6 +846,9 @@ void Story::applyFields(
if (channelPostsChanged) {
_channelPosts = std::move(channelPosts);
}
if (urlAreasChanged) {
_urlAreas = std::move(urlAreas);
}
if (reactionChanged) {
_sentReactionId = reaction;
}
@ -824,7 +858,8 @@ void Story::applyFields(
|| captionChanged
|| mediaChanged
|| locationsChanged
|| channelPostsChanged;
|| channelPostsChanged
|| urlAreasChanged;
const auto reactionsChanged = reactionChanged
|| suggestedReactionsChanged;
if (!initial && (changed || reactionsChanged)) {

View file

@ -122,6 +122,15 @@ struct ChannelPost {
const ChannelPost &) = default;
};
struct UrlArea {
StoryArea area;
QString url;
friend inline bool operator==(
const UrlArea &,
const UrlArea &) = default;
};
class Story final {
public:
Story(
@ -197,6 +206,8 @@ public:
-> const std::vector<SuggestedReaction> &;
[[nodiscard]] auto channelPosts() const
-> const std::vector<ChannelPost> &;
[[nodiscard]] auto urlAreas() const
-> const std::vector<UrlArea> &;
void applyChanges(
StoryMedia media,
@ -247,6 +258,7 @@ private:
std::vector<StoryLocation> _locations;
std::vector<SuggestedReaction> _suggestedReactions;
std::vector<ChannelPost> _channelPosts;
std::vector<UrlArea> _urlAreas;
StoryViews _views;
StoryViews _channelReactions;
const TimeId _date = 0;

View file

@ -285,7 +285,7 @@ dialogsFilter: InputField(defaultInputField) {
borderRadius: 18px;
borderDenominator: 2;
font: normalFont;
style: defaultTextStyle;
heightMin: 35px;
}

View file

@ -2588,6 +2588,15 @@ void InnerWidget::dragPinnedFromTouch() {
updateReorderPinned(now);
}
void InnerWidget::searchRequested(bool loading) {
_searchWaiting = false;
_searchLoading = loading;
if (loading) {
clearSearchResults(true);
}
refresh(true);
}
void InnerWidget::applySearchState(SearchState state) {
if (_searchState == state) {
return;
@ -2646,7 +2655,7 @@ void InnerWidget::applySearchState(SearchState state) {
onHashtagFilterUpdate(QStringView());
}
_searchState = std::move(state);
_searchingHashtag = IsHashtagSearchQuery(_searchState.query);
_searchHashOrCashtag = IsHashOrCashtagSearchQuery(_searchState.query);
updateSearchIn();
moveSearchIn();
@ -2700,9 +2709,13 @@ void InnerWidget::applySearchState(SearchState state) {
clearMouseSelection(true);
}
if (_state != WidgetState::Default) {
_searchLoading = true;
_searchMessages.fire({});
refresh(true);
_searchWaiting = true;
_searchRequests.fire(otherChanged
? SearchRequestDelay::Instant
: SearchRequestDelay::Delayed);
if (_searchWaiting) {
refresh(true);
}
}
}
@ -2918,8 +2931,8 @@ rpl::producer<Ui::ScrollToRequest> InnerWidget::dialogMoved() const {
return _dialogMoved.events();
}
rpl::producer<> InnerWidget::searchMessages() const {
return _searchMessages.events();
rpl::producer<SearchRequestDelay> InnerWidget::searchRequests() const {
return _searchRequests.events();
}
rpl::producer<QString> InnerWidget::completeHashtagRequests() const {
@ -3007,6 +3020,7 @@ void InnerWidget::searchReceived(
HistoryItem *inject,
SearchRequestType type,
int fullCount) {
_searchWaiting = false;
_searchLoading = false;
const auto uniquePeers = uniqueSearchResults();
@ -3171,7 +3185,7 @@ void InnerWidget::refreshEmpty() {
&& _searchResults.empty()
&& _peerSearchResults.empty()
&& _hashtagResults.empty();
if (_searchLoading || !empty) {
if (_searchLoading || _searchWaiting || !empty) {
if (_searchEmpty) {
_searchEmpty->hide();
}
@ -3185,7 +3199,7 @@ void InnerWidget::refreshEmpty() {
_searchEmpty->show();
}
if (!_searchLoading || !empty) {
if ((!_searchLoading && !_searchWaiting) || !empty) {
_loadingAnimation.destroy();
} else if (!_loadingAnimation) {
_loadingAnimation = Ui::CreateLoadingDialogRowWidget(
@ -3318,7 +3332,8 @@ auto InnerWidget::searchTagsChanges() const
}
void InnerWidget::updateSearchIn() {
if (!_searchState.inChat && !_searchingHashtag) {
if (!_searchState.inChat
&& _searchHashOrCashtag == HashOrCashtag::None) {
_searchIn = nullptr;
return;
} else if (!_searchIn) {
@ -3367,7 +3382,7 @@ void InnerWidget::updateSearchIn() {
? Ui::MakeUserpicThumbnail(sublist->peer())
: nullptr;
const auto myIcon = Ui::MakeIconThumbnail(st::menuIconChats);
const auto publicIcon = _searchingHashtag
const auto publicIcon = (_searchHashOrCashtag != HashOrCashtag::None)
? Ui::MakeIconThumbnail(st::menuIconChannel)
: nullptr;
const auto peerTabType = (peer && peer->isBroadcast())
@ -3661,7 +3676,7 @@ void InnerWidget::preloadRowsData() {
}
}
bool InnerWidget::chooseCollapsedRow() {
bool InnerWidget::chooseCollapsedRow(Qt::KeyboardModifiers modifiers) {
if (_state != WidgetState::Default) {
return false;
} else if ((_collapsedSelected < 0)
@ -3754,7 +3769,15 @@ bool InnerWidget::chooseHashtag() {
ChosenRow InnerWidget::computeChosenRow() const {
if (_state == WidgetState::Default) {
if (_selected) {
if ((_collapsedSelected >= 0)
&& (_collapsedSelected < _collapsedRows.size())) {
const auto &row = _collapsedRows[_collapsedSelected];
Assert(row->folder != nullptr);
return {
.key = row->folder,
.message = Data::UnreadMessagePosition,
};
} else if (_selected) {
return {
.key = _selected->key(),
.message = Data::UnreadMessagePosition,
@ -3798,9 +3821,7 @@ bool InnerWidget::isUserpicPressOnWide() const {
bool InnerWidget::chooseRow(
Qt::KeyboardModifiers modifiers,
MsgId pressedTopicRootId) {
if (chooseCollapsedRow()) {
return true;
} else if (chooseHashtag()) {
if (chooseHashtag()) {
return true;
}
const auto modifyChosenRow = [&](

View file

@ -62,6 +62,7 @@ class IndexedList;
class SearchTags;
class SearchEmpty;
class ChatSearchIn;
enum class HashOrCashtag : uchar;
struct ChosenRow {
Key key;
@ -71,7 +72,7 @@ struct ChosenRow {
bool newWindow : 1 = false;
};
enum class SearchRequestType {
enum class SearchRequestType : uchar {
FromStart,
FromOffset,
PeerFromStart,
@ -80,6 +81,12 @@ enum class SearchRequestType {
MigratedFromOffset,
};
enum class SearchRequestDelay : uchar {
InCache,
Instant,
Delayed,
};
enum class WidgetState {
Default,
Filtered,
@ -145,6 +152,7 @@ public:
}
[[nodiscard]] bool hasFilteredResults() const;
void searchRequested(bool loading);
void applySearchState(SearchState state);
[[nodiscard]] auto searchTagsChanges() const
-> rpl::producer<std::vector<Data::ReactionId>>;
@ -168,7 +176,7 @@ public:
[[nodiscard]] rpl::producer<int> scrollByDeltaRequests() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> dialogMoved() const;
[[nodiscard]] rpl::producer<> searchMessages() const;
[[nodiscard]] rpl::producer<SearchRequestDelay> searchRequests() const;
[[nodiscard]] rpl::producer<QString> completeHashtagRequests() const;
[[nodiscard]] rpl::producer<> refreshHashtagsRequests() const;
@ -244,7 +252,7 @@ private:
void repaintCollapsedFolderRow(not_null<Data::Folder*> folder);
void refreshWithCollapsedRows(bool toTop = false);
bool needCollapsedRowsRefresh() const;
bool chooseCollapsedRow();
bool chooseCollapsedRow(Qt::KeyboardModifiers modifiers);
void switchToFilter(FilterId filterId);
bool chooseHashtag();
ChosenRow computeChosenRow() const;
@ -507,7 +515,7 @@ private:
Ui::DraggingScrollManager _draggingScroll;
SearchState _searchState;
bool _searchingHashtag = false;
HashOrCashtag _searchHashOrCashtag = {};
History *_searchInMigrated = nullptr;
PeerData *_searchFromShown = nullptr;
Ui::Text::String _searchFromUserText;
@ -529,7 +537,7 @@ private:
rpl::event_stream<Ui::ScrollToRequest> _mustScrollTo;
rpl::event_stream<Ui::ScrollToRequest> _dialogMoved;
rpl::event_stream<> _searchMessages;
rpl::event_stream<SearchRequestDelay> _searchRequests;
rpl::event_stream<QString> _completeHashtagRequests;
rpl::event_stream<> _refreshHashtagsRequests;
@ -547,6 +555,7 @@ private:
bool _savedSublists = false;
bool _searchLoading = false;
bool _searchWaiting = false;
base::unique_qptr<Ui::PopupMenu> _menu;

View file

@ -99,6 +99,7 @@ namespace {
constexpr auto kSearchPerPage = 50;
constexpr auto kStoriesExpandDuration = crl::time(200);
constexpr auto kSearchRequestDelay = crl::time(900);
base::options::toggle OptionForumHideChatsList({
.id = kOptionForumHideChatsList,
@ -328,9 +329,9 @@ Widget::Widget(
_scroll->scrollToY(st + _inner->st()->height);
}
}, lifetime());
_inner->searchMessages(
) | rpl::start_with_next([=] {
searchRequested();
_inner->searchRequests(
) | rpl::start_with_next([=](SearchRequestDelay delay) {
searchRequested(delay);
}, lifetime());
_inner->completeHashtagRequests(
) | rpl::start_with_next([=](const QString &tag) {
@ -565,6 +566,8 @@ void Widget::chosenRow(const ChosenRow &row) {
if (topicJump) {
if (controller()->shownForum().current() == topicJump->forum()) {
controller()->closeForum();
} else if (row.newWindow) {
controller()->showInNewWindow(Window::SeparateId(topicJump));
} else {
if (!controller()->adaptive().isOneColumn()) {
controller()->showForum(
@ -578,11 +581,17 @@ void Widget::chosenRow(const ChosenRow &row) {
}
return;
} else if (const auto topic = row.key.topic()) {
session().data().saveViewAsMessages(topic->forum(), false);
controller()->showThread(
topic,
row.message.fullId.msg,
Window::SectionShow::Way::ClearStack);
if (row.newWindow) {
controller()->showInNewWindow(
Window::SeparateId(topic),
row.message.fullId.msg);
} else {
session().data().saveViewAsMessages(topic->forum(), false);
controller()->showThread(
topic,
row.message.fullId.msg,
Window::SectionShow::Way::ClearStack);
}
} else if (history
&& row.userpicClick
&& (row.message.fullId.msg == ShowAtUnreadMsgId)
@ -599,16 +608,19 @@ void Widget::chosenRow(const ChosenRow &row) {
const auto forum = history->peer->forum();
if (controller()->shownForum().current() == forum) {
controller()->closeForum();
return;
}
controller()->showForum(
forum,
Window::SectionShow().withChildColumn());
if (forum->channel()->viewForumAsMessages()) {
controller()->showThread(
history,
ShowAtUnreadMsgId,
Window::SectionShow::Way::ClearStack);
} else if (row.newWindow) {
controller()->showInNewWindow(
Window::SeparateId(Window::SeparateType::Forum, history));
} else {
controller()->showForum(
forum,
Window::SectionShow().withChildColumn());
if (forum->channel()->viewForumAsMessages()) {
controller()->showThread(
history,
ShowAtUnreadMsgId,
Window::SectionShow::Way::ClearStack);
}
}
return;
} else if (history) {
@ -634,6 +646,12 @@ void Widget::chosenRow(const ChosenRow &row) {
return;
}
}
if (row.newWindow) {
controller()->showInNewWindow(Window::SeparateId(
Window::SeparateType::Archive,
&session()));
return;
}
controller()->openFolder(folder);
hideChildList();
}
@ -912,13 +930,15 @@ void Widget::setupStories() {
Core::App().settings().setStoriesClickTooltipHidden(true);
Core::App().saveSettingsDelayed();
};
_stories->setShowTooltip(
parentWidget(),
rpl::combine(
Core::App().settings().storiesClickTooltipHiddenValue(),
shownValue(),
!rpl::mappers::_1 && rpl::mappers::_2),
hideTooltip);
InvokeQueued(_stories.get(), [=] {
_stories->setShowTooltip(
controller()->content(),
rpl::combine(
Core::App().settings().storiesClickTooltipHiddenValue(),
shownValue(),
!rpl::mappers::_1 && rpl::mappers::_2),
hideTooltip);
});
}
_storiesContents.fire(Stories::ContentForSession(
@ -1868,13 +1888,21 @@ void Widget::slideFinished() {
void Widget::escape() {
if (!cancelSearch({ .jumpBackToSearchedChat = true })) {
if (controller()->shownForum().current()) {
controller()->closeForum();
if (const auto forum = controller()->shownForum().current()) {
const auto id = controller()->windowId();
const auto initial = id.forum();
if (!initial) {
controller()->closeForum();
} else if (initial != forum) {
controller()->showForum(initial);
}
} else if (controller()->openedFolder().current()) {
controller()->closeFolder();
if (!controller()->windowId().folder()) {
controller()->closeFolder();
}
} else if (controller()->activeChatEntryCurrent().key) {
controller()->content()->dialogsCancelled();
} else {
} else if (controller()->isPrimary()) {
const auto filters = &session().data().chatsFilters();
const auto &list = filters->list();
const auto first = list.empty() ? FilterId() : list.front().id();
@ -1945,7 +1973,7 @@ void Widget::loadMoreBlockedByDate() {
session().api().requestMoreBlockedByDateDialogs();
}
bool Widget::search(bool inCache) {
bool Widget::search(bool inCache, SearchRequestDelay delay) {
_processingSearch = true;
const auto guard = gsl::finally([&] {
_processingSearch = false;
@ -1974,7 +2002,7 @@ bool Widget::search(bool inCache) {
return true;
} else if (inCache) {
const auto success = _singleMessageSearch.lookup(query, [=] {
searchRequested();
searchRequested(delay);
});
if (!success) {
return false;
@ -2092,6 +2120,9 @@ bool Widget::search(bool inCache) {
}).send();
_searchQueries.emplace(_searchRequest, _searchQuery);
}
_inner->searchRequested(true);
} else {
_inner->searchRequested(false);
}
const auto peerQuery = Api::ConvertPeerSearchQuery(query);
if (searchForPeersRequired(peerQuery)) {
@ -2143,20 +2174,25 @@ bool Widget::searchForPeersRequired(const QString &query) const {
return _searchState.filterChatsList()
&& !_openedForum
&& !query.isEmpty()
&& !IsHashtagSearchQuery(query);
&& (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None);
}
bool Widget::searchForTopicsRequired(const QString &query) const {
return _searchState.filterChatsList()
&& _openedForum
&& !query.isEmpty()
&& !IsHashtagSearchQuery(query)
&& (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None)
&& !_openedForum->topicsList()->loaded();
}
void Widget::searchRequested() {
if (!search(true)) {
_searchTimer.callOnce(AutoSearchTimeout);
void Widget::searchRequested(SearchRequestDelay delay) {
if (search(true, delay)) {
return;
} else if (delay == SearchRequestDelay::Instant) {
_searchTimer.cancel();
search();
} else {
_searchTimer.callOnce(kSearchRequestDelay);
}
}
@ -2211,10 +2247,11 @@ void Widget::searchTopics() {
}
void Widget::searchMore() {
if (_searchRequest || _searchInHistoryRequest) {
if (_searchRequest
|| _searchInHistoryRequest
|| _searchTimer.isActive()) {
return;
}
if (!_searchFull) {
} else if (!_searchFull) {
if (const auto peer = searchInPeer()) {
auto &histories = session().data().histories();
const auto topic = searchInTopic();
@ -2666,16 +2703,19 @@ void Widget::updateCancelSearch() {
QString Widget::validateSearchQuery() {
const auto query = currentSearchQuery();
if (_searchState.tab == ChatSearchTab::PublicPosts) {
_searchingHashtag = true;
if (_searchHashOrCashtag == HashOrCashtag::None) {
_searchHashOrCashtag = HashOrCashtag::Hashtag;
}
const auto fixed = FixHashtagSearchQuery(
query,
currentSearchQueryCursorPosition());
currentSearchQueryCursorPosition(),
_searchHashOrCashtag);
if (fixed.text != query) {
setSearchQuery(fixed.text, fixed.cursorPosition);
}
return fixed.text;
} else {
_searchingHashtag = IsHashtagSearchQuery(query);
_searchHashOrCashtag = IsHashOrCashtagSearchQuery(query);
}
return query;
}
@ -2713,6 +2753,9 @@ void Widget::updateForceDisplayWide() {
void Widget::showForum(
not_null<Data::Forum*> forum,
const Window::SectionShow &params) {
if (_openedForum == forum) {
return;
}
const auto nochat = !controller()->mainSectionShown();
if (!params.childColumn
|| (Core::App().settings().dialogsWidthRatio(nochat) == 0.)
@ -2872,11 +2915,12 @@ bool Widget::applySearchState(SearchState state) {
state.fromPeer = nullptr;
}
if (state.tab == ChatSearchTab::PublicPosts
&& !IsHashtagSearchQuery(state.query)) {
&& IsHashOrCashtagSearchQuery(state.query) == HashOrCashtag::None) {
state.tab = (_openedForum && !state.inChat)
? ChatSearchTab::ThisPeer
: ChatSearchTab::MyMessages;
} else if (!state.inChat && !_searchingHashtag) {
} else if (!state.inChat
&& _searchHashOrCashtag == HashOrCashtag::None) {
state.tab = (forum || _openedForum)
? ChatSearchTab::ThisPeer
: ChatSearchTab::MyMessages;
@ -2916,7 +2960,7 @@ bool Widget::applySearchState(SearchState state) {
&& !state.inChat
&& !_openedForum)
|| (state.tab == ChatSearchTab::PublicPosts
&& !_searchingHashtag)) {
&& _searchHashOrCashtag == HashOrCashtag::None)) {
state.tab = state.inChat.topic()
? ChatSearchTab::ThisTopic
: (state.inChat.owningHistory() || state.inChat.sublist())
@ -3648,7 +3692,11 @@ bool Widget::cancelSearch(CancelSearchOptions options) {
_inner->clearFilter();
applySearchState(std::move(updatedState));
if (_suggestions && clearSearchFocus) {
const auto clearLockedFocus = !_searchHasFocus;
setInnerFocus(true);
if (clearLockedFocus) {
processSearchFocusChange();
}
}
updateForceDisplayWide();
return clearingQuery || clearingInChat || clearSearchFocus;

View file

@ -57,6 +57,7 @@ namespace Window {
class SessionController;
class ConnectionState;
struct SectionShow;
struct SeparateId;
} // namespace Window
namespace Dialogs::Stories {
@ -74,10 +75,12 @@ class FakeRow;
class Key;
struct ChosenRow;
class InnerWidget;
enum class SearchRequestType;
enum class SearchRequestType : uchar;
enum class SearchRequestDelay : uchar;
class Suggestions;
class ChatSearchIn;
enum class ChatSearchTab : uchar;
enum class HashOrCashtag : uchar;
class Widget final : public Window::AbstractSectionWidget {
public:
@ -156,8 +159,8 @@ private:
[[nodiscard]] QString currentSearchQuery() const;
[[nodiscard]] int currentSearchQueryCursorPosition() const;
void clearSearchField();
void searchRequested();
bool search(bool inCache = false);
void searchRequested(SearchRequestDelay delay);
bool search(bool inCache = false, SearchRequestDelay after = {});
void searchTopics();
void searchMore();
@ -313,7 +316,7 @@ private:
object_ptr<Ui::JumpDownButton> _scrollToTop;
bool _scrollToTopIsShown = false;
bool _forumSearchRequested = false;
bool _searchingHashtag = false;
HashOrCashtag _searchHashOrCashtag = {};
Data::Folder *_openedFolder = nullptr;
Data::Forum *_openedForum = nullptr;

View file

@ -199,12 +199,14 @@ void Action::handleKeyPress(not_null<QKeyEvent*> e) {
FixedHashtagSearchQuery FixHashtagSearchQuery(
const QString &query,
int cursorPosition) {
int cursorPosition,
HashOrCashtag tag) {
const auto trimmed = query.trimmed();
const auto hash = int(trimmed.isEmpty()
? query.size()
: query.indexOf(trimmed));
const auto start = std::min(cursorPosition, hash);
const auto first = QChar(tag == HashOrCashtag::Cashtag ? '$' : '#');
auto result = query.mid(0, start);
for (const auto &ch : query.mid(start)) {
if (ch.isSpace()) {
@ -213,33 +215,41 @@ FixedHashtagSearchQuery FixHashtagSearchQuery(
}
continue;
} else if (result.size() == start) {
result += '#';
if (ch != '#') {
result += first;
if (ch != first) {
++cursorPosition;
}
}
if (ch != '#') {
if (ch != first) {
result += ch;
}
}
if (result.size() == start) {
result += '#';
result += first;
++cursorPosition;
}
return { result, cursorPosition };
}
bool IsHashtagSearchQuery(const QString &query) {
HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query) {
const auto trimmed = query.trimmed();
if (trimmed.isEmpty() || trimmed[0] != '#') {
return false;
}
for (const auto &ch : trimmed) {
if (ch.isSpace()) {
return false;
const auto first = trimmed.isEmpty() ? QChar() : trimmed[0];
if (first == '#') {
for (const auto &ch : trimmed) {
if (ch.isSpace()) {
return HashOrCashtag::None;
}
}
return HashOrCashtag::Hashtag;
} else if (first == '$') {
for (auto it = trimmed.begin() + 1; it != trimmed.end(); ++it) {
if ((*it) < 'A' || (*it) > 'Z') {
return HashOrCashtag::None;
}
}
return HashOrCashtag::Cashtag;
}
return true;
return HashOrCashtag::None;
}
void ChatSearchIn::Section::update() {

View file

@ -87,14 +87,21 @@ private:
};
enum class HashOrCashtag : uchar {
None,
Hashtag,
Cashtag,
};
struct FixedHashtagSearchQuery {
QString text;
int cursorPosition = 0;
};
[[nodiscard]] FixedHashtagSearchQuery FixHashtagSearchQuery(
const QString &query,
int cursorPosition);
int cursorPosition,
HashOrCashtag tag);
[[nodiscard]] bool IsHashtagSearchQuery(const QString &query);
[[nodiscard]] HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query);
} // namespace Dialogs

View file

@ -903,7 +903,7 @@ TextWithEntities List::computeTooltipText() const {
}
void List::setShowTooltip(
not_null<QWidget*> tooltipParent,
not_null<Ui::RpWidget*> tooltipParent,
rpl::producer<bool> shown,
Fn<void()> hide) {
_tooltip = nullptr;
@ -925,16 +925,6 @@ void List::setShowTooltip(
tooltip->toggleFast(false);
updateTooltipGeometry();
const auto handle = tooltipParent->window()->windowHandle();
auto windowActive = rpl::single(
handle->isActive()
) | rpl::then(base::qt_signal_producer(
handle,
&QWindow::activeChanged
) | rpl::map([=] {
return handle->isActive();
})) | rpl::distinct_until_changed();
{
const auto recompute = [=] {
updateTooltipGeometry();
@ -955,7 +945,7 @@ void List::setShowTooltip(
_tooltipText.value() | rpl::map(
notEmpty
) | rpl::distinct_until_changed(),
std::move(windowActive)
tooltipParent->windowActiveValue()
) | rpl::start_with_next([=](bool, bool, bool active) {
_tooltipWindowActive = active;
if (!isHidden()) {
@ -981,7 +971,7 @@ void List::toggleTooltip(bool fast) {
&& !isHidden()
&& _tooltipNotHidden.current()
&& !_tooltipText.current().empty()
&& window()->windowHandle()->isActive();
&& isActiveWindow();
if (_tooltip) {
if (fast) {
_tooltip->toggleFast(shown);

View file

@ -72,7 +72,7 @@ public:
style::align alignSmall,
QRect geometryFull = QRect());
void setShowTooltip(
not_null<QWidget*> tooltipParent,
not_null<Ui::RpWidget*> tooltipParent,
rpl::producer<bool> shown,
Fn<void()> hide);
void raiseTooltip();

View file

@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/dynamic_thumbnails.h"
#include "ui/painter.h"
#include "ui/unread_badge_paint.h"
#include "window/window_separate_id.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "styles/style_chat.h"

View file

@ -666,6 +666,28 @@ Invoice ParseInvoice(const MTPDmessageMediaInvoice &data) {
return result;
}
PaidMedia ParsePaidMedia(
ParseMediaContext &context,
const MTPDmessageMediaPaidMedia &data,
const QString &folder,
TimeId date) {
auto result = PaidMedia();
result.stars = data.vstars_amount().v;
result.extended.reserve(data.vextended_media().v.size());
for (const auto &extended : data.vextended_media().v) {
result.extended.push_back(extended.match([](
const MTPDmessageExtendedMediaPreview &)
-> std::unique_ptr<Media> {
return std::unique_ptr<Media>();
}, [&](const MTPDmessageExtendedMedia &data)
-> std::unique_ptr<Media> {
return std::make_unique<Media>(
ParseMedia(context, data.vmedia(), folder, date));
}));
}
return result;
}
Poll ParsePoll(const MTPDmessageMediaPoll &data) {
auto result = Poll();
data.vpoll().match([&](const MTPDpoll &poll) {
@ -1225,6 +1247,8 @@ Media ParseMedia(
result.content = ParseGiveaway(data);
}, [&](const MTPDmessageMediaGiveawayResults &data) {
// #TODO export giveaway
}, [&](const MTPDmessageMediaPaidMedia &data) {
result.content = ParsePaidMedia(context, data, folder, date);
}, [](const MTPDmessageMediaEmpty &data) {});
return result;
}

View file

@ -182,6 +182,18 @@ struct Invoice {
int32 receiptMsgId = 0;
};
struct Media;
struct PaidMedia {
PaidMedia() = default;
PaidMedia(PaidMedia &&) = default;
PaidMedia &operator=(PaidMedia &&) = default;
PaidMedia(const PaidMedia &) = delete;
PaidMedia &operator=(const PaidMedia &) = delete;
uint64 stars = 0;
std::vector<std::unique_ptr<Media>> extended;
};
struct Poll {
struct Answer {
Utf8String text;
@ -337,6 +349,7 @@ struct Media {
Invoice,
Poll,
GiveawayStart,
PaidMedia,
UnsupportedMedia> content;
TimeId ttl = 0;

View file

@ -2092,6 +2092,9 @@ MediaData HtmlWriter::Wrap::prepareMediaData(
result.status = Data::FormatMoneyAmount(data.amount, data.currency);
}, [](const Poll &data) {
}, [](const GiveawayStart &data) {
}, [&](const PaidMedia &data) {
result.classes = "media_invoice";
result.status = Data::FormatMoneyAmount(data.stars, "XTR");
}, [](const UnsupportedMedia &data) {
Unexpected("Unsupported message.");
}, [](v::null_t) {});

View file

@ -779,6 +779,8 @@ QByteArray SerializeMessage(
{ "until_date", SerializeDate(data.untilDate) },
{ "channels", serialized },
}));
}, [&](const PaidMedia &data) {
push("paid_stars_amount", data.stars);
}, [](const UnsupportedMedia &data) {
Unexpected("Unsupported message.");
}, [](v::null_t) {});

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