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/target_prepare_qrc.cmake)
include(cmake/options.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) include(cmake/external/qt/package.cmake)
set(desktop_app_skip_libs set(desktop_app_skip_libs

View file

@ -195,6 +195,7 @@ PRIVATE
api/api_earn.h api/api_earn.h
api/api_editing.cpp api/api_editing.cpp
api/api_editing.h api/api_editing.h
api/api_filter_updates.h
api/api_global_privacy.cpp api/api_global_privacy.cpp
api/api_global_privacy.h api/api_global_privacy.h
api/api_hash.cpp api/api_hash.cpp
@ -233,6 +234,10 @@ PRIVATE
api/api_single_message_search.h api/api_single_message_search.h
api/api_statistics.cpp api/api_statistics.cpp
api/api_statistics.h 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.cpp
api/api_text_entities.h api/api_text_entities.h
api/api_toggling_media.cpp api/api_toggling_media.cpp
@ -791,8 +796,6 @@ PRIVATE
history/view/media/history_view_dice.h history/view/media/history_view_dice.h
history/view/media/history_view_document.cpp history/view/media/history_view_document.cpp
history/view/media/history_view_document.h 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.cpp
history/view/media/history_view_file.h history/view/media/history_view_file.h
history/view/media/history_view_game.cpp history/view/media/history_view_game.cpp
@ -956,6 +959,10 @@ PRIVATE
history/history_view_highlight_manager.h history/history_view_highlight_manager.h
history/history_widget.cpp history/history_widget.cpp
history/history_widget.h 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.cpp
info/channel_statistics/boosts/create_giveaway_box.h info/channel_statistics/boosts/create_giveaway_box.h
info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp
@ -1608,6 +1615,8 @@ PRIVATE
window/window_peer_menu.cpp window/window_peer_menu.cpp
window/window_peer_menu.h window/window_peer_menu.h
window/window_section_common.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.cpp
window/window_session_controller.h window/window_session_controller.h
window/window_session_controller_link_info.h window/window_session_controller_link_info.h
@ -1899,12 +1908,44 @@ if (WIN32)
/DELAYLOAD:uxtheme.dll /DELAYLOAD:uxtheme.dll
/DELAYLOAD:crypt32.dll /DELAYLOAD:crypt32.dll
/DELAYLOAD:bcrypt.dll /DELAYLOAD:bcrypt.dll
/DELAYLOAD:imm32.dll
/DELAYLOAD:netapi32.dll /DELAYLOAD:netapi32.dll
/DELAYLOAD:imm32.dll
/DELAYLOAD:userenv.dll /DELAYLOAD:userenv.dll
/DELAYLOAD:wtsapi32.dll /DELAYLOAD:wtsapi32.dll
/DELAYLOAD:propsys.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() endif()
target_prepare_qrc(Telegram) 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_privacy_premium_link" = "Telegram Premium";
"lng_settings_passcode_disable" = "Disable Passcode"; "lng_settings_passcode_disable" = "Disable Passcode";
"lng_settings_passcode_disable_sure" = "Are you sure you want to 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_disable" = "Disable Cloud Password";
"lng_settings_password_abort" = "Abort two-step verification setup"; "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"; "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_confirm" = "Proceed";
"lng_settings_gift_premium_users_error#one" = "You can select maximum {count} user."; "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_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_backgrounds_header" = "Choose Wallpaper";
"lng_theme_sure_keep" = "Keep this theme?"; "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_ph" = "Your passcode";
"lng_passcode_submit" = "Submit"; "lng_passcode_submit" = "Submit";
"lng_passcode_logout" = "Log out"; "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_create_button" = "Save Passcode";
"lng_passcode_check_button" = "Submit"; "lng_passcode_check_button" = "Submit";
"lng_passcode_change_button" = "Save Passcode"; "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_link" = "Public Link";
"lng_manage_peer_bot_public_links" = "Public Links"; "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_intro" = "Edit Intro";
"lng_manage_peer_bot_edit_commands" = "Edit Commands"; "lng_manage_peer_bot_edit_commands" = "Edit Commands";
"lng_manage_peer_bot_edit_settings" = "Change Bot Settings"; "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_about_translation" = "Real-time translation of channels and chats into other languages.";
"lng_premium_summary_subtitle_business" = "Telegram Business"; "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_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_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_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"; "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_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#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_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#one" = "Confirm and Pay {emoji} {count} Star";
"lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars"; "lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars";
"lng_credits_box_out_about" = "Review the {link} for 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_title" = "Stars Acquired";
"lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance."; "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_summary_in_toast_about#other" = "**{count}** Stars added to your balance.";
"lng_credits_box_history_entry_peer" = "Recipient"; "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" = "Transaction ID";
"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard."; "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" = "You can dispute this transaction {link}.";
"lng_credits_box_history_entry_about_link" = "here"; "lng_credits_box_history_entry_about_link" = "here";
"lng_credits_small_balance_title#one" = "{count} Star Needed"; "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_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove 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_title" = "Fact Check";
"lng_factcheck_placeholder" = "Add Facts or Context"; "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_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_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_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to" = "Translate to {name}";
"lng_translate_bar_to_other" = "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_text" = "Text";
"lng_formatting_link_url" = "URL"; "lng_formatting_link_url" = "URL";
"lng_formatting_link_create" = "Create"; "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_text_copied" = "Text copied to clipboard.";
"lng_code_copied" = "Block 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_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_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_voice_messages" = "{user} restricted sending of voice messages to them.";
"lng_restricted_send_video_messages" = "{user} restricted sending of video 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" = "Refund";
"lng_channel_earn_history_return_about" = "Refunded back"; "lng_channel_earn_history_return_about" = "Refunded back";
"lng_channel_earn_history_pending" = "Pending"; "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#one" = "Show {count} More Transaction";
"lng_channel_earn_history_show_more#other" = "Show {count} More Transactions"; "lng_channel_earn_history_show_more#other" = "Show {count} More Transactions";
"lng_channel_earn_off" = "Switch Off Ads"; "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_revenue" = "Ad revenue";
"lng_channel_earn_chart_overriden_detail_currency" = "Revenue in TON"; "lng_channel_earn_chart_overriden_detail_currency" = "Revenue in TON";
"lng_channel_earn_chart_overriden_detail_usd" = "Revenue in USD"; "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_add" = "Add";
"lng_contact_send_message" = "message"; "lng_contact_send_message" = "message";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.1.7.0" /> Version="5.2.0.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,1,7,0 FILEVERSION 5,2,0,0
PRODUCTVERSION 5,1,7,0 PRODUCTVERSION 5,2,0,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.1.7.0" VALUE "FileVersion", "5.2.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.1.7.0" VALUE "ProductVersion", "5.2.0.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

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

View file

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

View file

@ -7,9 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "api/api_credits.h" #include "api/api_credits.h"
#include "apiwrap.h" #include "api/api_statistics_data_deserialize.h"
#include "api/api_updates.h" #include "api/api_updates.h"
#include "apiwrap.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -20,25 +23,62 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Api { namespace Api {
namespace { namespace {
constexpr auto kTransactionsLimit = 100;
[[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL( [[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL(
const MTPStarsTransaction &tl, const MTPStarsTransaction &tl,
not_null<PeerData*> peer) { not_null<PeerData*> peer) {
using HistoryPeerTL = MTPDstarsTransactionPeer; using HistoryPeerTL = MTPDstarsTransactionPeer;
using namespace Data;
const auto owner = &peer->owner();
const auto photo = tl.data().vphoto() const auto photo = tl.data().vphoto()
? peer->owner().photoFromWeb(*tl.data().vphoto(), ImageLocation()) ? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())
: nullptr; : 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{ return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()), .id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()), .title = qs(tl.data().vtitle().value_or_empty()),
.description = qs(tl.data().vdescription().value_or_empty()), .description = qs(tl.data().vdescription().value_or_empty()),
.date = base::unixtime::parse(tl.data().vdate().v), .date = base::unixtime::parse(tl.data().vdate().v),
.photoId = photo ? photo->id : 0, .photoId = photo ? photo->id : 0,
.extended = std::move(extended),
.credits = tl.data().vstars().v, .credits = tl.data().vstars().v,
.bareId = tl.data().vpeer().match([](const HistoryPeerTL &p) { .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),
return peerFromMTP(p.vpeer()); .barePeerId = barePeerId,
}, [](const auto &) {
return PeerId(0);
}).value,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer; return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) { }, [](const MTPDstarsTransactionPeerPlayMarket &) {
@ -51,8 +91,17 @@ namespace {
return Data::CreditsHistoryEntry::PeerType::Unsupported; return Data::CreditsHistoryEntry::PeerType::Unsupported;
}, [](const MTPDstarsTransactionPeerPremiumBot &) { }, [](const MTPDstarsTransactionPeerPremiumBot &) {
return Data::CreditsHistoryEntry::PeerType::PremiumBot; return Data::CreditsHistoryEntry::PeerType::PremiumBot;
}, [](const MTPDstarsTransactionPeerAds &) {
return Data::CreditsHistoryEntry::PeerType::Ads;
}), }),
.refunded = tl.data().is_refund(), .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( _requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags), MTP_flags(_flags),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input, _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token) MTP_string(token),
MTP_int(kTransactionsLimit)
)).done([=](const MTPpayments_StarsStatus &result) { )).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0; _requestId = 0;
done(StatusFromTL(result, _peer)); 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 } // namespace Api

View file

@ -7,13 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "api/api_statistics_sender.h"
#include "data/data_credits.h" #include "data/data_credits.h"
#include "data/data_credits_earn.h"
#include "mtproto/sender.h" #include "mtproto/sender.h"
namespace Main { namespace Main {
class Session; class Session;
} // namespace Main } // namespace Main
class UserData;
namespace Api { namespace Api {
class CreditsTopupOptions final { 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( [[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(
not_null<Main::Session*> session); 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 "api/api_cloud_password.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "ui/layers/generic_box.h"
#include "boxes/passcode_box.h" #include "boxes/passcode_box.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -34,22 +35,33 @@ void RestrictSponsored(
} }
void HandleWithdrawalButton( void HandleWithdrawalButton(
not_null<ChannelData*> channel, RewardReceiver receiver,
not_null<Ui::RippleButton*> button, not_null<Ui::RippleButton*> button,
std::shared_ptr<Ui::Show> show) { std::shared_ptr<Ui::Show> show) {
Expects(receiver.currencyReceiver
|| (receiver.creditsReceiver && receiver.creditsAmount));
struct State { struct State {
rpl::lifetime lifetime; rpl::lifetime lifetime;
bool loading = false; bool loading = false;
}; };
const auto channel = receiver.currencyReceiver;
const auto peer = receiver.creditsReceiver;
const auto state = button->lifetime().make_state<State>(); 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(); session->api().cloudPassword().reload();
button->setClickedCallback([=] { const auto processOut = [=] {
if (state->loading) { if (state->loading) {
return; return;
} }
if (peer && !receiver.creditsAmount()) {
return;
}
state->loading = true; state->loading = true;
state->lifetime = session->api().cloudPassword().state( state->lifetime = session->api().cloudPassword().state(
) | rpl::take( ) | rpl::take(
@ -58,10 +70,12 @@ void HandleWithdrawalButton(
state->loading = false; state->loading = false;
auto fields = PasscodeBox::CloudFields::From(pass); auto fields = PasscodeBox::CloudFields::From(pass);
fields.customTitle fields.customTitle = channel
= tr::lng_channel_earn_balance_password_title(); ? tr::lng_channel_earn_balance_password_title()
fields.customDescription : tr::lng_bot_earn_balance_password_title();
= tr::lng_channel_earn_balance_password_description(tr::now); 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.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = crl::guard(button, [=]( fields.customCheckCallback = crl::guard(button, [=](
const Core::CloudPasswordResult &result, const Core::CloudPasswordResult &result,
@ -74,22 +88,63 @@ void HandleWithdrawalButton(
} }
} }
}; };
const auto fail = [=](const QString &error) { const auto fail = [=](const MTP::Error &error) {
show->showToast(error); show->showToast(error.type());
}; };
session->api().request( if (channel) {
MTPstats_GetBroadcastRevenueWithdrawalUrl( session->api().request(
channel->inputChannel, MTPstats_GetBroadcastRevenueWithdrawalUrl(
result.result channel->inputChannel,
)).done([=](const MTPstats_BroadcastRevenueWithdrawalUrl &r) { result.result
done(qs(r.data().vurl())); )).done([=](const ChannelOutUrl &r) {
}).fail([=](const MTP::Error &error) { done(qs(r.data().vurl()));
fail(error.type()); }).fail(fail).send();
}).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)); 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, bool restricted,
Fn<void(QString)> failed); Fn<void(QString)> failed);
struct RewardReceiver final {
ChannelData *currencyReceiver = nullptr;
PeerData *creditsReceiver = nullptr;
Fn<uint64()> creditsAmount;
};
void HandleWithdrawalButton( void HandleWithdrawalButton(
not_null<ChannelData*> channel, RewardReceiver receiver,
not_null<Ui::RippleButton*> button, not_null<Ui::RippleButton*> button,
std::shared_ptr<Ui::Show> show); 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.h"
#include "api/api_statistics_data_deserialize.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "data/data_channel.h" #include "data/data_channel.h"
@ -15,33 +16,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_story.h" #include "data/data_story.h"
#include "history/history.h" #include "history/history.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "statistics/statistics_data_deserialize.h"
namespace Api { namespace Api {
namespace { 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( [[nodiscard]] Data::StatisticalValue StatisticalValueFromTL(
const MTPStatsAbsValueAndPrev &tl) { const MTPStatsAbsValueAndPrev &tl) {
const auto current = tl.data().vcurrent().v; const auto current = tl.data().vcurrent().v;
@ -223,61 +201,6 @@ Statistics::Statistics(not_null<ChannelData*> channel)
: StatisticsRequestSender(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() { rpl::producer<rpl::no_value, QString> Statistics::request() {
return [=](auto consumer) { return [=](auto consumer) {
auto lifetime = rpl::lifetime(); auto lifetime = rpl::lifetime();
@ -747,11 +670,11 @@ Data::BoostStatus Boosts::boostStatus() const {
return _boostStatus; return _boostStatus;
} }
EarnStatistics::EarnStatistics(not_null<ChannelData*> channel) ChannelEarnStatistics::ChannelEarnStatistics(not_null<ChannelData*> channel)
: StatisticsRequestSender(channel) { : StatisticsRequestSender(channel) {
} }
rpl::producer<rpl::no_value, QString> EarnStatistics::request() { rpl::producer<rpl::no_value, QString> ChannelEarnStatistics::request() {
return [=](auto consumer) { return [=](auto consumer) {
auto lifetime = rpl::lifetime(); 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, const Data::EarnHistorySlice::OffsetToken &token,
Fn<void(Data::EarnHistorySlice)> done) { Fn<void(Data::EarnHistorySlice)> done) {
if (_requestId) { if (_requestId) {
@ -865,7 +788,7 @@ void EarnStatistics::requestHistory(
}).send(); }).send();
} }
Data::EarnStatistics EarnStatistics::data() const { Data::EarnStatistics ChannelEarnStatistics::data() const {
return _data; return _data;
} }

View file

@ -7,45 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/timer.h" #include "api/api_statistics_sender.h"
#include "data/data_boosts.h" #include "data/data_boosts.h"
#include "data/data_channel_earn.h" #include "data/data_channel_earn.h"
#include "data/data_statistics.h" #include "data/data_statistics.h"
#include "mtproto/sender.h"
class ChannelData; class ChannelData;
class PeerData; class PeerData;
namespace Api { 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 { class Statistics final : public StatisticsRequestSender {
public: public:
explicit Statistics(not_null<ChannelData*> channel); explicit Statistics(not_null<ChannelData*> channel);
@ -108,9 +79,9 @@ private:
}; };
class EarnStatistics final : public StatisticsRequestSender { class ChannelEarnStatistics final : public StatisticsRequestSender {
public: public:
explicit EarnStatistics(not_null<ChannelData*> channel); explicit ChannelEarnStatistics(not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request(); [[nodiscard]] rpl::producer<rpl::no_value, QString> request();
void requestHistory( 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 peerId = peerFromMTP(d.vpeer());
const auto msgId = d.vmsg_id().v; const auto msgId = d.vmsg_id().v;
if (const auto item = session().data().message(peerId, msgId)) { if (const auto item = session().data().message(peerId, msgId)) {
item->applyEdition(d.vextended_media()); item->applyEdition(d.vextended_media().v);
} }
} break; } break;
@ -2129,6 +2129,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
}; };
if (IsForceLogoutNotification(d)) { if (IsForceLogoutNotification(d)) {
Core::App().forceLogOut(&session().account(), text); Core::App().forceLogOut(&session().account(), text);
} else if (IsWithdrawalNotification(d)) {
return;
} else if (d.is_popup()) { } else if (d.is_popup()) {
const auto &windows = session().windows(); const auto &windows = session().windows();
if (!windows.empty()) { 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 } // namespace Api

View file

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

View file

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

View file

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

View file

@ -2204,7 +2204,8 @@ void ApiWrap::saveDraftsToCloud() {
entities, entities,
Data::WebPageForMTP( Data::WebPageForMTP(
cloudDraft->webpage, cloudDraft->webpage,
textWithTags.text.isEmpty()) textWithTags.text.isEmpty()),
MTP_long(0) // effect
)).done([=](const MTPBool &result, const MTP::Response &response) { )).done([=](const MTPBool &result, const MTP::Response &response) {
const auto requestId = response.requestId; const auto requestId = response.requestId;
history->finishSavingCloudDraft( history->finishSavingCloudDraft(
@ -4259,7 +4260,11 @@ void ApiWrap::sendMediaWithRandomId(
MTP_flags(flags), MTP_flags(flags),
peer->input, peer->input,
Data::Histories::ReplyToPlaceholder(), Data::Histories::ReplyToPlaceholder(),
media, (options.price
? MTPInputMedia(MTP_inputMediaPaidMedia(
MTP_long(options.price),
MTP_vector<MTPInputMedia>(1, media)))
: media),
MTP_string(caption.text), MTP_string(caption.text),
MTP_long(randomId), MTP_long(randomId),
MTPReplyMarkup(), 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( void ApiWrap::sendAlbumWithUploaded(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MessageGroupId &groupId, const MessageGroupId &groupId,
@ -4334,8 +4415,11 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
if (!sample) { if (!sample) {
_sendingAlbums.remove(groupId); _sendingAlbums.remove(groupId);
return; return;
} else if (album->options.price > 0) {
sendMultiPaidMedia(sample, album);
return;
} else if (medias.size() < 2) { } else if (medias.size() < 2) {
const auto &single = medias.front().c_inputSingleMedia(); const auto &single = medias.front().data();
sendMediaWithRandomId( sendMediaWithRandomId(
sample, sample,
single.vmedia(), single.vmedia(),

View file

@ -545,6 +545,10 @@ private:
Api::SendOptions options, Api::SendOptions options,
uint64 randomId, uint64 randomId,
Fn<void(bool)> done = nullptr); 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 getTopPromotionDelayed(TimeId now, TimeId next);
void topPromotionDone(const MTPhelp_PromoData &proxy); void topPromotionDone(const MTPhelp_PromoData &proxy);

View file

@ -237,7 +237,7 @@ shareColumnSkip: 6px;
shareActivateDuration: 150; shareActivateDuration: 150;
shareScrollDuration: 300; shareScrollDuration: 300;
shareComment: InputField(defaultInputField) { shareComment: InputField(defaultInputField) {
font: normalFont; style: defaultTextStyle;
textMargins: margins(8px, 8px, 8px, 6px); textMargins: margins(8px, 8px, 8px, 6px);
heightMin: 36px; heightMin: 36px;
heightMax: 72px; heightMax: 72px;
@ -290,6 +290,29 @@ passcodeTextLine: 28px;
passcodeLittleSkip: 5px; passcodeLittleSkip: 5px;
passcodeAboutSkip: 7px; passcodeAboutSkip: 7px;
passcodeSkip: 23px; 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; newGroupAboutFg: windowSubTextFg;
newGroupPadding: margins(4px, 6px, 4px, 3px); newGroupPadding: margins(4px, 6px, 4px, 3px);
@ -585,7 +608,7 @@ groupStickersRemovePosition: point(6px, 6px);
groupStickersFieldPadding: margins(8px, 6px, 8px, 6px); groupStickersFieldPadding: margins(8px, 6px, 8px, 6px);
groupStickersField: InputField(defaultMultiSelectSearchField) { groupStickersField: InputField(defaultMultiSelectSearchField) {
placeholderFont: boxTextFont; placeholderFont: boxTextFont;
font: boxTextFont; style: boxTextStyle;
placeholderMargins: margins(0px, 0px, 0px, 0px); placeholderMargins: margins(0px, 0px, 0px, 0px);
textMargins: margins(0px, 7px, 0px, 0px); textMargins: margins(0px, 7px, 0px, 0px);
textBg: boxBg; textBg: boxBg;
@ -672,7 +695,6 @@ themesMenuToggle: IconButton(defaultIconButton) {
themesMenuPosition: point(-2px, 25px); themesMenuPosition: point(-2px, 25px);
createPollField: InputField(defaultInputField) { createPollField: InputField(defaultInputField) {
font: boxTextFont;
textMargins: margins(0px, 4px, 0px, 4px); textMargins: margins(0px, 4px, 0px, 4px);
textAlign: align(left); textAlign: align(left);
heightMin: 36px; heightMin: 36px;
@ -877,7 +899,6 @@ scheduleDateField: InputField(defaultInputField) {
placeholderScale: 0.; placeholderScale: 0.;
heightMin: 30px; heightMin: 30px;
textAlign: align(top); textAlign: align(top);
font: font(14px);
} }
scheduleTimeField: InputField(scheduleDateField) { scheduleTimeField: InputField(scheduleDateField) {
border: 0px; border: 0px;
@ -905,7 +926,6 @@ muteBoxTimeField: InputField(scheduleDateField) {
placeholderScale: 0.; placeholderScale: 0.;
heightMin: 30px; heightMin: 30px;
textAlign: align(left); textAlign: align(left);
font: font(14px);
} }
muteBoxTimeFieldPadding: margins(5px, 0px, 5px, 0px); muteBoxTimeFieldPadding: margins(5px, 0px, 5px, 0px);

View file

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

View file

@ -1044,7 +1044,16 @@ not_null<Ui::InputField*> CreatePollBox::setupSolution(
solution->setInstantReplaces(Ui::InstantReplaces::Default()); solution->setInstantReplaces(Ui::InstantReplaces::Default());
solution->setInstantReplacesEnabled( solution->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue()); 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( solution->setEditLinkCallback(
DefaultEditLinkCallback(_controller->uiShow(), solution)); DefaultEditLinkCallback(_controller->uiShow(), solution));
solution->customTab(true); solution->customTab(true);

View file

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

View file

@ -1011,14 +1011,16 @@ void GiftPremiumValidator::showChoosePeerBox(const QString &ref) {
}) | ranges::views::filter([](UserData *u) -> bool { }) | ranges::views::filter([](UserData *u) -> bool {
return u; return u;
}) | ranges::to<std::vector<not_null<UserData*>>>(); }) | ranges::to<std::vector<not_null<UserData*>>>();
if (!users.empty()) { if (users.empty()) {
const auto giftBox = show->show( show->showToast(
Box(GiftsBox, _controller, users, api, ref)); tr::lng_settings_gift_premium_choose(tr::now));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
} }
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
(*ignoreClose) = true; (*ignoreClose) = true;
peersBox->closeBox(); peersBox->closeBox();
}; };
@ -1644,12 +1646,60 @@ void AddCreditsHistoryEntryTable(
container, container,
st::giveawayGiftCodeTable), st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin); 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( AddTableRow(
table, table,
tr::lng_credits_box_history_entry_peer(), tr::lng_credits_box_history_entry_via(),
controller, tr::lng_credits_box_history_entry_app_store(
PeerId(entry.bareId)); 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()) { if (!entry.id.isEmpty()) {
constexpr auto kOneLineCount = 18; constexpr auto kOneLineCount = 18;
@ -1680,4 +1730,17 @@ void AddCreditsHistoryEntryTable(
tr::lng_gift_link_label_date(), tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.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); auto option = QTextOption(style::al_left);
option.setWrapMode(QTextOption::WrapAnywhere); option.setWrapMode(QTextOption::WrapAnywhere);
p.setFont(_linkOver p.setFont(_linkOver
? st::defaultInputField.font->underline() ? st::defaultInputField.style.font->underline()
: st::defaultInputField.font); : st::defaultInputField.style.font);
p.setPen(st::defaultLinkButton.color); p.setPen(st::defaultLinkButton.color);
const auto inviteLinkText = _channel->inviteLink().isEmpty() const auto inviteLinkText = _channel->inviteLink().isEmpty()
? tr::lng_group_invite_create(tr::now) ? 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 "boxes/peers/edit_peer_info_box.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "api/api_credits.h"
#include "api/api_peer_photo.h" #include "api/api_peer_photo.h"
#include "api/api_user_names.h" #include "api/api_user_names.h"
#include "main/main_session.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_premium_limits.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "history/admin_log/history_admin_log_section.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/channel_statistics/boosts/info_boosts_widget.h"
#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_values.h"
#include "info/info_memento.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/boxes/boost_box.h"
#include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button.h"
#include "ui/controls/userpic_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/rp_widget.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "ui/toast/toast.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_boxes.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include <QtSvg/QSvgRenderer>
namespace { namespace {
constexpr auto kBotManagerUsername = "BotFather"_cs; constexpr auto kBotManagerUsername = "BotFather"_cs;
@ -343,6 +349,7 @@ private:
void fillPendingRequestsButton(); void fillPendingRequestsButton();
void fillBotUsernamesButton(); void fillBotUsernamesButton();
void fillBotBalanceButton();
void fillBotEditIntroButton(); void fillBotEditIntroButton();
void fillBotEditCommandsButton(); void fillBotEditCommandsButton();
void fillBotEditSettingsButton(); void fillBotEditSettingsButton();
@ -1126,6 +1133,7 @@ void Controller::fillManageSection() {
::AddSkip(container, 0); ::AddSkip(container, 0);
fillBotUsernamesButton(); fillBotUsernamesButton();
fillBotBalanceButton();
fillBotEditIntroButton(); fillBotEditIntroButton();
fillBotEditCommandsButton(); fillBotEditCommandsButton();
fillBotEditSettingsButton(); fillBotEditSettingsButton();
@ -1536,6 +1544,84 @@ void Controller::fillBotUsernamesButton() {
{ &st::menuIconLinks }); { &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() { void Controller::fillBotEditIntroButton() {
Expects(_isBot); Expects(_isBot);

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/event_filter.h" #include "base/event_filter.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_document.h" #include "data/data_document.h"
@ -351,8 +352,8 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
const auto customEmojiPaused = [controller = args.controller] { const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer); return controller->isGifPausedAtLeastFor(PauseReason::Layer);
}; };
raw->setCustomEmojiFactory([=](QStringView data, Fn<void()> update) auto factory = [=](QStringView data, Fn<void()> update)
-> std::unique_ptr<Ui::Text::CustomEmoji> { -> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto id = Data::ParseCustomEmojiData(data); const auto id = Data::ParseCustomEmojiData(data);
auto result = owner->customEmojiManager().create( auto result = owner->customEmojiManager().create(
data, data,
@ -364,7 +365,13 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
} }
using namespace Ui::Text; using namespace Ui::Text;
return std::make_unique<FirstFrameEmoji>(std::move(result)); 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 callback = args.callback;
const auto isCustom = [=](DocumentId id) { 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(); return tr::lng_premium_summary_subtitle_translation();
case PremiumFeature::Business: case PremiumFeature::Business:
return tr::lng_premium_summary_subtitle_business(); return tr::lng_premium_summary_subtitle_business();
case PremiumFeature::Effects:
return tr::lng_premium_summary_subtitle_effects();
case PremiumFeature::BusinessLocation: case PremiumFeature::BusinessLocation:
return tr::lng_business_subtitle_location(); 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(); return tr::lng_premium_summary_about_translation();
case PremiumFeature::Business: case PremiumFeature::Business:
return tr::lng_premium_summary_about_business(); return tr::lng_premium_summary_about_business();
case PremiumFeature::Effects:
return tr::lng_premium_summary_about_effects();
case PremiumFeature::BusinessLocation: case PremiumFeature::BusinessLocation:
return tr::lng_business_about_location(); return tr::lng_business_about_location();
@ -529,6 +533,7 @@ struct VideoPreviewDocument {
case PremiumFeature::Wallpapers: return "wallpapers"; case PremiumFeature::Wallpapers: return "wallpapers";
case PremiumFeature::LastSeen: return "last_seen"; case PremiumFeature::LastSeen: return "last_seen";
case PremiumFeature::MessagePrivacy: return "message_privacy"; case PremiumFeature::MessagePrivacy: return "message_privacy";
case PremiumFeature::Effects: return "effects";
case PremiumFeature::BusinessLocation: return "business_location"; case PremiumFeature::BusinessLocation: return "business_location";
case PremiumFeature::BusinessHours: return "business_hours"; case PremiumFeature::BusinessHours: return "business_hours";

View file

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

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h" #include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext. #include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/data_credits.h" #include "data/data_credits.h"
#include "data/data_photo.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/stickers/data_custom_emoji.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" #include "styles/style_settings.h"
namespace Ui { 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( void SendCreditsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
@ -89,22 +231,10 @@ void SendCreditsBox(
}, ministarsContainer->lifetime()); }, ministarsContainer->lifetime());
} }
const auto bot = session->data().user(form->botId); const auto thumb = box->addRow(object_ptr<Ui::CenterWrap<>>(
content,
if (form->photo) { SendCreditsThumbnail(content, session, form.get(), photoSize)));
box->addRow(object_ptr<Ui::CenterWrap<>>( thumb->setAttribute(Qt::WA_TransparentForMouseEvents);
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);
}
Ui::AddSkip(content); Ui::AddSkip(content);
box->addRow(object_ptr<Ui::CenterWrap<>>( box->addRow(object_ptr<Ui::CenterWrap<>>(
@ -118,14 +248,7 @@ void SendCreditsBox(
box, box,
object_ptr<Ui::FlatLabel>( object_ptr<Ui::FlatLabel>(
box, box,
tr::lng_credits_box_out_sure( SendCreditsConfirmText(session, form.get()),
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),
st::creditsBoxAbout))); st::creditsBoxAbout)));
Ui::AddSkip(content); Ui::AddSkip(content);
Ui::AddSkip(content); Ui::AddSkip(content);
@ -158,26 +281,16 @@ void SendCreditsBox(
loadingAnimation->showOn(state->confirmButtonBusy.value()); 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( auto buttonText = tr::lng_credits_box_out_confirm(
lt_count, lt_count,
rpl::single(form->invoice.amount) | tr::to_count(), rpl::single(form->invoice.amount) | tr::to_count(),
lt_emoji, lt_emoji,
rpl::single(buttonEmoji), rpl::single(CreditsEmojiSmall(session)),
Ui::Text::RichLangValue); Ui::Text::RichLangValue);
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>( const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button, button,
rpl::single(QString()), rpl::single(QString()),
st::defaultFlatLabel); st::creditsBoxButtonLabel);
std::move( std::move(
buttonText buttonText
) | rpl::start_with_next([=](const TextWithEntities &text) { ) | 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 } // namespace Ui

View file

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

View file

@ -10,7 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
#include "storage/storage_media_prepare.h" #include "storage/storage_media_prepare.h"
#include "iv/iv_instance.h"
#include "mainwidget.h" #include "mainwidget.h"
#include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_session_settings.h" #include "main/main_session_settings.h"
#include "mtproto/mtproto_config.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/controls/history_view_characters_limit.h"
#include "history/view/history_view_schedule_box.h" #include "history/view/history_view_schedule_box.h"
#include "core/mime_type.h" #include "core/mime_type.h"
#include "core/ui_integration.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "base/call_delayed.h" #include "base/call_delayed.h"
#include "boxes/premium_limits_box.h" #include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h" #include "boxes/premium_preview_box.h"
#include "boxes/send_credits_box.h"
#include "ui/effects/scroll_content_shadow.h" #include "ui/effects/scroll_content_shadow.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/widgets/scroll_area.h" #include "ui/widgets/scroll_area.h"
#include "ui/widgets/popup_menu.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_file_preview.h"
#include "ui/chat/attach/attach_single_media_preview.h" #include "ui/chat/attach/attach_single_media_preview.h"
#include "ui/grouped_layout.h" #include "ui/grouped_layout.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/vertical_list.h"
#include "lottie/lottie_single_player.h" #include "lottie/lottie_single_player.h"
#include "data/data_channel.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_peer_values.h" // Data::AmPremiumValue.
@ -111,6 +119,84 @@ rpl::producer<QString> FieldPlaceholder(
: tr::lng_photos_comment(); : 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 } // namespace
SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) { SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) {
@ -161,7 +247,8 @@ SendFilesBox::Block::Block(
int from, int from,
int till, int till,
Fn<bool()> gifPaused, Fn<bool()> gifPaused,
SendFilesWay way) SendFilesWay way,
Fn<bool()> canToggleSpoiler)
: _items(items) : _items(items)
, _from(from) , _from(from)
, _till(till) { , _till(till) {
@ -178,14 +265,16 @@ SendFilesBox::Block::Block(
parent.get(), parent.get(),
st, st,
my, my,
way); way,
std::move(canToggleSpoiler));
_preview.reset(preview); _preview.reset(preview);
} else { } else {
const auto media = Ui::SingleMediaPreview::Create( const auto media = Ui::SingleMediaPreview::Create(
parent, parent,
st, st,
gifPaused, gifPaused,
first); first,
std::move(canToggleSpoiler));
if (media) { if (media) {
_isSingleMedia = true; _isSingleMedia = true;
_preview.reset(media); _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) { void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {
if (!_isAlbum) { if (!_isAlbum) {
if (_isSingleMedia) { 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( SendFilesBox::SendFilesBox(
QWidget*, QWidget*,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
@ -393,6 +502,9 @@ Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
: _invertCaption : _invertCaption
? SendMenu::CaptionState::Above ? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below; : SendMenu::CaptionState::Below;
result.price = canChangePrice()
? _price.current()
: std::optional<uint64>();
return result; return result;
}); });
} }
@ -406,6 +518,7 @@ auto SendFilesBox::prepareSendMenuCallback()
case Type::CaptionUp: _invertCaption = true; break; case Type::CaptionUp: _invertCaption = true; break;
case Type::SpoilerOn: toggleSpoilers(true); break; case Type::SpoilerOn: toggleSpoilers(true); break;
case Type::SpoilerOff: toggleSpoilers(false); break; case Type::SpoilerOff: toggleSpoilers(false); break;
case Type::ChangePrice: changePrice(); break;
default: default:
SendMenu::DefaultCallback( SendMenu::DefaultCallback(
_show, _show,
@ -596,14 +709,27 @@ void SendFilesBox::refreshButtons() {
addMenuButton(); addMenuButton();
} }
bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const { bool SendFilesBox::hasSendMenu(const MenuDetails &details) const {
return (details.type != SendMenu::Type::Disabled) return (details.type != SendMenu::Type::Disabled)
|| (details.spoiler != SendMenu::SpoilerState::None) || (details.spoiler != SendMenu::SpoilerState::None)
|| (details.caption != SendMenu::CaptionState::None); || (details.caption != SendMenu::CaptionState::None);
} }
bool SendFilesBox::hasSpoilerMenu() const { 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() { 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() { void SendFilesBox::addMenuButton() {
const auto details = _sendMenuDetails(); const auto details = _sendMenuDetails();
if (!hasSendMenu(details)) { if (!hasSendMenu(details)) {
@ -719,6 +957,7 @@ void SendFilesBox::initSendWay() {
block.setSendWay(value); block.setSendWay(value);
} }
refreshButtons(); refreshButtons();
refreshPriceTag();
if (was != hidden()) { if (was != hidden()) {
updateBoxSize(); updateBoxSize();
updateControlsGeometry(); updateControlsGeometry();
@ -804,7 +1043,8 @@ void SendFilesBox::pushBlock(int from, int till) {
from, from,
till, till,
gifPaused, gifPaused,
_sendWay.current()); _sendWay.current(),
[=] { return !hasPrice(); });
auto &block = _blocks.back(); auto &block = _blocks.back();
const auto widget = _inner->add( const auto widget = _inner->add(
block.takeWidget(), block.takeWidget(),
@ -927,10 +1167,18 @@ void SendFilesBox::pushBlock(int from, int till) {
st::sendMediaPreviewSize, st::sendMediaPreviewSize,
[=] { refreshAllAfterChanges(from); }); [=] { refreshAllAfterChanges(from); });
}, widget->lifetime()); }, widget->lifetime());
block.orderUpdated() | rpl::start_with_next([=]{
if (_priceTag) {
_priceTagBg = QImage();
_priceTag->update();
}
}, widget->lifetime());
} }
void SendFilesBox::refreshControls(bool initial) { void SendFilesBox::refreshControls(bool initial) {
refreshButtons(); refreshButtons();
refreshPriceTag();
refreshTitleText(); refreshTitleText();
updateSendWayControls(); updateSendWayControls();
updateCaptionPlaceholder(); updateCaptionPlaceholder();
@ -1499,6 +1747,7 @@ void SendFilesBox::send(
auto child = _sendMenuDetails(); auto child = _sendMenuDetails();
child.spoiler = SendMenu::SpoilerState::None; child.spoiler = SendMenu::SpoilerState::None;
child.caption = SendMenu::CaptionState::None; child.caption = SendMenu::CaptionState::None;
child.price = std::nullopt;
return SendMenu::DefaultCallback(_show, sendCallback())( return SendMenu::DefaultCallback(_show, sendCallback())(
{ .type = SendMenu::ActionType::Schedule }, { .type = SendMenu::ActionType::Schedule },
child); child);
@ -1526,10 +1775,16 @@ void SendFilesBox::send(
auto caption = (_caption && !_caption->isHidden()) auto caption = (_caption && !_caption->isHidden())
? _caption->getTextWithAppliedMarkdown() ? _caption->getTextWithAppliedMarkdown()
: TextWithTags(); : TextWithTags();
options.invertCaption = _invertCaption;
if (!validateLength(caption.text)) { if (!validateLength(caption.text)) {
return; return;
} }
options.invertCaption = _invertCaption;
options.price = hasPrice() ? _price.current() : 0;
if (options.price > 0) {
for (auto &file : _list.files) {
file.spoiler = false;
}
}
_confirmedCallback( _confirmedCallback(
std::move(_list), std::move(_list),
_sendWay.current(), _sendWay.current(),

View file

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

View file

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

View file

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

View file

@ -383,20 +383,14 @@ void Panel::initWindow() {
&& _fullScreenOrMaximized.current()) { && _fullScreenOrMaximized.current()) {
toggleFullScreen(); 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; 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) { window()->setBodyTitleArea([=](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag; using Flag = Ui::WindowTitleHitTestFlag;
const auto titleRect = QRect( const auto titleRect = QRect(

View file

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

View file

@ -71,6 +71,7 @@ ComposeIcons {
menuSpoilerOff: icon; menuSpoilerOff: icon;
menuBelow: icon; menuBelow: icon;
menuAbove: icon; menuAbove: icon;
menuPrice: icon;
stripBubble: icon; stripBubble: icon;
stripExpandPanel: icon; stripExpandPanel: icon;
@ -610,6 +611,7 @@ defaultComposeIcons: ComposeIcons {
menuSpoilerOff: menuIconSpoilerOff; menuSpoilerOff: menuIconSpoilerOff;
menuBelow: menuIconBelow; menuBelow: menuIconBelow;
menuAbove: menuIconAbove; menuAbove: menuIconAbove;
menuPrice: menuIconEarn;
stripBubble: icon{ stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg }, { "chat/reactions_bubble_shadow", windowShadowFg },
@ -989,8 +991,33 @@ historyUnreadReactions: TwoIconButton(historyToDown) {
} }
historyUnreadThingsSkip: 4px; 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) { historyComposeField: InputField(defaultInputField) {
font: normalFont; style: historyTextStyle;
textMargins: margins(0px, 0px, 0px, 0px); textMargins: margins(0px, 0px, 0px, 0px);
textAlign: align(left); textAlign: align(left);
textFg: historyComposeAreaFg; textFg: historyComposeAreaFg;
@ -1381,3 +1408,18 @@ editTagField: InputField(defaultInputField) {
editTagLimit: FlatLabel(defaultFlatLabel) { editTagLimit: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg; 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; return (Window::SessionController*)nullptr;
}; };
auto &app = Core::App(); auto &app = Core::App();
const auto account = not_null(&session->account());
if (const auto a = check(app.activeWindow())) { if (const auto a = check(app.activeWindow())) {
return a; return a;
} else if (const auto b = check(app.activePrimaryWindow())) { } else if (const auto b = check(app.activePrimaryWindow())) {
return b; return b;
} else if (const auto c = check(app.windowFor(&session->account()))) { } else if (const auto c = check(app.windowFor(account))) {
return c; return c;
} else if (const auto d = check( } else if (const auto d = check(app.ensureSeparateWindowFor(
app.ensureSeparateWindowForAccount( account))) {
&session->account()))) {
return d; return d;
} }
return nullptr; return nullptr;

View file

@ -14,11 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qthelp_regex.h" #include "base/qthelp_regex.h"
#include "base/qthelp_url.h" #include "base/qthelp_url.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "ui/chat/chat_style.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "core/shortcuts.h" #include "core/shortcuts.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "core/ui_integration.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.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_boxes.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h" #include "base/qt/qt_common_adapters.h"
#include <QtCore/QMimeData> #include <QtCore/QMimeData>
@ -58,6 +61,7 @@ using EditLinkSelection = Ui::InputField::EditLinkSelection;
constexpr auto kParseLinksTimeout = crl::time(500); constexpr auto kParseLinksTimeout = crl::time(500);
constexpr auto kTypesDuration = 4 * crl::time(1000); constexpr auto kTypesDuration = 4 * crl::time(1000);
constexpr auto kCodeLanguageLimit = 32;
// For mention / custom emoji tags save and validate selfId, // For mention / custom emoji tags save and validate selfId,
// ignore tags for different users. // ignore tags for different users.
@ -222,6 +226,51 @@ void EditLinkBox(
}, text->lifetime()); }, 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) { TextWithEntities StripSupportHashtag(TextWithEntities text) {
static const auto expression = QRegularExpression( static const auto expression = QRegularExpression(
u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q, u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q,
@ -274,15 +323,24 @@ TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
bool EditTextChanged( bool EditTextChanged(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const TextWithTags &updated) { TextWithTags updated) {
const auto original = PrepareEditText(item); 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 // Tags can be different for the same entities, because for
// animated emoji each tag contains a different random number. // animated emoji each tag contains a different random number.
// So we compare entities instead of tags. // So we compare entities instead of tags.
return (original.text != updated.text) return originalWithEntities != updatedWithEntities;
|| (TextUtilities::ConvertTextTagsToEntities(original.tags)
!= TextUtilities::ConvertTextTagsToEntities(updated.tags));
} }
Fn<bool( 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( void InitMessageFieldHandlers(
not_null<Main::Session*> session, not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show, std::shared_ptr<Main::SessionShow> show,
@ -329,12 +394,16 @@ void InitMessageFieldHandlers(
const style::InputField *fieldStyle) { const style::InputField *fieldStyle) {
field->setTagMimeProcessor( field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji)); 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(); return On(PowerSaving::kEmojiChat) || customEmojiPaused();
}; }, [customEmojiPaused] {
field->setCustomEmojiFactory( return On(PowerSaving::kChatSpoiler) || customEmojiPaused();
session->data().customEmojiManager().factory(), });
std::move(customEmojiPaused));
field->setInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled( field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue()); Core::App().settings().replaceEmojiValue());
@ -342,8 +411,18 @@ void InitMessageFieldHandlers(
if (show) { if (show) {
field->setEditLinkCallback( field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle)); DefaultEditLinkCallback(show, field, fieldStyle));
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, fieldStyle != nullptr); 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) { [[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {
@ -569,12 +648,26 @@ void InitMessageFieldFade(
Ui::DestroyChild(b.data()); Ui::DestroyChild(b.data());
}, topFade->lifetime()); }, topFade->lifetime());
topFade->show(); const auto descent = field->st().style.font->descent;
bottomFade->showOn( rpl::merge(
field->scrollTop().value( field->changes(),
) | rpl::map([field, descent = field->st().font->descent](int scroll) { field->scrollTop().changes() | rpl::to_empty,
return (scroll + descent) < field->scrollTopMax(); field->sizeValue() | rpl::to_empty
}) | rpl::distinct_until_changed()); ) | 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( InlineBotQuery ParseInlineBotQuery(
@ -766,11 +859,7 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
const auto text = static_cast<QKeyEvent*>(event)->text(); const auto text = static_cast<QKeyEvent*>(event)->text();
if (!text.isEmpty() && text.size() < 3) { if (!text.isEmpty() && text.size() < 3) {
const auto ch = text[0]; const auto ch = text[0];
if (false if (IsSpace(ch)) {
|| ch == '\n'
|| ch == '\r'
|| ch.isSpace()
|| ch == QChar::LineSeparator) {
_timer.callOnce(0); _timer.callOnce(0);
} }
} }
@ -1097,7 +1186,9 @@ base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
const auto margins = (st.textMargins + st.placeholderMargins); const auto margins = (st.textMargins + st.placeholderMargins);
const auto available = width - margins.left() - margins.right(); const auto available = width - margins.left() - margins.right();
label->resizeToWidth(available); 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( link->move(
(width - link->width()) / 2, (width - link->width()) / 2,
label->y() + label->height()); label->y() + label->height());
@ -1117,8 +1208,8 @@ void SelectTextInFieldWithMargins(
auto textCursor = field->textCursor(); auto textCursor = field->textCursor();
// Try to set equal margins for top and bottom sides. // Try to set equal margins for top and bottom sides.
const auto charsCountInLine = field->width() const auto charsCountInLine = field->width()
/ field->st().font->width('W'); / field->st().style.font->width('W');
const auto linesCount = (field->height() / field->st().font->height); const auto linesCount = field->height() / field->st().style.font->height;
const auto selectedLines = (selection.to - selection.from) const auto selectedLines = (selection.to - selection.from)
/ charsCountInLine; / charsCountInLine;
constexpr auto kMinDiff = ushort(3); constexpr auto kMinDiff = ushort(3);

View file

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

View file

@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/timer.h"
#include "mtproto/mtproto_auth_key.h" #include "mtproto/mtproto_auth_key.h"
#include "mtproto/mtproto_proxy_data.h" #include "mtproto/mtproto_proxy_data.h"
#include "base/timer.h" #include "window/window_separate_id.h"
class History; class History;
@ -29,11 +30,9 @@ namespace Window {
class Controller; class Controller;
} // namespace Window } // namespace Window
namespace Window { namespace Window::Notifications {
namespace Notifications {
class System; class System;
} // namespace Notifications } // namespace Window::Notifications
} // namespace Window
namespace ChatHelpers { namespace ChatHelpers {
class EmojiKeywords; class EmojiKeywords;
@ -170,19 +169,17 @@ public:
not_null<QWidget*> widget) const; not_null<QWidget*> widget) const;
[[nodiscard]] Window::Controller *activeWindow() const; [[nodiscard]] Window::Controller *activeWindow() const;
[[nodiscard]] Window::Controller *activePrimaryWindow() const; [[nodiscard]] Window::Controller *activePrimaryWindow() const;
[[nodiscard]] Window::Controller *separateWindowForAccount( [[nodiscard]] Window::Controller *separateWindowFor(
not_null<Main::Account*> account) const; Window::SeparateId id) const;
[[nodiscard]] Window::Controller *separateWindowForPeer( Window::Controller *ensureSeparateWindowFor(
not_null<PeerData*> peer) const; Window::SeparateId id,
Window::Controller *ensureSeparateWindowForPeer( MsgId showAtMsgId = 0);
not_null<PeerData*> peer,
MsgId showAtMsgId);
Window::Controller *ensureSeparateWindowForAccount(
not_null<Main::Account*> account);
[[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch. [[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch.
Window::SeparateId id) const;
[[nodiscard]] Window::Controller *windowForShowingHistory(
not_null<PeerData*> peer) const; not_null<PeerData*> peer) const;
[[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch. [[nodiscard]] Window::Controller *windowForShowingForum(
not_null<Main::Account*> account) const; not_null<Data::Forum*> forum) const;
[[nodiscard]] bool closeNonLastAsync( [[nodiscard]] bool closeNonLastAsync(
not_null<Window::Controller*> window); not_null<Window::Controller*> window);
void closeWindow(not_null<Window::Controller*> window); void closeWindow(not_null<Window::Controller*> window);
@ -195,7 +192,7 @@ public:
void checkSystemDarkMode(); void checkSystemDarkMode();
[[nodiscard]] bool isActiveForTrayMenu() const; [[nodiscard]] bool isActiveForTrayMenu() const;
void closeChatFromWindows(not_null<PeerData*> peer); void closeChatFromWindows(not_null<PeerData*> peer);
void checkWindowAccount(not_null<Window::Controller*> window); void checkWindowId(not_null<Window::Controller*> window);
void activate(); void activate();
// Media view interface. // Media view interface.
@ -423,12 +420,9 @@ private:
const std::unique_ptr<Calls::Instance> _calls; const std::unique_ptr<Calls::Instance> _calls;
const std::unique_ptr<Iv::Instance> _iv; const std::unique_ptr<Iv::Instance> _iv;
base::flat_map< base::flat_map<
Main::Account*, Window::SeparateId,
std::unique_ptr<Window::Controller>> _primaryWindows; std::unique_ptr<Window::Controller>> _windows;
base::flat_set<not_null<Window::Controller*>> _closingAsyncWindows; 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; std::vector<not_null<Window::Controller*>> _windowStack;
Window::Controller *_lastActiveWindow = nullptr; Window::Controller *_lastActiveWindow = nullptr;
Window::Controller *_lastActivePrimaryWindow = 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_controller.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h" #include "window/window_session_controller_link_info.h"
#include "styles/style_calls.h" // groupCallBoxLabel
#include "styles/style_layers.h" #include "styles/style_layers.h"
namespace { namespace {
@ -121,7 +122,10 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
} else { } else {
const auto parsedUrl = QUrl::fromUserInput(url); const auto parsedUrl = QUrl::fromUserInput(url);
if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) { 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() const auto displayed = parsedUrl.isValid()
? parsedUrl.toDisplayString() ? parsedUrl.toDisplayString()
: url; : url;
@ -130,7 +134,6 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
: parsedUrl.isValid() : parsedUrl.isValid()
? QString::fromUtf8(parsedUrl.toEncoded()) ? QString::fromUtf8(parsedUrl.toEncoded())
: ShowEncoded(displayed); : ShowEncoded(displayed);
const auto my = context.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get(); const auto controller = my.sessionWindow.get();
const auto use = controller const auto use = controller
? &controller->window() ? &controller->window()
@ -140,8 +143,11 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
.text = (tr::lng_open_this_link(tr::now)), .text = (tr::lng_open_this_link(tr::now)),
.confirmed = [=](Fn<void()> hide) { hide(); open(); }, .confirmed = [=](Fn<void()> hide) { hide(); open(); },
.confirmText = tr::lng_open_link(), .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()); box->addSkip(st.style.lineHeight - st::boxPadding.bottom());
const auto url = box->addRow( const auto url = box->addRow(
object_ptr<Ui::FlatLabel>(box, displayUrl, st)); object_ptr<Ui::FlatLabel>(box, displayUrl, st));
@ -190,6 +196,7 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const {
_bot->name()), _bot->name()),
.confirmed = callback, .confirmed = callback,
.confirmText = tr::lng_allow_bot(), .confirmText = tr::lng_allow_bot(),
.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,
})); }));
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,7 +58,7 @@ public:
const Ui::Emoji::One *defaultEmojiVariant( const Ui::Emoji::One *defaultEmojiVariant(
const Ui::Emoji::One *emoji) override; const Ui::Emoji::One *emoji) override;
std::unique_ptr<Ui::Text::CustomEmoji> createCustomEmoji( std::unique_ptr<Ui::Text::CustomEmoji> createCustomEmoji(
const QString &data, QStringView data,
const std::any &context) override; const std::any &context) override;
Fn<void()> createSpoilerRepaint(const std::any &context) override; Fn<void()> createSpoilerRepaint(const std::any &context) override;
bool allowClickHandlerActivation( 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 AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5001007; constexpr auto AppVersion = 5002000;
constexpr auto AppVersionStr = "5.1.7"; constexpr auto AppVersionStr = "5.2";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

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

View file

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

View file

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

View file

@ -19,6 +19,16 @@ struct CreditTopupOption final {
using CreditTopupOptions = std::vector<CreditTopupOption>; using CreditTopupOptions = std::vector<CreditTopupOption>;
enum class CreditsHistoryMediaType {
Photo,
Video,
};
struct CreditsHistoryMedia {
CreditsHistoryMediaType type = CreditsHistoryMediaType::Photo;
uint64 id = 0;
};
struct CreditsHistoryEntry final { struct CreditsHistoryEntry final {
using PhotoId = uint64; using PhotoId = uint64;
enum class PeerType { enum class PeerType {
@ -28,16 +38,26 @@ struct CreditsHistoryEntry final {
Fragment, Fragment,
Unsupported, Unsupported,
PremiumBot, PremiumBot,
Ads,
}; };
QString id; QString id;
QString title; QString title;
QString description; QString description;
QDateTime date; QDateTime date;
PhotoId photoId = 0; PhotoId photoId = 0;
std::vector<CreditsHistoryMedia> extended;
uint64 credits = 0; uint64 credits = 0;
uint64 bareId = 0; uint64 bareMsgId = 0;
uint64 barePeerId = 0;
PeerType peerType; PeerType peerType;
bool refunded = false; bool refunded = false;
bool pending = false;
bool failed = false;
QDateTime successDate;
QString successLink;
bool in = false;
}; };
struct CreditsStatusSlice final { 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; return;
} }
const auto window = Core::App().windowFor( const auto window = Core::App().windowFor(
&item->history()->session().account()); not_null(&item->history()->session().account()));
if (!window) { if (!window) {
return; return;
} }

View file

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

View file

@ -81,6 +81,7 @@ void Groups::refreshMessage(
bool justRefreshViews) { bool justRefreshViews) {
if (!isGrouped(item)) { if (!isGrouped(item)) {
unregisterMessage(item); unregisterMessage(item);
_data->requestItemViewRefresh(item);
return; return;
} }
if (!item->isRegular() && !item->isScheduled()) { 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 "data/data_media_types.h"
#include "base/random.h"
#include "boxes/send_credits_box.h" // CreditsEmoji.
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" // CreateMedia. #include "history/history_item.h" // CreateMedia.
#include "history/history_location_manager.h" #include "history/history_location_manager.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "history/view/history_view_item_preview.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_photo.h"
#include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_gif.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_giveaway.h"
#include "history/view/media/history_view_invoice.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_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_call.h"
#include "history/view/media/history_view_web_page.h" #include "history/view/media/history_view_web_page.h"
#include "history/view/media/history_view_poll.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 ItemPreview = HistoryView::ItemPreview;
using ItemPreviewImage = HistoryView::ItemPreviewImage; using ItemPreviewImage = HistoryView::ItemPreviewImage;
struct AlbumCounts {
int photos = 0;
int videos = 0;
int audios = 0;
int files = 0;
};
[[nodiscard]] TextWithEntities WithCaptionNotificationText( [[nodiscard]] TextWithEntities WithCaptionNotificationText(
const QString &attachType, const QString &attachType,
const TextWithEntities &caption, const TextWithEntities &caption,
@ -165,7 +174,7 @@ template <typename MediaType>
return (reinterpret_cast<uint64>(data.get()) & ~1) | (spoiler ? 1 : 0); return (reinterpret_cast<uint64>(data.get()) & ~1) | (spoiler ? 1 : 0);
} }
[[nodiscard]] ItemPreviewImage PreparePhotoPreview( [[nodiscard]] ItemPreviewImage PreparePhotoPreviewImage(
not_null<const HistoryItem*> item, not_null<const HistoryItem*> item,
const std::shared_ptr<PhotoMedia> &media, const std::shared_ptr<PhotoMedia> &media,
ImageRoundRadius radius, ImageRoundRadius radius,
@ -181,14 +190,15 @@ template <typename MediaType>
} }
const auto allowedToDownload = media->autoLoadThumbnailAllowed( const auto allowedToDownload = media->autoLoadThumbnailAllowed(
item->history()->peer); item->history()->peer);
const auto cacheKey = allowedToDownload ? 0 : counted; const auto spoilered = uint64(spoiler ? 1 : 0);
const auto cacheKey = allowedToDownload ? spoilered : counted;
if (allowedToDownload) { if (allowedToDownload) {
media->owner()->load(PhotoSize::Small, item->fullId()); media->owner()->load(PhotoSize::Small, item->fullId());
} }
if (const auto blurred = media->thumbnailInline()) { if (const auto blurred = media->thumbnailInline()) {
return { PreparePreviewImage(blurred, radius, spoiler), cacheKey }; return { PreparePreviewImage(blurred, radius, spoiler), cacheKey };
} }
return { QImage(), allowedToDownload ? 0 : cacheKey }; return { QImage(), allowedToDownload ? spoilered : cacheKey };
} }
[[nodiscard]] ItemPreviewImage PrepareFilePreviewImage( [[nodiscard]] ItemPreviewImage PrepareFilePreviewImage(
@ -207,10 +217,11 @@ template <typename MediaType>
}; };
} }
document->loadThumbnail(item->fullId()); document->loadThumbnail(item->fullId());
const auto spoilered = uint64(spoiler ? 1 : 0);
if (const auto blurred = media->thumbnailInline()) { 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) { [[nodiscard]] QImage PutPlayIcon(QImage preview) {
@ -225,6 +236,18 @@ template <typename MediaType>
return preview; 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( [[nodiscard]] ItemPreviewImage PrepareFilePreview(
not_null<const HistoryItem*> item, not_null<const HistoryItem*> item,
const std::shared_ptr<DocumentMedia> &media, const std::shared_ptr<DocumentMedia> &media,
@ -261,48 +284,82 @@ template <typename MediaType>
} }
bool UpdateExtendedMedia( bool UpdateExtendedMedia(
Invoice &invoice, std::unique_ptr<Media> &media,
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) { const MTPMessageExtendedMedia &extended) {
return media.match([&](const MTPDmessageExtendedMediaPreview &data) { return extended.match([&](const MTPDmessageExtendedMediaPreview &data) {
if (invoice.extendedMedia) { auto photo = (PhotoData*)nullptr;
return false; 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 changed = false;
auto &preview = invoice.extendedPreview; auto size = QSize();
auto thumbnail = QByteArray();
auto videoDuration = std::optional<TimeId>();
if (const auto &w = data.vw()) { if (const auto &w = data.vw()) {
const auto &h = data.vh(); const auto &h = data.vh();
Assert(h.has_value()); Assert(h.has_value());
const auto dimensions = QSize(w->v, h->v); size = QSize(w->v, h->v);
if (preview.dimensions != dimensions) { if (!changed && photo->size(PhotoSize::Large) != size) {
preview.dimensions = dimensions;
changed = true; changed = true;
} }
} }
if (const auto &thumb = data.vthumb()) { if (const auto &thumb = data.vthumb()) {
if (thumb->type() == mtpc_photoStrippedSize) { if (thumb->type() == mtpc_photoStrippedSize) {
const auto bytes = thumb->c_photoStrippedSize().vbytes().v; thumbnail = thumb->c_photoStrippedSize().vbytes().v;
if (preview.inlineThumbnailBytes != bytes) { if (!changed && photo->inlineThumbnailBytes() != thumbnail) {
preview.inlineThumbnailBytes = bytes;
changed = true; changed = true;
} }
} }
} }
if (const auto &duration = data.vvideo_duration()) { if (const auto &duration = data.vvideo_duration()) {
if (preview.videoDuration != duration->v) { videoDuration = duration->v;
preview.videoDuration = duration->v; if (photo->extendedMediaVideoDuration() != videoDuration) {
changed = true; 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; return changed;
}, [&](const MTPDmessageExtendedMedia &data) { }, [&](const MTPDmessageExtendedMedia &data) {
invoice.extendedMedia = HistoryItem::CreateMedia( media = HistoryItem::CreateMedia(item, data.vmedia());
item,
data.vmedia());
return true; 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( TextForMimeData WithCaptionClipboardText(
const QString &attachType, const QString &attachType,
TextForMimeData &&caption) { TextForMimeData &&caption) {
@ -322,6 +379,29 @@ TextForMimeData WithCaptionClipboardText(
return result; 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 } // namespace
Invoice ComputeInvoiceData( Invoice ComputeInvoiceData(
@ -344,11 +424,23 @@ Invoice ComputeInvoiceData(
.isTest = data.is_test(), .isTest = data.is_test(),
}; };
if (const auto &media = data.vextended_media()) { if (const auto &media = data.vextended_media()) {
UpdateExtendedMedia(result, item, *media); UpdateExtendedMedia(result, item, { *media });
} }
return result; 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) { Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
auto result = Call(); auto result = Call();
result.finishReason = [&] { result.finishReason = [&] {
@ -424,6 +516,27 @@ GiveawayResults ComputeGiveawayResultsData(
return result; 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) { Media::Media(not_null<HistoryItem*> parent) : _parent(parent) {
} }
@ -586,21 +699,18 @@ ItemPreview Media::toGroupPreview(
ToPreviewOptions options) const { ToPreviewOptions options) const {
auto result = ItemPreview(); auto result = ItemPreview();
auto loadingContext = std::vector<std::any>(); auto loadingContext = std::vector<std::any>();
auto photoCount = 0; auto counts = AlbumCounts();
auto videoCount = 0;
auto audioCount = 0;
auto fileCount = 0;
auto manyCaptions = false; auto manyCaptions = false;
for (const auto &item : items) { for (const auto &item : items) {
if (const auto media = item->media()) { if (const auto media = item->media()) {
if (media->photo()) { if (media->photo()) {
photoCount++; counts.photos++;
} else if (const auto document = media->document()) { } else if (const auto document = media->document()) {
(document->isVideoFile() (document->isVideoFile()
? videoCount ? counts.videos
: document->isAudioFile() : document->isAudioFile()
? audioCount ? counts.audios
: fileCount)++; : counts.files)++;
} }
auto copy = options; auto copy = options;
copy.ignoreGroup = true; copy.ignoreGroup = true;
@ -630,19 +740,7 @@ ItemPreview Media::toGroupPreview(
} }
} }
if (manyCaptions || result.text.text.isEmpty()) { if (manyCaptions || result.text.text.isEmpty()) {
const auto mediaCount = photoCount + videoCount; result.text = Ui::Text::Colorized(ComputeAlbumCountsString(counts));
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);
} }
if (!loadingContext.empty()) { if (!loadingContext.empty()) {
result.loadingContext = std::move(loadingContext); result.loadingContext = std::move(loadingContext);
@ -1851,14 +1949,15 @@ MediaInvoice::MediaInvoice(
.currency = data.currency, .currency = data.currency,
.title = data.title, .title = data.title,
.description = data.description, .description = data.description,
.extendedPreview = data.extendedPreview,
.extendedMedia = (data.extendedMedia
? data.extendedMedia->clone(parent)
: nullptr),
.photo = data.photo, .photo = data.photo,
.isPaidMedia = data.isPaidMedia,
.isTest = data.isTest, .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(); Ui::PreloadImageSpoiler();
} }
} }
@ -1894,9 +1993,89 @@ bool MediaInvoice::replyPreviewLoaded() const {
} }
TextWithEntities MediaInvoice::notificationText() 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 }; 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 { QString MediaInvoice::pinnedTextSubstring() const {
return QString::fromUtf8("\xC2\xAB") return QString::fromUtf8("\xC2\xAB")
+ _invoice.title + _invoice.title
@ -1917,7 +2096,7 @@ bool MediaInvoice::updateSentMedia(const MTPMessageMedia &media) {
bool MediaInvoice::updateExtendedMedia( bool MediaInvoice::updateExtendedMedia(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) { const QVector<MTPMessageExtendedMedia> &media) {
Expects(item == parent()); Expects(item == parent());
return UpdateExtendedMedia(_invoice, item, media); return UpdateExtendedMedia(_invoice, item, media);
@ -1927,15 +2106,15 @@ std::unique_ptr<HistoryView::Media> MediaInvoice::createView(
not_null<HistoryView::Element*> message, not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
HistoryView::Element *replacing) { HistoryView::Element *replacing) {
if (_invoice.extendedMedia) { if (_invoice.extendedMedia.size() == 1) {
return _invoice.extendedMedia->createView( return _invoice.extendedMedia.front()->createView(
message, message,
realParent, realParent,
replacing); replacing);
} else if (_invoice.extendedPreview) { } else if (!_invoice.extendedMedia.empty()) {
return std::make_unique<HistoryView::ExtendedPreview>( return std::make_unique<HistoryView::GroupedMedia>(
message, message,
&_invoice); _invoice.extendedMedia);
} }
return std::make_unique<HistoryView::Invoice>(message, &_invoice); 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; class Media;
struct Invoice { struct Invoice {
@ -105,11 +92,14 @@ struct Invoice {
QString currency; QString currency;
QString title; QString title;
TextWithEntities description; TextWithEntities description;
ExtendedPreview extendedPreview; std::vector<std::unique_ptr<Media>> extendedMedia;
std::unique_ptr<Media> extendedMedia;
PhotoData *photo = nullptr; PhotoData *photo = nullptr;
bool isPaidMedia = false;
bool isTest = false; bool isTest = false;
}; };
[[nodiscard]] bool HasExtendedMedia(const Invoice &invoice);
[[nodiscard]] bool HasUnpaidMedia(const Invoice &invoice);
[[nodiscard]] bool IsFirstVideo(const Invoice &invoice);
struct GiveawayStart { struct GiveawayStart {
std::vector<not_null<ChannelData*>> channels; std::vector<not_null<ChannelData*>> channels;
@ -207,7 +197,7 @@ public:
virtual bool updateSentMedia(const MTPMessageMedia &media) = 0; virtual bool updateSentMedia(const MTPMessageMedia &media) = 0;
virtual bool updateExtendedMedia( virtual bool updateExtendedMedia(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) { const QVector<MTPMessageExtendedMedia> &media) {
return false; return false;
} }
virtual std::unique_ptr<HistoryView::Media> createView( virtual std::unique_ptr<HistoryView::Media> createView(
@ -517,6 +507,7 @@ public:
Image *replyPreview() const override; Image *replyPreview() const override;
bool replyPreviewLoaded() const override; bool replyPreviewLoaded() const override;
TextWithEntities notificationText() const override; TextWithEntities notificationText() const override;
ItemPreview toPreview(ToPreviewOptions way) const override;
QString pinnedTextSubstring() const override; QString pinnedTextSubstring() const override;
TextForMimeData clipboardText() const override; TextForMimeData clipboardText() const override;
@ -524,7 +515,7 @@ public:
bool updateSentMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override;
bool updateExtendedMedia( bool updateExtendedMedia(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MTPMessageExtendedMedia &media) override; const QVector<MTPMessageExtendedMedia> &media) override;
std::unique_ptr<HistoryView::Media> createView( std::unique_ptr<HistoryView::Media> createView(
not_null<HistoryView::Element*> message, not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
@ -750,6 +741,9 @@ private:
[[nodiscard]] Invoice ComputeInvoiceData( [[nodiscard]] Invoice ComputeInvoiceData(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MTPDmessageMediaInvoice &data); const MTPDmessageMediaInvoice &data);
[[nodiscard]] Invoice ComputeInvoiceData(
not_null<HistoryItem*> item,
const MTPDmessageMediaPaidMedia &data);
[[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call); [[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call);

View file

@ -50,6 +50,38 @@ PhotoData::~PhotoData() {
base::take(_videoSizes); 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 { Data::Session &PhotoData::owner() const {
return *_owner; return *_owner;
} }
@ -74,6 +106,10 @@ void PhotoData::load(
load(PhotoSize::Large, origin, fromCloud, autoLoading); load(PhotoSize::Large, origin, fromCloud, autoLoading);
} }
TimeId PhotoData::date() const {
return _extendedMediaPreview ? 0 : _dateOrExtendedVideoDuration;
}
bool PhotoData::loading() const { bool PhotoData::loading() const {
return loading(PhotoSize::Large); return loading(PhotoSize::Large);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -666,6 +666,28 @@ Invoice ParseInvoice(const MTPDmessageMediaInvoice &data) {
return result; 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) { Poll ParsePoll(const MTPDmessageMediaPoll &data) {
auto result = Poll(); auto result = Poll();
data.vpoll().match([&](const MTPDpoll &poll) { data.vpoll().match([&](const MTPDpoll &poll) {
@ -1225,6 +1247,8 @@ Media ParseMedia(
result.content = ParseGiveaway(data); result.content = ParseGiveaway(data);
}, [&](const MTPDmessageMediaGiveawayResults &data) { }, [&](const MTPDmessageMediaGiveawayResults &data) {
// #TODO export giveaway // #TODO export giveaway
}, [&](const MTPDmessageMediaPaidMedia &data) {
result.content = ParsePaidMedia(context, data, folder, date);
}, [](const MTPDmessageMediaEmpty &data) {}); }, [](const MTPDmessageMediaEmpty &data) {});
return result; return result;
} }

View file

@ -182,6 +182,18 @@ struct Invoice {
int32 receiptMsgId = 0; 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 Poll {
struct Answer { struct Answer {
Utf8String text; Utf8String text;
@ -337,6 +349,7 @@ struct Media {
Invoice, Invoice,
Poll, Poll,
GiveawayStart, GiveawayStart,
PaidMedia,
UnsupportedMedia> content; UnsupportedMedia> content;
TimeId ttl = 0; TimeId ttl = 0;

View file

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

View file

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

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