diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml
index ca0c7bf097016..6de53f12d658e 100644
--- a/.github/workflows/win.yml
+++ b/.github/workflows/win.yml
@@ -169,6 +169,8 @@ jobs:
%TDESKTOP_BUILD_GENERATOR% ^
%TDESKTOP_BUILD_ARCH% ^
%TDESKTOP_BUILD_API% ^
+ -D CMAKE_C_FLAGS="/WX" ^
+ -D CMAKE_CXX_FLAGS="/WX" ^
-D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^
-D DESKTOP_APP_NO_PDB=ON ^
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4e2ac34c256ba..9393d81d723d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -57,14 +57,6 @@ include(cmake/validate_d3d_compiler.cmake)
include(cmake/target_prepare_qrc.cmake)
include(cmake/options.cmake)
-
-if (NOT DESKTOP_APP_USE_PACKAGED)
- if (WIN32)
- set(qt_version 5.15.13)
- elseif (APPLE)
- set(qt_version 6.2.8)
- endif()
-endif()
include(cmake/external/qt/package.cmake)
set(desktop_app_skip_libs
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 1517a952e5db1..6528a858e0ce6 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -126,6 +126,7 @@ PRIVATE
api/api_earn.h
api/api_editing.cpp
api/api_editing.h
+ api/api_filter_updates.h
api/api_global_privacy.cpp
api/api_global_privacy.h
api/api_hash.cpp
@@ -164,6 +165,10 @@ PRIVATE
api/api_single_message_search.h
api/api_statistics.cpp
api/api_statistics.h
+ api/api_statistics_data_deserialize.cpp
+ api/api_statistics_data_deserialize.h
+ api/api_statistics_sender.cpp
+ api/api_statistics_sender.h
api/api_text_entities.cpp
api/api_text_entities.h
api/api_toggling_media.cpp
@@ -722,8 +727,6 @@ PRIVATE
history/view/media/history_view_dice.h
history/view/media/history_view_document.cpp
history/view/media/history_view_document.h
- history/view/media/history_view_extended_preview.cpp
- history/view/media/history_view_extended_preview.h
history/view/media/history_view_file.cpp
history/view/media/history_view_file.h
history/view/media/history_view_game.cpp
@@ -887,6 +890,10 @@ PRIVATE
history/history_view_highlight_manager.h
history/history_widget.cpp
history/history_widget.h
+ info/bot/earn/info_bot_earn_list.cpp
+ info/bot/earn/info_bot_earn_list.h
+ info/bot/earn/info_bot_earn_widget.cpp
+ info/bot/earn/info_bot_earn_widget.h
info/channel_statistics/boosts/create_giveaway_box.cpp
info/channel_statistics/boosts/create_giveaway_box.h
info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp
@@ -895,10 +902,10 @@ PRIVATE
info/channel_statistics/boosts/info_boosts_inner_widget.h
info/channel_statistics/boosts/info_boosts_widget.cpp
info/channel_statistics/boosts/info_boosts_widget.h
- info/channel_statistics/earn/info_earn_inner_widget.cpp
- info/channel_statistics/earn/info_earn_inner_widget.h
- info/channel_statistics/earn/info_earn_widget.cpp
- info/channel_statistics/earn/info_earn_widget.h
+ info/channel_statistics/earn/info_channel_earn_list.cpp
+ info/channel_statistics/earn/info_channel_earn_list.h
+ info/channel_statistics/earn/info_channel_earn_widget.cpp
+ info/channel_statistics/earn/info_channel_earn_widget.h
info/common_groups/info_common_groups_inner_widget.cpp
info/common_groups/info_common_groups_inner_widget.h
info/common_groups/info_common_groups_widget.cpp
@@ -1539,6 +1546,8 @@ PRIVATE
window/window_peer_menu.cpp
window/window_peer_menu.h
window/window_section_common.h
+ window/window_separate_id.cpp
+ window/window_separate_id.h
window/window_session_controller.cpp
window/window_session_controller.h
window/window_session_controller_link_info.h
@@ -1828,12 +1837,44 @@ if (WIN32)
/DELAYLOAD:uxtheme.dll
/DELAYLOAD:crypt32.dll
/DELAYLOAD:bcrypt.dll
- /DELAYLOAD:imm32.dll
/DELAYLOAD:netapi32.dll
+ /DELAYLOAD:imm32.dll
/DELAYLOAD:userenv.dll
/DELAYLOAD:wtsapi32.dll
/DELAYLOAD:propsys.dll
)
+ if (QT_VERSION GREATER 6)
+ target_link_options(Telegram
+ PRIVATE
+ /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll
+ /DELAYLOAD:API-MS-Win-Core-File-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-LibraryLoader-l1-2-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Localization-l1-2-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Memory-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Memory-l1-1-1.dll
+ /DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
+ /DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll
+ /DELAYLOAD:authz.dll # Authz.lib
+ /DELAYLOAD:comdlg32.dll
+ /DELAYLOAD:dwrite.dll # DWrite.lib
+ /DELAYLOAD:dxgi.dll # DXGI.lib
+ /DELAYLOAD:d3d9.dll # D3D9.lib
+ /DELAYLOAD:d3d11.dll # D3D11.lib
+ /DELAYLOAD:d3d12.dll # D3D12.lib
+ /DELAYLOAD:setupapi.dll # SetupAPI.lib
+ /DELAYLOAD:winhttp.dll
+ )
+ endif()
endif()
target_prepare_qrc(Telegram)
diff --git a/Telegram/Resources/icons/menu/passcode_finger.png b/Telegram/Resources/icons/menu/passcode_finger.png
new file mode 100644
index 0000000000000..ee267f7089135
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_finger.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_finger@2x.png b/Telegram/Resources/icons/menu/passcode_finger@2x.png
new file mode 100644
index 0000000000000..a0a34240bfb0e
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_finger@2x.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_finger@3x.png b/Telegram/Resources/icons/menu/passcode_finger@3x.png
new file mode 100644
index 0000000000000..291fc8eaff151
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_finger@3x.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_watch.png b/Telegram/Resources/icons/menu/passcode_watch.png
new file mode 100644
index 0000000000000..161da4e870802
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_watch.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_watch@2x.png b/Telegram/Resources/icons/menu/passcode_watch@2x.png
new file mode 100644
index 0000000000000..516e313b7f5d1
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_watch@2x.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_watch@3x.png b/Telegram/Resources/icons/menu/passcode_watch@3x.png
new file mode 100644
index 0000000000000..2b5f71f725d7b
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_watch@3x.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_winhello.png b/Telegram/Resources/icons/menu/passcode_winhello.png
new file mode 100644
index 0000000000000..e3effa23a67a5
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_winhello.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_winhello@2x.png b/Telegram/Resources/icons/menu/passcode_winhello@2x.png
new file mode 100644
index 0000000000000..32bc819f5fa58
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_winhello@2x.png differ
diff --git a/Telegram/Resources/icons/menu/passcode_winhello@3x.png b/Telegram/Resources/icons/menu/passcode_winhello@3x.png
new file mode 100644
index 0000000000000..6ac7445c185bb
Binary files /dev/null and b/Telegram/Resources/icons/menu/passcode_winhello@3x.png differ
diff --git a/Telegram/Resources/icons/payments/small_star.png b/Telegram/Resources/icons/payments/small_star.png
new file mode 100644
index 0000000000000..94e3fc4c5adfa
Binary files /dev/null and b/Telegram/Resources/icons/payments/small_star.png differ
diff --git a/Telegram/Resources/icons/payments/small_star@2x.png b/Telegram/Resources/icons/payments/small_star@2x.png
new file mode 100644
index 0000000000000..4dc7016fe9f52
Binary files /dev/null and b/Telegram/Resources/icons/payments/small_star@2x.png differ
diff --git a/Telegram/Resources/icons/payments/small_star@3x.png b/Telegram/Resources/icons/payments/small_star@3x.png
new file mode 100644
index 0000000000000..8f7ae06daa197
Binary files /dev/null and b/Telegram/Resources/icons/payments/small_star@3x.png differ
diff --git a/Telegram/Resources/icons/settings/premium/effects.png b/Telegram/Resources/icons/settings/premium/effects.png
new file mode 100644
index 0000000000000..56af6306310d2
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/effects.png differ
diff --git a/Telegram/Resources/icons/settings/premium/effects@2x.png b/Telegram/Resources/icons/settings/premium/effects@2x.png
new file mode 100644
index 0000000000000..5f5e4ada62765
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/effects@2x.png differ
diff --git a/Telegram/Resources/icons/settings/premium/effects@3x.png b/Telegram/Resources/icons/settings/premium/effects@3x.png
new file mode 100644
index 0000000000000..30ec7d9eab70d
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/effects@3x.png differ
diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png
new file mode 100644
index 0000000000000..06a33dc3c0897
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png differ
diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png
new file mode 100644
index 0000000000000..996851dbc9f57
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png differ
diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png
new file mode 100644
index 0000000000000..0863b33e8df56
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fa1aab1b885c2..5c461c1efdb7a 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -683,6 +683,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_privacy_premium_link" = "Telegram Premium";
"lng_settings_passcode_disable" = "Disable Passcode";
"lng_settings_passcode_disable_sure" = "Are you sure you want to disable passcode?";
+"lng_settings_use_winhello" = "Unlock with Windows Hello";
+"lng_settings_use_winhello_about" = "You need to enter your passcode once before unlocking Telegram with Windows Hello.";
+"lng_settings_use_touchid" = "Unlock with Touch ID";
+"lng_settings_use_touchid_about" = "You need to enter your passcode once before unlocking Telegram with Touch ID.";
+"lng_settings_use_applewatch" = "Unlock with Apple Watch";
+"lng_settings_use_applewatch_about" = "You need to enter your passcode once before unlocking Telegram with Apple Watch.";
+"lng_settings_use_systempwd" = "Unlock with System Password";
+"lng_settings_use_systempwd_about" = "You need to enter your passcode once before unlocking Telegram with System Password.";
"lng_settings_password_disable" = "Disable Cloud Password";
"lng_settings_password_abort" = "Abort two-step verification setup";
"lng_settings_about_bio" = "Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco";
@@ -897,6 +905,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_gift_premium_users_confirm" = "Proceed";
"lng_settings_gift_premium_users_error#one" = "You can select maximum {count} user.";
"lng_settings_gift_premium_users_error#other" = "You can select maximum {count} users.";
+"lng_settings_gift_premium_choose" = "Please choose at least one recipient.";
"lng_backgrounds_header" = "Choose Wallpaper";
"lng_theme_sure_keep" = "Keep this theme?";
@@ -991,6 +1000,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_passcode_ph" = "Your passcode";
"lng_passcode_submit" = "Submit";
"lng_passcode_logout" = "Log out";
+"lng_passcode_winhello" = "You need to enter your passcode\nbefore you can use Windows Hello.";
+"lng_passcode_touchid" = "You need to enter your passcode\nbefore you can use Touch ID.";
+"lng_passcode_applewatch" = "You need to enter your passcode\nbefore you can use Watch to unlock.";
+"lng_passcode_systempwd" = "You need to enter your passcode\nbefore you can use system password.";
+"lng_passcode_winhello_unlock" = "Telegram wants to unlock with Windows Hello.";
+"lng_passcode_touchid_unlock" = "unlock";
"lng_passcode_create_button" = "Save Passcode";
"lng_passcode_check_button" = "Submit";
"lng_passcode_change_button" = "Save Passcode";
@@ -1100,6 +1115,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_faq" = "Telegram FAQ";
"lng_settings_faq_link" = "https://telegram.org/faq#general-questions";
"lng_settings_features" = "Telegram Features";
+"lng_settings_credits" = "Your Stars";
"lng_settings_logout" = "Log Out";
"lng_sure_logout" = "Are you sure you want to log out?";
@@ -1560,6 +1576,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_bot_public_link" = "Public Link";
"lng_manage_peer_bot_public_links" = "Public Links";
+"lng_manage_peer_bot_balance" = "Balance";
"lng_manage_peer_bot_edit_intro" = "Edit Intro";
"lng_manage_peer_bot_edit_commands" = "Edit Commands";
"lng_manage_peer_bot_edit_settings" = "Change Bot Settings";
@@ -1872,6 +1889,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_boost_apply#one" = "{from} boosted the group";
"lng_action_boost_apply#other" = "{from} boosted the group {count} times";
"lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?";
+"lng_action_payment_refunded" = "{peer} refunded back {amount}";
"lng_similar_channels_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all";
@@ -2185,6 +2203,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_translation" = "Real-time translation of channels and chats into other languages.";
"lng_premium_summary_subtitle_business" = "Telegram Business";
"lng_premium_summary_about_business" = "Upgrade your account with business features such as location, opening hours and quick replies.";
+"lng_premium_summary_subtitle_effects" = "Message Effects";
+"lng_premium_summary_about_effects" = "Add over 500 animated effects to private messages.";
"lng_premium_summary_bottom_subtitle" = "About Telegram Premium";
"lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
"lng_premium_summary_button" = "Subscribe for {cost} per month";
@@ -2325,15 +2345,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_out_title" = "Confirm Your Purchase";
"lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?";
"lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?";
+"lng_credits_box_out_media#one" = "Do you want to unlock {media} in {chat} for **{count} Star**?";
+"lng_credits_box_out_media#other" = "Do you want to unlock {media} in {chat} for **{count} Stars**?";
+"lng_credits_box_out_photo" = "a photo";
+"lng_credits_box_out_photos#one" = "{count} photo";
+"lng_credits_box_out_photos#other" = "{count} photos";
+"lng_credits_box_out_video" = "a video";
+"lng_credits_box_out_videos#one" = "{count} video";
+"lng_credits_box_out_videos#other" = "{count} videos";
+"lng_credits_box_out_both" = "{photo} and {video}";
"lng_credits_box_out_confirm#one" = "Confirm and Pay {emoji} {count} Star";
"lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars";
"lng_credits_box_out_about" = "Review the {link} for Stars.";
+"lng_credits_media_done_title" = "Media Unlocked";
+"lng_credits_media_done_text#one" = "**{count} Star** transferred to {chat}.";
+"lng_credits_media_done_text#other" = "**{count} Stars** transferred to {chat}.";
"lng_credits_summary_in_toast_title" = "Stars Acquired";
"lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance.";
"lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance.";
"lng_credits_box_history_entry_peer" = "Recipient";
+"lng_credits_box_history_entry_peer_in" = "From";
+"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_premium_bot" = "Stars Top-Up";
+"lng_credits_box_history_entry_via_premium_bot" = "Premium Bot";
"lng_credits_box_history_entry_id" = "Transaction ID";
"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard.";
+"lng_credits_box_history_entry_success_date" = "Transaction date";
+"lng_credits_box_history_entry_success_url" = "Transaction link";
+"lng_credits_box_history_entry_media" = "Media";
"lng_credits_box_history_entry_about" = "You can dispute this transaction {link}.";
"lng_credits_box_history_entry_about_link" = "here";
"lng_credits_small_balance_title#one" = "{count} Star Needed";
@@ -2772,12 +2815,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_badge" = "x{amount}";
"lng_prizes_results_title" = "Winners Selected!";
+"lng_prizes_results_title_one" = "Winner Selected!";
"lng_prizes_results_about#one" = "**{count}** winner of the {link} was randomly selected by Telegram.";
"lng_prizes_results_about#other" = "**{count}** winners of the {link} were randomly selected by Telegram.";
"lng_prizes_results_link" = "Giveaway";
+"lng_prizes_results_winner" = "Winner";
"lng_prizes_results_winners" = "Winners";
"lng_prizes_results_more#one" = "and {count} more!";
"lng_prizes_results_more#other" = "and {count} more!";
+"lng_prizes_results_one" = "The winner received their gift link in a private message.";
"lng_prizes_results_all" = "All winners received gift links in private messages.";
"lng_prizes_results_some" = "Some winners couldn't be selected.";
@@ -3305,6 +3351,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler";
+"lng_context_make_paid" = "Make This Content Paid";
+"lng_context_change_price" = "Change Price";
"lng_factcheck_title" = "Fact Check";
"lng_factcheck_placeholder" = "Add Facts or Context";
@@ -3316,6 +3364,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_factcheck_bottom" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation.";
"lng_factcheck_links" = "Only **t.me/** links are allowed.";
+"lng_paid_title" = "Paid Content";
+"lng_paid_enter_cost" = "Enter Unlock Cost";
+"lng_paid_cost_placeholder" = "Stars to Unlock";
+"lng_paid_about" = "Users will have to transfer this amount of Stars to your channel in order to view this media. {link}";
+"lng_paid_about_link" = "More about stars >";
+"lng_paid_about_link_url" = "https://telegram.org/blog/telegram-stars";
+"lng_paid_price" = "Unlock for {price}";
+
"lng_translate_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}";
"lng_translate_bar_to_other" = "Translate to {name}";
@@ -3582,6 +3638,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_formatting_link_text" = "Text";
"lng_formatting_link_url" = "URL";
"lng_formatting_link_create" = "Create";
+"lng_formatting_code_title" = "Code Language";
+"lng_formatting_code_language" = "Language for syntax highlighting.";
+"lng_formatting_code_auto" = "Auto-Detect";
"lng_text_copied" = "Text copied to clipboard.";
"lng_code_copied" = "Block copied to clipboard.";
@@ -4132,6 +4191,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_restricted_send_polls_all" = "Posting polls isn't allowed in this group.";
"lng_restricted_send_public_polls" = "Sorry, public polls can't be forwarded to channels.";
+"lng_restricted_send_paid_media" = "Sorry, paid media can't be sent to this channel.";
"lng_restricted_send_voice_messages" = "{user} restricted sending of voice messages to them.";
"lng_restricted_send_video_messages" = "{user} restricted sending of video messages to them.";
@@ -5159,6 +5219,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channel_earn_history_return" = "Refund";
"lng_channel_earn_history_return_about" = "Refunded back";
"lng_channel_earn_history_pending" = "Pending";
+"lng_channel_earn_history_failed" = "Failed";
"lng_channel_earn_history_show_more#one" = "Show {count} More Transaction";
"lng_channel_earn_history_show_more#other" = "Show {count} More Transactions";
"lng_channel_earn_off" = "Switch Off Ads";
@@ -5181,6 +5242,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channel_earn_chart_revenue" = "Ad revenue";
"lng_channel_earn_chart_overriden_detail_currency" = "Revenue in TON";
"lng_channel_earn_chart_overriden_detail_usd" = "Revenue in USD";
+"lng_channel_earn_currency_history" = "TON Transactions";
+"lng_channel_earn_credits_history" = "Stars Transactions";
+"lng_channel_earn_out_check_password_about" = "You can withdraw only if you have:";
+
+"lng_bot_earn_title" = "Stars Balance";
+"lng_bot_earn_chart_revenue" = "Revenue";
+"lng_bot_earn_overview_title" = "Proceeds overview";
+"lng_bot_earn_available" = "Available balance";
+"lng_bot_earn_total" = "Total lifetime proceeds";
+"lng_bot_earn_balance_title" = "Available balance";
+"lng_bot_earn_balance_about" = "Stars from your total balance become available for spending on ads and rewards 21 days after they are earned.";
+"lng_bot_earn_balance_about_url" = "https://telegram.org/tos/stars";
+"lng_bot_earn_balance_button#one" = "Withdraw {emoji} {count}";
+"lng_bot_earn_balance_button#other" = "Withdraw {emoji} {count}";
+"lng_bot_earn_balance_button_all" = "Withdraw all stars";
+"lng_bot_earn_balance_button_locked" = "Withdraw";
+"lng_bot_earn_balance_button_buy_ads" = "Buy Ads";
+"lng_bot_earn_learn_credits_out_about" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. {link}";
+"lng_bot_earn_out_ph" = "Enter amount to withdraw";
+"lng_bot_earn_balance_password_title" = "Two-step verification";
+"lng_bot_earn_balance_password_description" = "Please enter your password to collect.";
+"lng_bot_earn_credits_out_minimal" = "You cannot withdraw less then {link}.";
+"lng_bot_earn_credits_out_minimal_link#one" = "{count} star";
+"lng_bot_earn_credits_out_minimal_link#other" = "{count} stars";
"lng_contact_add" = "Add";
"lng_contact_send_message" = "message";
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 68f8a47f7147a..46918013d2ffb 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="5.2.3.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index d9a60a870f757..c47677eb21751 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,1,5,0
- PRODUCTVERSION 5,1,5,0
+ FILEVERSION 5,2,3,0
+ PRODUCTVERSION 5,2,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
- VALUE "FileVersion", "5.1.5.0"
+ VALUE "FileVersion", "5.2.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
- VALUE "ProductVersion", "5.1.5.0"
+ VALUE "ProductVersion", "5.2.3.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index afbc675ba79f7..5594959fe574c 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,1,5,0
- PRODUCTVERSION 5,1,5,0
+ FILEVERSION 5,2,3,0
+ PRODUCTVERSION 5,2,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
- VALUE "FileVersion", "5.1.5.0"
+ VALUE "FileVersion", "5.2.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
- VALUE "ProductVersion", "5.1.5.0"
+ VALUE "ProductVersion", "5.2.3.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/_other/updater_win.cpp b/Telegram/SourceFiles/_other/updater_win.cpp
index 0b9b247853279..e779c8f20fcfe 100644
--- a/Telegram/SourceFiles/_other/updater_win.cpp
+++ b/Telegram/SourceFiles/_other/updater_win.cpp
@@ -571,8 +571,8 @@ void _generateDump(EXCEPTION_POINTERS* pExceptionPointers) {
}
if (!hDumpFile || hDumpFile == INVALID_HANDLE_VALUE) {
WCHAR wstrPath[maxFileLen];
- DWORD wstrPathLen;
- if (wstrPathLen = GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen)) {
+ DWORD wstrPathLen = GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen);
+ if (wstrPathLen) {
wsprintf(wstrPath + wstrPathLen, L"\\%s\\", _programName);
hDumpFile = _generateDumpFileAtPath(wstrPath);
}
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index cf4611563ae0a..9290512611391 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -127,11 +127,7 @@ void SendBotCallbackData(
UrlClickHandler::Open(link);
return;
}
- const auto scoreLink = AppendShareGameScoreUrl(
- session,
- link,
- item->fullId());
- BotGameUrlClickHandler(bot, scoreLink).onClick({
+ BotGameUrlClickHandler(bot, link).onClick({
Qt::LeftButton,
QVariant::fromValue(ClickHandlerContext{
.itemId = item->fullId(),
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index efd92a9bcb443..cd8aa54e2d7fa 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -20,6 +20,7 @@ namespace Api {
inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
struct SendOptions {
+ uint64 price = 0;
PeerData *sendAs = nullptr;
TimeId scheduled = 0;
BusinessShortcutId shortcutId = 0;
diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp
index 44d274e5862d4..356e2cffaf2ba 100644
--- a/Telegram/SourceFiles/api/api_credits.cpp
+++ b/Telegram/SourceFiles/api/api_credits.cpp
@@ -7,9 +7,12 @@ For license and copyright information please follow this link:
*/
#include "api/api_credits.h"
-#include "apiwrap.h"
+#include "api/api_statistics_data_deserialize.h"
#include "api/api_updates.h"
+#include "apiwrap.h"
#include "base/unixtime.h"
+#include "data/data_channel.h"
+#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
@@ -20,25 +23,62 @@ For license and copyright information please follow this link:
namespace Api {
namespace {
+constexpr auto kTransactionsLimit = 100;
+
[[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL(
const MTPStarsTransaction &tl,
not_null peer) {
using HistoryPeerTL = MTPDstarsTransactionPeer;
+ using namespace Data;
+ const auto owner = &peer->owner();
const auto photo = tl.data().vphoto()
- ? peer->owner().photoFromWeb(*tl.data().vphoto(), ImageLocation())
+ ? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())
: nullptr;
+ auto extended = std::vector();
+ if (const auto list = tl.data().vextended_media()) {
+ extended.reserve(list->v.size());
+ for (const auto &media : list->v) {
+ media.match([&](const MTPDmessageMediaPhoto &photo) {
+ if (const auto inner = photo.vphoto()) {
+ const auto photo = owner->processPhoto(*inner);
+ if (!photo->isNull()) {
+ extended.push_back(CreditsHistoryMedia{
+ .type = CreditsHistoryMediaType::Photo,
+ .id = photo->id,
+ });
+ }
+ }
+ }, [&](const MTPDmessageMediaDocument &document) {
+ if (const auto inner = document.vdocument()) {
+ const auto document = owner->processDocument(*inner);
+ if (document->isAnimation()
+ || document->isVideoFile()
+ || document->isGifv()) {
+ extended.push_back(CreditsHistoryMedia{
+ .type = CreditsHistoryMediaType::Video,
+ .id = document->id,
+ });
+ }
+ }
+ }, [&](const auto &) {});
+ }
+ }
+ const auto barePeerId = tl.data().vpeer().match([](
+ const HistoryPeerTL &p) {
+ return peerFromMTP(p.vpeer());
+ }, [](const auto &) {
+ return PeerId(0);
+ }).value;
return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()),
.description = qs(tl.data().vdescription().value_or_empty()),
.date = base::unixtime::parse(tl.data().vdate().v),
.photoId = photo ? photo->id : 0,
+ .extended = std::move(extended),
.credits = tl.data().vstars().v,
- .bareId = tl.data().vpeer().match([](const HistoryPeerTL &p) {
- return peerFromMTP(p.vpeer());
- }, [](const auto &) {
- return PeerId(0);
- }).value,
+ .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),
+ .barePeerId = barePeerId,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
@@ -51,8 +91,17 @@ namespace {
return Data::CreditsHistoryEntry::PeerType::Unsupported;
}, [](const MTPDstarsTransactionPeerPremiumBot &) {
return Data::CreditsHistoryEntry::PeerType::PremiumBot;
+ }, [](const MTPDstarsTransactionPeerAds &) {
+ return Data::CreditsHistoryEntry::PeerType::Ads;
}),
.refunded = tl.data().is_refund(),
+ .pending = tl.data().is_pending(),
+ .failed = tl.data().is_failed(),
+ .successDate = tl.data().vtransaction_date()
+ ? base::unixtime::parse(tl.data().vtransaction_date()->v)
+ : QDateTime(),
+ .successLink = qs(tl.data().vtransaction_url().value_or_empty()),
+ .in = (int64(tl.data().vstars().v) >= 0),
};
}
@@ -152,7 +201,8 @@ void CreditsHistory::request(
_requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
- MTP_string(token)
+ MTP_string(token),
+ MTP_int(kTransactionsLimit)
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
@@ -199,4 +249,58 @@ rpl::producer> PremiumPeerBot(
};
}
+CreditsEarnStatistics::CreditsEarnStatistics(not_null peer)
+: StatisticsRequestSender(peer)
+, _isUser(peer->isUser()) {
+}
+
+rpl::producer 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
diff --git a/Telegram/SourceFiles/api/api_credits.h b/Telegram/SourceFiles/api/api_credits.h
index 265e7b3871076..e633807344ac9 100644
--- a/Telegram/SourceFiles/api/api_credits.h
+++ b/Telegram/SourceFiles/api/api_credits.h
@@ -7,13 +7,17 @@ For license and copyright information please follow this link:
*/
#pragma once
+#include "api/api_statistics_sender.h"
#include "data/data_credits.h"
+#include "data/data_credits_earn.h"
#include "mtproto/sender.h"
namespace Main {
class Session;
} // namespace Main
+class UserData;
+
namespace Api {
class CreditsTopupOptions final {
@@ -68,6 +72,21 @@ class CreditsHistory final {
};
+class CreditsEarnStatistics final : public StatisticsRequestSender {
+public:
+ explicit CreditsEarnStatistics(not_null);
+
+ [[nodiscard]] rpl::producer request();
+ [[nodiscard]] Data::CreditsEarnStatistics data() const;
+
+private:
+ Data::CreditsEarnStatistics _data;
+ bool _isUser = false;
+
+ mtpRequestId _requestId = 0;
+
+};
+
[[nodiscard]] rpl::producer> PremiumPeerBot(
not_null session);
diff --git a/Telegram/SourceFiles/api/api_earn.cpp b/Telegram/SourceFiles/api/api_earn.cpp
index d6425ef69b738..f05e5f6188193 100644
--- a/Telegram/SourceFiles/api/api_earn.cpp
+++ b/Telegram/SourceFiles/api/api_earn.cpp
@@ -9,6 +9,7 @@ For license and copyright information please follow this link:
#include "api/api_cloud_password.h"
#include "apiwrap.h"
+#include "ui/layers/generic_box.h"
#include "boxes/passcode_box.h"
#include "data/data_channel.h"
#include "data/data_session.h"
@@ -34,22 +35,33 @@ void RestrictSponsored(
}
void HandleWithdrawalButton(
- not_null channel,
+ RewardReceiver receiver,
not_null button,
std::shared_ptr show) {
+ Expects(receiver.currencyReceiver
+ || (receiver.creditsReceiver && receiver.creditsAmount));
struct State {
rpl::lifetime lifetime;
bool loading = false;
};
+ const auto channel = receiver.currencyReceiver;
+ const auto peer = receiver.creditsReceiver;
+
const auto state = button->lifetime().make_state();
- const auto session = &channel->session();
+ const auto session = (channel ? &channel->session() : &peer->session());
+
+ using ChannelOutUrl = MTPstats_BroadcastRevenueWithdrawalUrl;
+ using CreditsOutUrl = MTPpayments_StarsRevenueWithdrawalUrl;
session->api().cloudPassword().reload();
- button->setClickedCallback([=] {
+ const auto processOut = [=] {
if (state->loading) {
return;
}
+ if (peer && !receiver.creditsAmount()) {
+ return;
+ }
state->loading = true;
state->lifetime = session->api().cloudPassword().state(
) | rpl::take(
@@ -58,10 +70,12 @@ void HandleWithdrawalButton(
state->loading = false;
auto fields = PasscodeBox::CloudFields::From(pass);
- fields.customTitle
- = tr::lng_channel_earn_balance_password_title();
- fields.customDescription
- = tr::lng_channel_earn_balance_password_description(tr::now);
+ fields.customTitle = channel
+ ? tr::lng_channel_earn_balance_password_title()
+ : tr::lng_bot_earn_balance_password_title();
+ fields.customDescription = channel
+ ? tr::lng_channel_earn_balance_password_description(tr::now)
+ : tr::lng_bot_earn_balance_password_description(tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = crl::guard(button, [=](
const Core::CloudPasswordResult &result,
@@ -74,22 +88,63 @@ void HandleWithdrawalButton(
}
}
};
- const auto fail = [=](const QString &error) {
- show->showToast(error);
+ const auto fail = [=](const MTP::Error &error) {
+ show->showToast(error.type());
};
- session->api().request(
- MTPstats_GetBroadcastRevenueWithdrawalUrl(
- channel->inputChannel,
- result.result
- )).done([=](const MTPstats_BroadcastRevenueWithdrawalUrl &r) {
- done(qs(r.data().vurl()));
- }).fail([=](const MTP::Error &error) {
- fail(error.type());
- }).send();
+ if (channel) {
+ session->api().request(
+ MTPstats_GetBroadcastRevenueWithdrawalUrl(
+ channel->inputChannel,
+ result.result
+ )).done([=](const ChannelOutUrl &r) {
+ done(qs(r.data().vurl()));
+ }).fail(fail).send();
+ } else if (peer) {
+ session->api().request(
+ MTPpayments_GetStarsRevenueWithdrawalUrl(
+ peer->input,
+ MTP_long(receiver.creditsAmount()),
+ result.result
+ )).done([=](const CreditsOutUrl &r) {
+ done(qs(r.data().vurl()));
+ }).fail(fail).send();
+ }
});
show->show(Box(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::max()),
+ MTP_inputCheckPasswordEmpty()
+ )).fail(fail).send();
+ }
});
}
diff --git a/Telegram/SourceFiles/api/api_earn.h b/Telegram/SourceFiles/api/api_earn.h
index 93f2bf6eb41a0..cbee5d25ab03a 100644
--- a/Telegram/SourceFiles/api/api_earn.h
+++ b/Telegram/SourceFiles/api/api_earn.h
@@ -21,8 +21,14 @@ void RestrictSponsored(
bool restricted,
Fn failed);
+struct RewardReceiver final {
+ ChannelData *currencyReceiver = nullptr;
+ PeerData *creditsReceiver = nullptr;
+ Fn creditsAmount;
+};
+
void HandleWithdrawalButton(
- not_null channel,
+ RewardReceiver receiver,
not_null button,
std::shared_ptr show);
diff --git a/Telegram/SourceFiles/api/api_filter_updates.h b/Telegram/SourceFiles/api/api_filter_updates.h
new file mode 100644
index 0000000000000..0126d37364642
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_filter_updates.h
@@ -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
+void PerformForUpdate(
+ const MTPUpdates &updates,
+ Fn 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
diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp
index ce1966623cd2b..869d0bf8ad0d3 100644
--- a/Telegram/SourceFiles/api/api_statistics.cpp
+++ b/Telegram/SourceFiles/api/api_statistics.cpp
@@ -7,6 +7,7 @@ For license and copyright information please follow this link:
*/
#include "api/api_statistics.h"
+#include "api/api_statistics_data_deserialize.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
@@ -15,33 +16,10 @@ For license and copyright information please follow this link:
#include "data/data_story.h"
#include "history/history.h"
#include "main/main_session.h"
-#include "statistics/statistics_data_deserialize.h"
namespace Api {
namespace {
-constexpr auto kCheckRequestsTimer = 10 * crl::time(1000);
-
-[[nodiscard]] Data::StatisticalGraph StatisticalGraphFromTL(
- const MTPStatsGraph &tl) {
- return tl.match([&](const MTPDstatsGraph &d) {
- using namespace Statistic;
- const auto zoomToken = d.vzoom_token().has_value()
- ? qs(*d.vzoom_token()).toUtf8()
- : QByteArray();
- return Data::StatisticalGraph{
- StatisticalChartFromJSON(qs(d.vjson().data().vdata()).toUtf8()),
- zoomToken,
- };
- }, [&](const MTPDstatsGraphAsync &data) {
- return Data::StatisticalGraph{
- .zoomToken = qs(data.vtoken()).toUtf8(),
- };
- }, [&](const MTPDstatsGraphError &data) {
- return Data::StatisticalGraph{ .error = qs(data.verror()) };
- });
-}
-
[[nodiscard]] Data::StatisticalValue StatisticalValueFromTL(
const MTPStatsAbsValueAndPrev &tl) {
const auto current = tl.data().vcurrent().v;
@@ -223,61 +201,6 @@ Statistics::Statistics(not_null channel)
: StatisticsRequestSender(channel) {
}
-StatisticsRequestSender::StatisticsRequestSender(not_null 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
-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)
- ).toDC(
- dcId ? MTP::ShiftDcId(dcId, MTP::kStatsDcShift) : 0
- ).overrideId(id));
-}
-
rpl::producer Statistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
@@ -747,11 +670,11 @@ Data::BoostStatus Boosts::boostStatus() const {
return _boostStatus;
}
-EarnStatistics::EarnStatistics(not_null channel)
+ChannelEarnStatistics::ChannelEarnStatistics(not_null channel)
: StatisticsRequestSender(channel) {
}
-rpl::producer EarnStatistics::request() {
+rpl::producer ChannelEarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
@@ -795,7 +718,7 @@ rpl::producer EarnStatistics::request() {
};
}
-void EarnStatistics::requestHistory(
+void ChannelEarnStatistics::requestHistory(
const Data::EarnHistorySlice::OffsetToken &token,
Fn done) {
if (_requestId) {
@@ -865,7 +788,7 @@ void EarnStatistics::requestHistory(
}).send();
}
-Data::EarnStatistics EarnStatistics::data() const {
+Data::EarnStatistics ChannelEarnStatistics::data() const {
return _data;
}
diff --git a/Telegram/SourceFiles/api/api_statistics.h b/Telegram/SourceFiles/api/api_statistics.h
index f18cba71dc960..213ab92933b6b 100644
--- a/Telegram/SourceFiles/api/api_statistics.h
+++ b/Telegram/SourceFiles/api/api_statistics.h
@@ -7,45 +7,16 @@ For license and copyright information please follow this link:
*/
#pragma once
-#include "base/timer.h"
+#include "api/api_statistics_sender.h"
#include "data/data_boosts.h"
#include "data/data_channel_earn.h"
#include "data/data_statistics.h"
-#include "mtproto/sender.h"
class ChannelData;
class PeerData;
namespace Api {
-class StatisticsRequestSender {
-protected:
- explicit StatisticsRequestSender(not_null channel);
- ~StatisticsRequestSender();
-
- template <
- typename Request,
- typename = std::enable_if_t>,
- typename = typename Request::Unboxed>
- [[nodiscard]] auto makeRequest(Request &&request);
-
- [[nodiscard]] MTP::Sender &api() {
- return _api;
- }
- [[nodiscard]] not_null channel() {
- return _channel;
- }
-
-private:
- void checkRequests();
-
- const not_null _channel;
- MTP::Sender _api;
- base::Timer _timer;
- base::flat_map> _requests;
-
-};
-
class Statistics final : public StatisticsRequestSender {
public:
explicit Statistics(not_null channel);
@@ -108,9 +79,9 @@ class MessageStatistics final : public StatisticsRequestSender {
};
-class EarnStatistics final : public StatisticsRequestSender {
+class ChannelEarnStatistics final : public StatisticsRequestSender {
public:
- explicit EarnStatistics(not_null channel);
+ explicit ChannelEarnStatistics(not_null channel);
[[nodiscard]] rpl::producer request();
void requestHistory(
diff --git a/Telegram/SourceFiles/api/api_statistics_data_deserialize.cpp b/Telegram/SourceFiles/api/api_statistics_data_deserialize.cpp
new file mode 100644
index 0000000000000..9ce75af7858b5
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_statistics_data_deserialize.cpp
@@ -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
diff --git a/Telegram/SourceFiles/api/api_statistics_data_deserialize.h b/Telegram/SourceFiles/api/api_statistics_data_deserialize.h
new file mode 100644
index 0000000000000..385b99d17d31d
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_statistics_data_deserialize.h
@@ -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
diff --git a/Telegram/SourceFiles/api/api_statistics_sender.cpp b/Telegram/SourceFiles/api/api_statistics_sender.cpp
new file mode 100644
index 0000000000000..21d068e3d7453
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_statistics_sender.cpp
@@ -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 peer)
+: _peer(peer)
+, _channel(peer->asChannel())
+, _user(peer->asUser())
+, _api(&_peer->session().api().instance())
+, _timer([=] { checkRequests(); }) {
+}
+
+MTP::Sender &StatisticsRequestSender::api() {
+ return _api;
+}
+
+not_null StatisticsRequestSender::channel() {
+ Expects(_channel);
+ return _channel;
+}
+
+not_null 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
diff --git a/Telegram/SourceFiles/api/api_statistics_sender.h b/Telegram/SourceFiles/api/api_statistics_sender.h
new file mode 100644
index 0000000000000..2ad0b664d27d7
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_statistics_sender.h
@@ -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 peer);
+ ~StatisticsRequestSender();
+
+ template <
+ typename Request,
+ typename = std::enable_if_t>,
+ typename = typename Request::Unboxed>
+ [[nodiscard]] auto makeRequest(Request &&request) {
+ const auto [id, dcId] = ensureRequestIsRegistered();
+ return std::move(_api.request(
+ std::forward(request)
+ ).toDC(
+ dcId ? MTP::ShiftDcId(dcId, MTP::kStatsDcShift) : 0
+ ).overrideId(id));
+ }
+
+ [[nodiscard]] MTP::Sender &api();
+ [[nodiscard]] not_null channel();
+ [[nodiscard]] not_null user();
+
+private:
+ struct Registered final {
+ mtpRequestId id;
+ MTP::DcId dcId;
+ };
+ [[nodiscard]] Registered ensureRequestIsRegistered();
+ void checkRequests();
+
+ const not_null _peer;
+ ChannelData * const _channel;
+ UserData * const _user;
+ MTP::Sender _api;
+ base::Timer _timer;
+ base::flat_map> _requests;
+
+};
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index 0ebf17f334128..1951dc2928cc4 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -1696,7 +1696,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
const auto peerId = peerFromMTP(d.vpeer());
const auto msgId = d.vmsg_id().v;
if (const auto item = session().data().message(peerId, msgId)) {
- item->applyEdition(d.vextended_media());
+ item->applyEdition(d.vextended_media().v);
}
} break;
@@ -2121,6 +2121,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
};
if (IsForceLogoutNotification(d)) {
Core::App().forceLogOut(&session().account(), text);
+ } else if (IsWithdrawalNotification(d)) {
+ return;
} else if (d.is_popup()) {
const auto &windows = session().windows();
if (!windows.empty()) {
@@ -2622,4 +2624,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
diff --git a/Telegram/SourceFiles/api/api_updates.h b/Telegram/SourceFiles/api/api_updates.h
index f21baa9641ce1..c028ae75aa1e3 100644
--- a/Telegram/SourceFiles/api/api_updates.h
+++ b/Telegram/SourceFiles/api/api_updates.h
@@ -211,4 +211,7 @@ class Updates final {
};
+[[nodiscard]] bool IsWithdrawalNotification(
+ const MTPDupdateServiceNotification &);
+
} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_views.cpp b/Telegram/SourceFiles/api/api_views.cpp
index 6c8ec8df7f6d4..e80f56e213b7b 100644
--- a/Telegram/SourceFiles/api/api_views.cpp
+++ b/Telegram/SourceFiles/api/api_views.cpp
@@ -55,7 +55,9 @@ void ViewsManager::removeIncremented(not_null peer) {
_incremented.remove(peer);
}
-void ViewsManager::pollExtendedMedia(not_null item) {
+void ViewsManager::pollExtendedMedia(
+ not_null item,
+ bool force) {
if (!item->isRegular()) {
return;
}
@@ -63,14 +65,20 @@ void ViewsManager::pollExtendedMedia(not_null item) {
const auto peer = item->history()->peer;
auto &request = _pollRequests[peer];
if (request.ids.contains(id) || request.sent.contains(id)) {
- return;
+ if (!force || request.forced) {
+ return;
+ }
}
request.ids.emplace(id);
- if (!request.id && !request.when) {
- request.when = crl::now() + kPollExtendedMediaPeriod;
+ if (force) {
+ request.forced = true;
+ }
+ const auto delay = force ? 1 : kPollExtendedMediaPeriod;
+ if (!request.id && (!request.when || force)) {
+ request.when = crl::now() + delay;
}
- if (!_pollTimer.isActive()) {
- _pollTimer.callOnce(kPollExtendedMediaPeriod);
+ if (!_pollTimer.isActive() || force) {
+ _pollTimer.callOnce(delay);
}
}
@@ -160,9 +168,12 @@ void ViewsManager::sendPollRequests(
if (i->second.ids.empty()) {
i = _pollRequests.erase(i);
} else {
- i->second.when = now + kPollExtendedMediaPeriod;
- if (!_pollTimer.isActive()) {
- _pollTimer.callOnce(kPollExtendedMediaPeriod);
+ const auto delay = i->second.forced
+ ? 1
+ : kPollExtendedMediaPeriod;
+ i->second.when = now + delay;
+ if (!_pollTimer.isActive() || i->second.forced) {
+ _pollTimer.callOnce(delay);
}
++i;
}
diff --git a/Telegram/SourceFiles/api/api_views.h b/Telegram/SourceFiles/api/api_views.h
index 759d3c6a8a66b..f8d19d15bcca3 100644
--- a/Telegram/SourceFiles/api/api_views.h
+++ b/Telegram/SourceFiles/api/api_views.h
@@ -26,7 +26,7 @@ class ViewsManager final {
void scheduleIncrement(not_null item);
void removeIncremented(not_null peer);
- void pollExtendedMedia(not_null item);
+ void pollExtendedMedia(not_null item, bool force = false);
private:
struct PollExtendedMediaRequest {
@@ -34,6 +34,7 @@ class ViewsManager final {
mtpRequestId id = 0;
base::flat_set ids;
base::flat_set sent;
+ bool forced = false;
};
void viewsIncrement();
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index ba44006844ad4..04a2e25f11444 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -2165,7 +2165,8 @@ void ApiWrap::saveDraftsToCloud() {
entities,
Data::WebPageForMTP(
cloudDraft->webpage,
- textWithTags.text.isEmpty())
+ textWithTags.text.isEmpty()),
+ MTP_long(0) // effect
)).done([=](const MTPBool &result, const MTP::Response &response) {
const auto requestId = response.requestId;
history->finishSavingCloudDraft(
@@ -4187,7 +4188,11 @@ void ApiWrap::sendMediaWithRandomId(
MTP_flags(flags),
peer->input,
Data::Histories::ReplyToPlaceholder(),
- media,
+ (options.price
+ ? MTPInputMedia(MTP_inputMediaPaidMedia(
+ MTP_long(options.price),
+ MTP_vector(1, media)))
+ : media),
MTP_string(caption.text),
MTP_long(randomId),
MTPReplyMarkup(),
@@ -4207,6 +4212,82 @@ void ApiWrap::sendMediaWithRandomId(
});
}
+void ApiWrap::sendMultiPaidMedia(
+ not_null item,
+ not_null album,
+ Fn 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>();
+
+ 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(
+ MTP_flags(flags),
+ peer->input,
+ Data::Histories::ReplyToPlaceholder(),
+ MTP_inputMediaPaidMedia(
+ MTP_long(options.price),
+ MTP_vector(std::move(medias))),
+ MTP_string(caption.text),
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTP_int(options.scheduled),
+ (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
+ Data::ShortcutIdToMTP(_session, options.shortcutId),
+ MTP_long(options.effectId)
+ ), [=](const MTPUpdates &result, const MTP::Response &response) {
+ if (const auto album = _sendingAlbums.take(groupId)) {
+ const auto copy = (*album)->items;
+ for (const auto &part : copy) {
+ if (const auto item = history->owner().message(part.msgId)) {
+ item->destroy();
+ }
+ }
+ }
+ if (done) done(true);
+ }, [=](const MTP::Error &error, const MTP::Response &response) {
+ if (done) done(false);
+ sendMessageFail(error, peer, randomId, itemId);
+ });
+}
+
void ApiWrap::sendAlbumWithUploaded(
not_null item,
const MessageGroupId &groupId,
@@ -4260,8 +4341,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) {
if (!sample) {
_sendingAlbums.remove(groupId);
return;
+ } else if (album->options.price > 0) {
+ sendMultiPaidMedia(sample, album);
+ return;
} else if (medias.size() < 2) {
- const auto &single = medias.front().c_inputSingleMedia();
+ const auto &single = medias.front().data();
sendMediaWithRandomId(
sample,
single.vmedia(),
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 9710ee1bd8291..15c0941c39fc8 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -545,6 +545,10 @@ class ApiWrap final : public MTP::Sender {
Api::SendOptions options,
uint64 randomId,
Fn done = nullptr);
+ void sendMultiPaidMedia(
+ not_null item,
+ not_null album,
+ Fn done = nullptr);
void getTopPromotionDelayed(TimeId now, TimeId next);
void topPromotionDone(const MTPhelp_PromoData &proxy);
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index cfd123c9337c6..c3cad02eb466a 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -237,7 +237,7 @@ shareColumnSkip: 6px;
shareActivateDuration: 150;
shareScrollDuration: 300;
shareComment: InputField(defaultInputField) {
- font: normalFont;
+ style: defaultTextStyle;
textMargins: margins(8px, 8px, 8px, 6px);
heightMin: 36px;
heightMax: 72px;
@@ -290,6 +290,26 @@ passcodeTextLine: 28px;
passcodeLittleSkip: 5px;
passcodeAboutSkip: 7px;
passcodeSkip: 23px;
+passcodeSystemUnlock: IconButton(defaultIconButton) {
+ width: 32px;
+ height: 36px;
+ icon: icon{{ "menu/passcode_winhello", lightButtonFg }};
+ iconOver: icon{{ "menu/passcode_winhello", lightButtonFg }};
+ iconPosition: point(4px, 4px);
+ rippleAreaSize: 32px;
+ rippleAreaPosition: point(0px, 0px);
+ ripple: RippleAnimation(defaultRippleAnimation) {
+ color: lightButtonBgOver;
+ }
+}
+passcodeSystemTouchID: icon{{ "menu/passcode_finger", lightButtonFg }};
+passcodeSystemAppleWatch: icon{{ "menu/passcode_watch", lightButtonFg }};
+passcodeSystemSystemPwd: icon{{ "menu/permissions", lightButtonFg }};
+passcodeSystemUnlockLater: FlatLabel(defaultFlatLabel) {
+ align: align(top);
+ textFg: windowSubTextFg;
+}
+passcodeSystemUnlockSkip: 12px;
newGroupAboutFg: windowSubTextFg;
newGroupPadding: margins(4px, 6px, 4px, 3px);
@@ -585,7 +605,7 @@ groupStickersRemovePosition: point(6px, 6px);
groupStickersFieldPadding: margins(8px, 6px, 8px, 6px);
groupStickersField: InputField(defaultMultiSelectSearchField) {
placeholderFont: boxTextFont;
- font: boxTextFont;
+ style: boxTextStyle;
placeholderMargins: margins(0px, 0px, 0px, 0px);
textMargins: margins(0px, 7px, 0px, 0px);
textBg: boxBg;
@@ -672,7 +692,6 @@ themesMenuToggle: IconButton(defaultIconButton) {
themesMenuPosition: point(-2px, 25px);
createPollField: InputField(defaultInputField) {
- font: boxTextFont;
textMargins: margins(0px, 4px, 0px, 4px);
textAlign: align(left);
heightMin: 36px;
@@ -877,7 +896,6 @@ scheduleDateField: InputField(defaultInputField) {
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(top);
- font: font(14px);
}
scheduleTimeField: InputField(scheduleDateField) {
border: 0px;
@@ -905,7 +923,6 @@ muteBoxTimeField: InputField(scheduleDateField) {
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(left);
- font: font(14px);
}
muteBoxTimeFieldPadding: margins(5px, 0px, 5px, 0px);
diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp
index bc61e9fe2271b..4e99a01ba855d 100644
--- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp
@@ -39,6 +39,7 @@ Data::ChatFilter ChangedFilter(
filter.id(),
filter.title(),
filter.iconEmoji(),
+ filter.colorIndex(),
filter.flags(),
std::move(always),
filter.pinned(),
@@ -58,6 +59,7 @@ Data::ChatFilter ChangedFilter(
filter.id(),
filter.title(),
filter.iconEmoji(),
+ filter.colorIndex(),
filter.flags(),
std::move(always),
filter.pinned(),
@@ -81,7 +83,7 @@ void ChangeFilterById(
MTP_int(filter.id()),
filter.tl()
)).done([=, chat = history->peer->name(), name = filter.title()] {
- const auto account = &history->session().account();
+ const auto account = not_null(&history->session().account());
if (const auto controller = Core::App().windowFor(account)) {
controller->showToast((add
? tr::lng_filters_toast_add
diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp
index 46138e3fd3724..9ff85e099abac 100644
--- a/Telegram/SourceFiles/boxes/create_poll_box.cpp
+++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp
@@ -1044,7 +1044,16 @@ not_null CreatePollBox::setupSolution(
solution->setInstantReplaces(Ui::InstantReplaces::Default());
solution->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
- solution->setMarkdownReplacesEnabled(true);
+ solution->setMarkdownReplacesEnabled(rpl::single(
+ Ui::MarkdownEnabledState{ Ui::MarkdownEnabled{ {
+ Ui::InputField::kTagBold,
+ Ui::InputField::kTagItalic,
+ Ui::InputField::kTagUnderline,
+ Ui::InputField::kTagStrikeOut,
+ Ui::InputField::kTagCode,
+ Ui::InputField::kTagSpoiler,
+ } } }
+ ));
solution->setEditLinkCallback(
DefaultEditLinkCallback(_controller->uiShow(), solution));
solution->customTab(true);
diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
index d46121c2f9657..bd089a9b5acf0 100644
--- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
@@ -463,6 +463,7 @@ void EditCaptionBox::rebuildPreview() {
st::defaultComposeControls,
gifPaused,
file,
+ [] { return true; },
Ui::AttachControls::Type::EditOnly);
_isPhoto = (media && media->isPhoto());
const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
index ce23d235ff551..df85e625443e0 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
@@ -83,6 +83,7 @@ not_null SetupChatsPreview(
rules.id(),
rules.title(),
rules.iconEmoji(),
+ rules.colorIndex(),
(rules.flags() & ~flag),
rules.always(),
rules.pinned(),
@@ -104,6 +105,7 @@ not_null SetupChatsPreview(
rules.id(),
rules.title(),
rules.iconEmoji(),
+ rules.colorIndex(),
rules.flags(),
std::move(always),
std::move(pinned),
@@ -170,6 +172,7 @@ void EditExceptions(
rules.id(),
rules.title(),
rules.iconEmoji(),
+ rules.colorIndex(),
((rules.flags() & ~options)
| rawController->chosenOptions()),
include ? std::move(changed) : std::move(removeFrom),
@@ -240,6 +243,7 @@ void CreateIconSelector(
rules.id(),
rules.title(),
Ui::LookupFilterIcon(icon).emoji,
+ rules.colorIndex(),
rules.flags(),
rules.always(),
rules.pinned(),
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index ef87c642369b6..fdf1eda03f30b 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -1011,14 +1011,16 @@ void GiftPremiumValidator::showChoosePeerBox(const QString &ref) {
}) | ranges::views::filter([](UserData *u) -> bool {
return u;
}) | ranges::to>>();
- if (!users.empty()) {
- const auto giftBox = show->show(
- Box(GiftsBox, _controller, users, api, ref));
- giftBox->boxClosing(
- ) | rpl::start_with_next([=] {
- _manyGiftsLifetime.destroy();
- }, giftBox->lifetime());
+ if (users.empty()) {
+ show->showToast(
+ tr::lng_settings_gift_premium_choose(tr::now));
}
+ const auto giftBox = show->show(
+ Box(GiftsBox, _controller, users, api, ref));
+ giftBox->boxClosing(
+ ) | rpl::start_with_next([=] {
+ _manyGiftsLifetime.destroy();
+ }, giftBox->lifetime());
(*ignoreClose) = true;
peersBox->closeBox();
};
@@ -1644,12 +1646,68 @@ void AddCreditsHistoryEntryTable(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
- if (entry.bareId) {
+ const auto peerId = PeerId(entry.barePeerId);
+ if (peerId) {
+ auto text = entry.in
+ ? tr::lng_credits_box_history_entry_peer_in()
+ : 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(
+ table,
+ rpl::single(Ui::Text::Link(link)),
+ st::giveawayGiftCodeValue);
+ label->setClickHandlerFilter([=](const auto &...) {
+ controller->showPeerHistory(channel, {}, msgId);
+ return false;
+ });
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_media(),
+ std::move(label),
+ st::giveawayGiftCodeValueMargin);
+ }
+ }
+ using Type = Data::CreditsHistoryEntry::PeerType;
+ if (entry.peerType == Type::AppStore) {
AddTableRow(
table,
- tr::lng_credits_box_history_entry_peer(),
- controller,
- PeerId(entry.bareId));
+ tr::lng_credits_box_history_entry_via(),
+ tr::lng_credits_box_history_entry_app_store(
+ Ui::Text::RichLangValue));
+ } else if (entry.peerType == Type::PlayMarket) {
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_via(),
+ tr::lng_credits_box_history_entry_play_market(
+ Ui::Text::RichLangValue));
+ } else if (entry.peerType == Type::Fragment) {
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_via(),
+ tr::lng_credits_box_history_entry_fragment(
+ Ui::Text::RichLangValue));
+ } else if (entry.peerType == Type::Ads) {
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_via(),
+ tr::lng_credits_box_history_entry_ads(Ui::Text::RichLangValue));
+ } else if (entry.peerType == Type::PremiumBot) {
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_via(),
+ tr::lng_credits_box_history_entry_via_premium_bot(
+ Ui::Text::RichLangValue));
}
if (!entry.id.isEmpty()) {
constexpr auto kOneLineCount = 18;
@@ -1680,4 +1738,17 @@ void AddCreditsHistoryEntryTable(
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
}
+ if (!entry.successDate.isNull()) {
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_success_date(),
+ rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
+ }
+ if (!entry.successLink.isEmpty()) {
+ AddTableRow(
+ table,
+ tr::lng_credits_box_history_entry_success_url(),
+ rpl::single(
+ Ui::Text::Link(entry.successLink, entry.successLink)));
+ }
}
diff --git a/Telegram/SourceFiles/boxes/max_invite_box.cpp b/Telegram/SourceFiles/boxes/max_invite_box.cpp
index b6230b673e594..a7c1f84089115 100644
--- a/Telegram/SourceFiles/boxes/max_invite_box.cpp
+++ b/Telegram/SourceFiles/boxes/max_invite_box.cpp
@@ -133,8 +133,8 @@ void MaxInviteBox::paintEvent(QPaintEvent *e) {
auto option = QTextOption(style::al_left);
option.setWrapMode(QTextOption::WrapAnywhere);
p.setFont(_linkOver
- ? st::defaultInputField.font->underline()
- : st::defaultInputField.font);
+ ? st::defaultInputField.style.font->underline()
+ : st::defaultInputField.style.font);
p.setPen(st::defaultLinkButton.color);
const auto inviteLinkText = _channel->inviteLink().isEmpty()
? tr::lng_group_invite_create(tr::now)
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
index 99427aca0ac4b..0ba6354f996bd 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
@@ -8,6 +8,7 @@ For license and copyright information please follow this link:
#include "boxes/peers/edit_peer_info_box.h"
#include "apiwrap.h"
+#include "api/api_credits.h"
#include "api/api_peer_photo.h"
#include "api/api_user_names.h"
#include "main/main_session.h"
@@ -42,6 +43,7 @@ For license and copyright information please follow this link:
#include "data/data_premium_limits.h"
#include "data/data_user.h"
#include "history/admin_log/history_admin_log_section.h"
+#include "info/bot/earn/info_bot_earn_widget.h"
#include "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/info_memento.h"
@@ -52,6 +54,8 @@ For license and copyright information please follow this link:
#include "ui/boxes/boost_box.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/userpic_button.h"
+#include "ui/effects/premium_graphics.h"
+#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/vertical_list.h"
#include "ui/toast/toast.h"
@@ -71,6 +75,8 @@ For license and copyright information please follow this link:
#include "styles/style_boxes.h"
#include "styles/style_info.h"
+#include
+
namespace {
constexpr auto kBotManagerUsername = "BotFather"_cs;
@@ -343,6 +349,7 @@ class Controller : public base::has_weak_ptr {
void fillPendingRequestsButton();
void fillBotUsernamesButton();
+ void fillBotBalanceButton();
void fillBotEditIntroButton();
void fillBotEditCommandsButton();
void fillBotEditSettingsButton();
@@ -1126,6 +1133,7 @@ void Controller::fillManageSection() {
::AddSkip(container, 0);
fillBotUsernamesButton();
+ fillBotBalanceButton();
fillBotEditIntroButton();
fillBotEditCommandsButton();
fillBotEditSettingsButton();
@@ -1536,6 +1544,84 @@ void Controller::fillBotUsernamesButton() {
{ &st::menuIconLinks });
}
+void Controller::fillBotBalanceButton() {
+ Expects(_isBot);
+
+ struct State final {
+ rpl::variable balance;
+ };
+
+ auto &lifetime = _controls.buttonsLayout->lifetime();
+ const auto state = lifetime.make_state();
+
+ const auto wrap = _controls.buttonsLayout->add(
+ object_ptr>(
+ _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(
+ _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(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(
+ colorized.toUtf8());
+ svg->setViewBox(svg->viewBox() + Margins(kStrokeWidth));
+
+ const auto starSize = Size(icon->height());
+
+ icon->paintRequest(
+ ) | rpl::start_with_next([=] {
+ auto p = QPainter(icon);
+ svg->render(&p, Rect(starSize));
+ }, icon->lifetime());
+
+ button->sizeValue(
+ ) | rpl::start_with_next([=](const QSize &size) {
+ icon->moveToLeft(
+ button->st().iconLeft + kSizeShift / 2.,
+ (size.height() - icon->height()) / 2);
+ }, icon->lifetime());
+ }
+
+}
+
void Controller::fillBotEditIntroButton() {
Expects(_isBot);
@@ -2114,8 +2200,11 @@ void Controller::saveForum() {
channel->inputChannel,
MTP_bool(*_savingData.forum)
)).done([=](const MTPUpdates &result) {
+ const auto weak = base::make_weak(this);
channel->session().api().applyUpdates(result);
- continueSave();
+ if (weak) { // todo better to be able to save in closed already box.
+ continueSave();
+ }
}).fail([=](const MTP::Error &error) {
if (error.type() == u"CHAT_NOT_MODIFIED"_q) {
continueSave();
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp
index 0873426e52249..8905c33c13cde 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp
@@ -11,6 +11,7 @@ For license and copyright information please follow this link:
#include "base/event_filter.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
+#include "core/ui_integration.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h"
@@ -351,8 +352,8 @@ object_ptr AddReactionsSelector(
const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
};
- raw->setCustomEmojiFactory([=](QStringView data, Fn update)
- -> std::unique_ptr {
+ auto factory = [=](QStringView data, Fn update)
+ -> std::unique_ptr {
const auto id = Data::ParseCustomEmojiData(data);
auto result = owner->customEmojiManager().create(
data,
@@ -364,7 +365,13 @@ object_ptr AddReactionsSelector(
}
using namespace Ui::Text;
return std::make_unique(std::move(result));
- }, std::move(customEmojiPaused));
+ };
+ raw->setCustomTextContext([=](Fn repaint) {
+ return std::any(Core::MarkedTextContext{
+ .session = session,
+ .customEmojiRepaint = std::move(repaint),
+ });
+ }, customEmojiPaused, customEmojiPaused, std::move(factory));
const auto callback = args.callback;
const auto isCustom = [=](DocumentId id) {
diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
index 70648d627aa24..a3bc8b69bd374 100644
--- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
@@ -459,6 +459,7 @@ Ui::BoostFeatures LookupBoostFeatures(not_null channel) {
.customWallpaperLevel = group
? levelLimits.groupCustomWallpaperLevelMin()
: levelLimits.channelCustomWallpaperLevelMin(),
+ .sponsoredLevel = levelLimits.channelRestrictSponsoredLevelMin(),
};
}
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
index 76a2a18b76788..f163be612296d 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
@@ -131,6 +131,8 @@ void PreloadSticker(const std::shared_ptr &media) {
return tr::lng_premium_summary_subtitle_translation();
case PremiumFeature::Business:
return tr::lng_premium_summary_subtitle_business();
+ case PremiumFeature::Effects:
+ return tr::lng_premium_summary_subtitle_effects();
case PremiumFeature::BusinessLocation:
return tr::lng_business_subtitle_location();
@@ -192,6 +194,8 @@ void PreloadSticker(const std::shared_ptr &media) {
return tr::lng_premium_summary_about_translation();
case PremiumFeature::Business:
return tr::lng_premium_summary_about_business();
+ case PremiumFeature::Effects:
+ return tr::lng_premium_summary_about_effects();
case PremiumFeature::BusinessLocation:
return tr::lng_business_about_location();
@@ -529,6 +533,7 @@ struct VideoPreviewDocument {
case PremiumFeature::Wallpapers: return "wallpapers";
case PremiumFeature::LastSeen: return "last_seen";
case PremiumFeature::MessagePrivacy: return "message_privacy";
+ case PremiumFeature::Effects: return "effects";
case PremiumFeature::BusinessLocation: return "business_location";
case PremiumFeature::BusinessHours: return "business_hours";
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h
index 63b1bd1bec133..4dae5c30d5f04 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.h
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.h
@@ -70,6 +70,7 @@ enum class PremiumFeature {
LastSeen,
MessagePrivacy,
Business,
+ Effects,
// Business features.
BusinessLocation,
diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp
index 8820c0c65f6f4..39d7983d7378d 100644
--- a/Telegram/SourceFiles/boxes/send_credits_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp
@@ -11,6 +11,7 @@ For license and copyright information please follow this link:
#include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/data_credits.h"
+#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
@@ -39,6 +40,147 @@ For license and copyright information please follow this link:
#include "styles/style_settings.h"
namespace Ui {
+namespace {
+
+struct PaidMediaData {
+ const Data::Invoice *invoice = nullptr;
+ HistoryItem *item = nullptr;
+ PeerData *peer = nullptr;
+ int photos = 0;
+ int videos = 0;
+
+ explicit operator bool() const {
+ return invoice && item && peer && (photos || videos);
+ }
+};
+
+[[nodiscard]] PaidMediaData LookupPaidMediaData(
+ not_null session,
+ not_null form) {
+ using namespace Payments;
+ const auto message = std::get_if(&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 SendCreditsConfirmText(
+ not_null session,
+ not_null 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 SendCreditsThumbnail(
+ not_null parent,
+ not_null session,
+ not_null 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(
+ parent,
+ bot,
+ st::defaultUserpicButton);
+}
+
+} // namespace
void SendCreditsBox(
not_null box,
@@ -89,22 +231,10 @@ void SendCreditsBox(
}, ministarsContainer->lifetime());
}
- const auto bot = session->data().user(form->botId);
-
- if (form->photo) {
- box->addRow(object_ptr>(
- content,
- Settings::HistoryEntryPhoto(content, form->photo, photoSize)));
- } else {
- const auto widget = box->addRow(
- object_ptr>(
- content,
- object_ptr(
- content,
- bot,
- st::defaultUserpicButton)));
- widget->setAttribute(Qt::WA_TransparentForMouseEvents);
- }
+ const auto thumb = box->addRow(object_ptr>(
+ content,
+ SendCreditsThumbnail(content, session, form.get(), photoSize)));
+ thumb->setAttribute(Qt::WA_TransparentForMouseEvents);
Ui::AddSkip(content);
box->addRow(object_ptr>(
@@ -118,14 +248,7 @@ void SendCreditsBox(
box,
object_ptr(
box,
- tr::lng_credits_box_out_sure(
- lt_count,
- rpl::single(form->invoice.amount) | tr::to_count(),
- lt_text,
- rpl::single(TextWithEntities{ form->title }),
- lt_bot,
- rpl::single(TextWithEntities{ bot->name() }),
- Ui::Text::RichLangValue),
+ SendCreditsConfirmText(session, form.get()),
st::creditsBoxAbout)));
Ui::AddSkip(content);
Ui::AddSkip(content);
@@ -158,26 +281,16 @@ void SendCreditsBox(
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
{
- const auto emojiMargin = QMargins(
- 0,
- -st::moderateBoxExpandInnerSkip,
- 0,
- 0);
- const auto buttonEmoji = Ui::Text::SingleCustomEmoji(
- session->data().customEmojiManager().registerInternalEmoji(
- st::settingsPremiumIconStar,
- emojiMargin,
- true));
auto buttonText = tr::lng_credits_box_out_confirm(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_emoji,
- rpl::single(buttonEmoji),
+ rpl::single(CreditsEmojiSmall(session)),
Ui::Text::RichLangValue);
const auto buttonLabel = Ui::CreateChild(
button,
rpl::single(QString()),
- st::defaultFlatLabel);
+ st::creditsBoxButtonLabel);
std::move(
buttonText
) | rpl::start_with_next([=](const TextWithEntities &text) {
@@ -247,4 +360,22 @@ void SendCreditsBox(
}
}
+TextWithEntities CreditsEmoji(not_null 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 session) {
+ return Ui::Text::SingleCustomEmoji(
+ session->data().customEmojiManager().registerInternalEmoji(
+ st::starIconSmall,
+ st::starIconSmallPadding,
+ true),
+ QString(QChar(0x2B50)));
+}
+
} // namespace Ui
diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h
index 25ceb1d562db9..c0107a3602614 100644
--- a/Telegram/SourceFiles/boxes/send_credits_box.h
+++ b/Telegram/SourceFiles/boxes/send_credits_box.h
@@ -9,6 +9,10 @@ For license and copyright information please follow this link:
class HistoryItem;
+namespace Main {
+class Session;
+} // namespace Main
+
namespace Payments {
struct CreditsFormData;
} // namespace Payments
@@ -22,4 +26,10 @@ void SendCreditsBox(
std::shared_ptr data,
Fn sent);
+[[nodiscard]] TextWithEntities CreditsEmoji(
+ not_null session);
+
+[[nodiscard]] TextWithEntities CreditsEmojiSmall(
+ not_null session);
+
} // namespace Ui
diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp
index fbaaf561a7896..0eade94ca0113 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_files_box.cpp
@@ -10,7 +10,9 @@ For license and copyright information please follow this link:
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "storage/storage_media_prepare.h"
+#include "iv/iv_instance.h"
#include "mainwidget.h"
+#include "main/main_app_config.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "mtproto/mtproto_config.h"
@@ -24,11 +26,14 @@ For license and copyright information please follow this link:
#include "history/view/controls/history_view_characters_limit.h"
#include "history/view/history_view_schedule_box.h"
#include "core/mime_type.h"
+#include "core/ui_integration.h"
#include "base/event_filter.h"
#include "base/call_delayed.h"
#include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h"
+#include "boxes/send_credits_box.h"
#include "ui/effects/scroll_content_shadow.h"
+#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/popup_menu.h"
@@ -36,10 +41,13 @@ For license and copyright information please follow this link:
#include "ui/chat/attach/attach_single_file_preview.h"
#include "ui/chat/attach/attach_single_media_preview.h"
#include "ui/grouped_layout.h"
+#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/controls/emoji_button.h"
#include "ui/painter.h"
+#include "ui/vertical_list.h"
#include "lottie/lottie_single_player.h"
+#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_user.h"
#include "data/data_peer_values.h" // Data::AmPremiumValue.
@@ -103,6 +111,84 @@ rpl::producer FieldPlaceholder(
: tr::lng_photos_comment();
}
+void EditPriceBox(
+ not_null box,
+ not_null session,
+ uint64 price,
+ Fn 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(
+ u"stars_paid_post_amount_max"_q,
+ 10'000);
+ const auto wrap = box->addRow(object_ptr(
+ box,
+ st::editTagField.heightMin));
+ auto owned = object_ptr(
+ 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(
+ box,
+ tr::lng_paid_about(
+ lt_link,
+ tr::lng_paid_about_link() | Ui::Text::ToLink(),
+ Ui::Text::WithEntities),
+ st::paidAmountAbout),
+ st::boxRowPadding + QMargins(0, st::sendMediaRowSkip, 0, 0));
+ about->setClickHandlerFilter([=](const auto &...) {
+ Core::App().iv().openWithIvPreferred(
+ session,
+ tr::lng_paid_about_link_url(tr::now));
+ return false;
+ });
+
+ field->paintRequest() | rpl::start_with_next([=](QRect clip) {
+ auto p = QPainter(field);
+ st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width());
+ }, field->lifetime());
+
+ const auto save = [=] {
+ const auto now = field->getLastText().toULongLong();
+ if (now > limit) {
+ field->showError();
+ return;
+ }
+ const auto weak = Ui::MakeWeak(box);
+ apply(now);
+ if (const auto strong = weak.data()) {
+ strong->closeBox();
+ }
+ };
+
+ QObject::connect(field, &Ui::NumberInput::submitted, box, save);
+
+ box->addButton(tr::lng_settings_save(), save);
+ box->addButton(tr::lng_cancel(), [=] {
+ box->closeBox();
+ });
+}
+
} // namespace
SendFilesLimits DefaultLimitsForPeer(not_null peer) {
@@ -153,7 +239,8 @@ SendFilesBox::Block::Block(
int from,
int till,
Fn gifPaused,
- SendFilesWay way)
+ SendFilesWay way,
+ Fn canToggleSpoiler)
: _items(items)
, _from(from)
, _till(till) {
@@ -170,14 +257,16 @@ SendFilesBox::Block::Block(
parent.get(),
st,
my,
- way);
+ way,
+ std::move(canToggleSpoiler));
_preview.reset(preview);
} else {
const auto media = Ui::SingleMediaPreview::Create(
parent,
st,
gifPaused,
- first);
+ first,
+ std::move(canToggleSpoiler));
if (media) {
_isSingleMedia = true;
_preview.reset(media);
@@ -253,6 +342,14 @@ rpl::producer SendFilesBox::Block::itemModifyRequest() const {
}
}
+rpl::producer<> SendFilesBox::Block::orderUpdated() const {
+ if (_isAlbum) {
+ const auto album = static_cast(_preview.get());
+ return album->orderUpdated();
+ }
+ return rpl::never<>();
+}
+
void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {
if (!_isAlbum) {
if (_isSingleMedia) {
@@ -321,6 +418,18 @@ void SendFilesBox::Block::applyChanges() {
}
}
+QImage SendFilesBox::Block::generatePriceTagBackground() const {
+ const auto preview = _preview.get();
+ if (_isAlbum) {
+ const auto album = static_cast(preview);
+ return album->generatePriceTagBackground();
+ } else if (_isSingleMedia) {
+ const auto media = static_cast(preview);
+ return media->generatePriceTagBackground();
+ }
+ return QImage();
+}
+
SendFilesBox::SendFilesBox(
QWidget*,
not_null controller,
@@ -385,6 +494,9 @@ Fn SendFilesBox::prepareSendMenuDetails(
: _invertCaption
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below;
+ result.price = canChangePrice()
+ ? _price.current()
+ : std::optional();
return result;
});
}
@@ -398,6 +510,7 @@ auto SendFilesBox::prepareSendMenuCallback()
case Type::CaptionUp: _invertCaption = true; break;
case Type::SpoilerOn: toggleSpoilers(true); break;
case Type::SpoilerOff: toggleSpoilers(false); break;
+ case Type::ChangePrice: changePrice(); break;
default:
SendMenu::DefaultCallback(
_show,
@@ -588,14 +701,27 @@ void SendFilesBox::refreshButtons() {
addMenuButton();
}
-bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const {
+bool SendFilesBox::hasSendMenu(const MenuDetails &details) const {
return (details.type != SendMenu::Type::Disabled)
|| (details.spoiler != SendMenu::SpoilerState::None)
|| (details.caption != SendMenu::CaptionState::None);
}
bool SendFilesBox::hasSpoilerMenu() const {
- return _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos());
+ return !hasPrice()
+ && _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos());
+}
+
+bool SendFilesBox::canChangePrice() const {
+ const auto way = _sendWay.current();
+ const auto broadcast = _captionToPeer
+ ? _captionToPeer->asBroadcast()
+ : nullptr;
+ return broadcast
+ && broadcast->canPostPaidMedia()
+ && _list.canChangePrice(
+ way.groupFiles() && way.sendImagesAsPhotos(),
+ way.sendImagesAsPhotos());
}
void SendFilesBox::applyBlockChanges() {
@@ -618,6 +744,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(_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(
+ raw,
+ QString(),
+ st::paidTagLabel);
+ std::move(text) | rpl::start_with_next([=](TextWithEntities &&text) {
+ label->setMarkedText(text, Core::MarkedTextContext{
+ .session = session,
+ .customEmojiRepaint = [=] { label->update(); },
+ });
+ }, label->lifetime());
+ label->show();
+ label->sizeValue() | rpl::start_with_next([=](QSize size) {
+ const auto inner = QRect(QPoint(), size);
+ const auto rect = inner.marginsAdded(st::paidTagPadding);
+ raw->resize(rect.size());
+ label->move(-rect.topLeft());
+ }, label->lifetime());
+ _inner->sizeValue() | rpl::start_with_next([=](QSize size) {
+ raw->move(
+ (size.width() - raw->width()) / 2,
+ (size.height() - raw->height()) / 2);
+ }, raw->lifetime());
+ } else {
+ _priceTag->raise();
+ _priceTag->update();
+ _priceTagBg = QImage();
+ }
+}
+
+QImage SendFilesBox::preparePriceTagBg(QSize size) const {
+ const auto ratio = style::DevicePixelRatio();
+ const auto outer = _blocks.empty()
+ ? size
+ : _inner->widgetAt(0)->geometry().size();
+ auto bg = _blocks.empty()
+ ? QImage()
+ : _blocks.front().generatePriceTagBackground();
+ if (bg.isNull()) {
+ bg = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);
+ bg.fill(Qt::black);
+ }
+
+ auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
+ result.setDevicePixelRatio(ratio);
+ result.fill(Qt::black);
+ auto p = QPainter(&result);
+ auto hq = PainterHighQualityEnabler(p);
+ p.drawImage(
+ QRect(
+ (size.width() - outer.width()) / 2,
+ (size.height() - outer.height()) / 2,
+ outer.width(),
+ outer.height()),
+ bg);
+ p.fillRect(QRect(QPoint(), size), st::msgDateImgBg);
+ p.end();
+
+ const auto radius = std::min(size.width(), size.height()) / 2;
+ return Images::Round(std::move(result), Images::CornersMask(radius));
+}
+
void SendFilesBox::addMenuButton() {
const auto details = _sendMenuDetails();
if (!hasSendMenu(details)) {
@@ -681,6 +919,7 @@ void SendFilesBox::initSendWay() {
block.setSendWay(value);
}
refreshButtons();
+ refreshPriceTag();
if (was != hidden()) {
updateBoxSize();
updateControlsGeometry();
@@ -766,7 +1005,8 @@ void SendFilesBox::pushBlock(int from, int till) {
from,
till,
gifPaused,
- _sendWay.current());
+ _sendWay.current(),
+ [=] { return !hasPrice(); });
auto &block = _blocks.back();
const auto widget = _inner->add(
block.takeWidget(),
@@ -889,10 +1129,18 @@ void SendFilesBox::pushBlock(int from, int till) {
st::sendMediaPreviewSize,
[=] { refreshAllAfterChanges(from); });
}, widget->lifetime());
+
+ block.orderUpdated() | rpl::start_with_next([=]{
+ if (_priceTag) {
+ _priceTagBg = QImage();
+ _priceTag->update();
+ }
+ }, widget->lifetime());
}
void SendFilesBox::refreshControls(bool initial) {
refreshButtons();
+ refreshPriceTag();
refreshTitleText();
updateSendWayControls();
updateCaptionPlaceholder();
@@ -1447,6 +1695,7 @@ void SendFilesBox::send(
auto child = _sendMenuDetails();
child.spoiler = SendMenu::SpoilerState::None;
child.caption = SendMenu::CaptionState::None;
+ child.price = std::nullopt;
return SendMenu::DefaultCallback(_show, sendCallback())(
{ .type = SendMenu::ActionType::Schedule },
child);
@@ -1474,10 +1723,16 @@ void SendFilesBox::send(
auto caption = (_caption && !_caption->isHidden())
? _caption->getTextWithAppliedMarkdown()
: TextWithTags();
- options.invertCaption = _invertCaption;
if (!validateLength(caption.text)) {
return;
}
+ options.invertCaption = _invertCaption;
+ options.price = hasPrice() ? _price.current() : 0;
+ if (options.price > 0) {
+ for (auto &file : _list.files) {
+ file.spoiler = false;
+ }
+ }
_confirmedCallback(
std::move(_list),
_sendWay.current(),
diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h
index 1e95a1aa81365..8f3432ebd7e91 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.h
+++ b/Telegram/SourceFiles/boxes/send_files_box.h
@@ -149,7 +149,8 @@ class SendFilesBox : public Ui::BoxContent {
int from,
int till,
Fn gifPaused,
- Ui::SendFilesWay way);
+ Ui::SendFilesWay way,
+ Fn canToggleSpoiler);
Block(Block &&other) = default;
Block &operator=(Block &&other) = default;
@@ -160,11 +161,14 @@ class SendFilesBox : public Ui::BoxContent {
[[nodiscard]] rpl::producer itemDeleteRequest() const;
[[nodiscard]] rpl::producer itemReplaceRequest() const;
[[nodiscard]] rpl::producer itemModifyRequest() const;
+ [[nodiscard]] rpl::producer<> orderUpdated() const;
void setSendWay(Ui::SendFilesWay way);
void toggleSpoilers(bool enabled);
void applyChanges();
+ [[nodiscard]] QImage generatePriceTagBackground() const;
+
private:
base::unique_qptr _preview;
not_null*> _items;
@@ -190,6 +194,12 @@ class SendFilesBox : public Ui::BoxContent {
void addMenuButton();
void applyBlockChanges();
void toggleSpoilers(bool enabled);
+ void changePrice();
+
+ [[nodiscard]] bool canChangePrice() const;
+ [[nodiscard]] bool hasPrice() const;
+ void refreshPriceTag();
+ [[nodiscard]] QImage preparePriceTagBg(QSize size) const;
bool validateLength(const QString &text) const;
void refreshButtons();
@@ -251,6 +261,9 @@ class SendFilesBox : public Ui::BoxContent {
SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback;
Fn _cancelledCallback;
+ rpl::variable _price = 0;
+ std::unique_ptr _priceTag;
+ QImage _priceTagBg;
bool _confirmed = false;
bool _invertCaption = false;
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index 043a8ad18650a..e7e3054aa4217 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -1409,55 +1409,6 @@ std::vector> ShareBox::Inner::selected() const {
return result;
}
-QString AppendShareGameScoreUrl(
- not_null session,
- const QString &url,
- const FullMsgId &fullId) {
- auto shareHashData = QByteArray(0x20, Qt::Uninitialized);
- auto shareHashDataInts = reinterpret_cast(shareHashData.data());
- const auto peer = fullId.peer
- ? session->data().peerLoaded(fullId.peer)
- : static_cast(nullptr);
- const auto channelAccessHash = uint64((peer && peer->isChannel())
- ? peer->asChannel()->access
- : 0);
- shareHashDataInts[0] = session->userId().bare;
- shareHashDataInts[1] = fullId.peer.value;
- shareHashDataInts[2] = uint64(fullId.msg.bare);
- shareHashDataInts[3] = channelAccessHash;
-
- // Count SHA1() of data.
- auto key128Size = 0x10;
- auto shareHashEncrypted = QByteArray(key128Size + shareHashData.size(), Qt::Uninitialized);
- hashSha1(shareHashData.constData(), shareHashData.size(), shareHashEncrypted.data());
-
- //// Mix in channel access hash to the first 64 bits of SHA1 of data.
- //*reinterpret_cast(shareHashEncrypted.data()) ^= channelAccessHash;
-
- // Encrypt data.
- if (!session->local().encrypt(shareHashData.constData(), shareHashEncrypted.data() + key128Size, shareHashData.size(), shareHashEncrypted.constData())) {
- return url;
- }
-
- auto shareHash = shareHashEncrypted.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
- auto shareUrl = u"tg://share_game_score?hash="_q + QString::fromLatin1(shareHash);
-
- auto shareComponent = u"tgShareScoreUrl="_q + qthelp::url_encode(shareUrl);
-
- auto hashPosition = url.indexOf('#');
- if (hashPosition < 0) {
- return url + '#' + shareComponent;
- }
- auto hash = url.mid(hashPosition + 1);
- if (hash.indexOf('=') >= 0 || hash.indexOf('?') >= 0) {
- return url + '&' + shareComponent;
- }
- if (!hash.isEmpty()) {
- return url + '?' + shareComponent;
- }
- return url + shareComponent;
-}
-
ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(
const std::vector> &result,
const MessageIdsList &msgIds) {
@@ -1612,9 +1563,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
}
void FastShareMessage(
- not_null controller,
+ std::shared_ptr show,
not_null item) {
- const auto show = controller->uiShow();
const auto history = item->history();
const auto owner = &history->owner();
const auto session = &history->session();
@@ -1643,7 +1593,7 @@ void FastShareMessage(
}
if (item->hasDirectLink()) {
using namespace HistoryView;
- CopyPostLink(controller, item->fullId(), Context::History);
+ CopyPostLink(show, item->fullId(), Context::History);
} else if (const auto bot = item->getMessageBot()) {
if (const auto media = item->media()) {
if (const auto game = media->game()) {
@@ -1675,23 +1625,27 @@ void FastShareMessage(
auto copyLinkCallback = canCopyLink
? Fn(std::move(copyCallback))
: Fn();
- controller->show(
- Box(ShareBox::Descriptor{
- .session = session,
- .copyCallback = std::move(copyLinkCallback),
- .submitCallback = ShareBox::DefaultForwardCallback(
- show,
- history,
- msgIds),
- .filterCallback = std::move(filterCallback),
- .forwardOptions = {
- .sendersCount = ItemsForwardSendersCount(items),
- .captionsCount = ItemsForwardCaptionsCount(items),
- .show = !hasOnlyForcedForwardedInfo,
- },
- .premiumRequiredError = SharePremiumRequiredError(),
- }),
- Ui::LayerOption::CloseOther);
+ show->show(Box(ShareBox::Descriptor{
+ .session = session,
+ .copyCallback = std::move(copyLinkCallback),
+ .submitCallback = ShareBox::DefaultForwardCallback(
+ show,
+ history,
+ msgIds),
+ .filterCallback = std::move(filterCallback),
+ .forwardOptions = {
+ .sendersCount = ItemsForwardSendersCount(items),
+ .captionsCount = ItemsForwardCaptionsCount(items),
+ .show = !hasOnlyForcedForwardedInfo,
+ },
+ .premiumRequiredError = SharePremiumRequiredError(),
+ }), Ui::LayerOption::CloseOther);
+}
+
+void FastShareMessage(
+ not_null controller,
+ not_null item) {
+ FastShareMessage(controller->uiShow(), item);
}
void FastShareLink(
@@ -1793,111 +1747,3 @@ auto SharePremiumRequiredError()
-> Fn)> {
return WritePremiumRequiredError;
}
-
-void ShareGameScoreByHash(
- not_null controller,
- const QString &hash) {
- auto &session = controller->session();
- auto key128Size = 0x10;
-
- auto hashEncrypted = QByteArray::fromBase64(hash.toLatin1(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
- if (hashEncrypted.size() <= key128Size || (hashEncrypted.size() != key128Size + 0x20)) {
- controller->show(
- Ui::MakeInformBox(tr::lng_confirm_phone_link_invalid()),
- Ui::LayerOption::CloseOther);
- return;
- }
-
- // Decrypt data.
- auto hashData = QByteArray(hashEncrypted.size() - key128Size, Qt::Uninitialized);
- if (!session.local().decrypt(hashEncrypted.constData() + key128Size, hashData.data(), hashEncrypted.size() - key128Size, hashEncrypted.constData())) {
- return;
- }
-
- // Count SHA1() of data.
- char dataSha1[20] = { 0 };
- hashSha1(hashData.constData(), hashData.size(), dataSha1);
-
- //// Mix out channel access hash from the first 64 bits of SHA1 of data.
- //auto channelAccessHash = *reinterpret_cast(hashEncrypted.data()) ^ *reinterpret_cast(dataSha1);
-
- //// Check next 64 bits of SHA1() of data.
- //auto skipSha1Part = sizeof(channelAccessHash);
- //if (memcmp(dataSha1 + skipSha1Part, hashEncrypted.constData() + skipSha1Part, key128Size - skipSha1Part) != 0) {
- // Ui::show(Box(tr::lng_share_wrong_user(tr::now)));
- // return;
- //}
-
- // Check 128 bits of SHA1() of data.
- if (memcmp(dataSha1, hashEncrypted.constData(), key128Size) != 0) {
- controller->show(
- Ui::MakeInformBox(tr::lng_share_wrong_user()),
- Ui::LayerOption::CloseOther);
- return;
- }
-
- auto hashDataInts = reinterpret_cast(hashData.data());
- if (hashDataInts[0] != session.userId().bare) {
- controller->show(
- Ui::MakeInformBox(tr::lng_share_wrong_user()),
- Ui::LayerOption::CloseOther);
- return;
- }
-
- const auto peerId = PeerId(hashDataInts[1]);
- const auto channelAccessHash = hashDataInts[3];
- if (!peerIsChannel(peerId) && channelAccessHash) {
- // If there is no channel id, there should be no channel access_hash.
- controller->show(
- Ui::MakeInformBox(tr::lng_share_wrong_user()),
- Ui::LayerOption::CloseOther);
- return;
- }
-
- const auto msgId = MsgId(int64(hashDataInts[2]));
- if (const auto item = session.data().message(peerId, msgId)) {
- FastShareMessage(controller, item);
- } else {
- const auto weak = base::make_weak(controller);
- const auto resolveMessageAndShareScore = crl::guard(weak, [=](
- PeerData *peer) {
- auto done = crl::guard(weak, [=] {
- const auto item = weak->session().data().message(
- peerId,
- msgId);
- if (item) {
- FastShareMessage(weak.get(), item);
- } else {
- weak->show(
- Ui::MakeInformBox(tr::lng_edit_deleted()),
- Ui::LayerOption::CloseOther);
- }
- });
- auto &api = weak->session().api();
- api.requestMessageData(peer, msgId, std::move(done));
- });
-
- const auto peer = peerIsChannel(peerId)
- ? controller->session().data().peerLoaded(peerId)
- : nullptr;
- if (peer || !peerIsChannel(peerId)) {
- resolveMessageAndShareScore(peer);
- } else {
- const auto owner = &controller->session().data();
- controller->session().api().request(MTPchannels_GetChannels(
- MTP_vector(
- 1,
- MTP_inputChannel(
- MTP_long(peerToChannel(peerId).bare),
- MTP_long(channelAccessHash)))
- )).done([=](const MTPmessages_Chats &result) {
- result.match([&](const auto &data) {
- owner->processChats(data.vchats());
- });
- if (const auto peer = owner->peerLoaded(peerId)) {
- resolveMessageAndShareScore(peer);
- }
- }).send();
- }
- }
-}
diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h
index 32e824b15cff4..d0bf28ce91e2c 100644
--- a/Telegram/SourceFiles/boxes/share_box.h
+++ b/Telegram/SourceFiles/boxes/share_box.h
@@ -59,13 +59,11 @@ class SlideWrap;
class PopupMenu;
} // namespace Ui
-QString AppendShareGameScoreUrl(
- not_null session,
- const QString &url,
- const FullMsgId &fullId);
-void ShareGameScoreByHash(
- not_null controller,
- const QString &hash);
+class ShareBox;
+
+void FastShareMessage(
+ std::shared_ptr show,
+ not_null item);
void FastShareMessage(
not_null controller,
not_null item);
diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style
index cc009ad43b068..a133bba683df6 100644
--- a/Telegram/SourceFiles/calls/calls.style
+++ b/Telegram/SourceFiles/calls/calls.style
@@ -1393,7 +1393,6 @@ groupCallScheduleDateField: InputField(groupCallField) {
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(top);
- font: font(14px);
}
groupCallScheduleTimeField: InputField(groupCallScheduleDateField) {
textBg: groupCallMembersBg;
diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp
index 527839363b0be..9f12728596ed6 100644
--- a/Telegram/SourceFiles/calls/calls_call.cpp
+++ b/Telegram/SourceFiles/calls/calls_call.cpp
@@ -706,7 +706,8 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
}
}
if (data.is_need_rating() && _id && _accessHash) {
- const auto window = Core::App().windowFor(_user);
+ const auto window = Core::App().windowFor(
+ Window::SeparateId(_user));
const auto session = &_user->session();
const auto callId = _id;
const auto callAccessHash = _accessHash;
@@ -1402,7 +1403,8 @@ void Call::handleRequestError(const QString &error) {
_user->name())
: QString();
if (!inform.isEmpty()) {
- if (const auto window = Core::App().windowFor(_user)) {
+ if (const auto window = Core::App().windowFor(
+ Window::SeparateId(_user))) {
window->show(Ui::MakeInformBox(inform));
} else {
Ui::show(Ui::MakeInformBox(inform));
@@ -1420,7 +1422,8 @@ void Call::handleControllerError(const QString &error) {
? tr::lng_call_error_audio_io(tr::now)
: QString();
if (!inform.isEmpty()) {
- if (const auto window = Core::App().windowFor(_user)) {
+ if (const auto window = Core::App().windowFor(
+ Window::SeparateId(_user))) {
window->show(Ui::MakeInformBox(inform));
} else {
Ui::show(Ui::MakeInformBox(inform));
diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
index 9e967b50e60b0..84c33613104ba 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
@@ -383,20 +383,14 @@ void Panel::initWindow() {
&& _fullScreenOrMaximized.current()) {
toggleFullScreen();
}
+ } else if (e->type() == QEvent::WindowStateChange && _call->rtmp()) {
+ const auto state = window()->windowState();
+ _fullScreenOrMaximized = (state & Qt::WindowFullScreen)
+ || (state & Qt::WindowMaximized);
}
return base::EventFilterResult::Continue;
});
- if (_call->rtmp()) {
- QObject::connect(
- window()->windowHandle(),
- &QWindow::windowStateChanged,
- [=](Qt::WindowState state) {
- _fullScreenOrMaximized = (state == Qt::WindowFullScreen)
- || (state == Qt::WindowMaximized);
- });
- }
-
window()->setBodyTitleArea([=](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag;
const auto titleRect = QRect(
diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp
index 93c5ecf1ed01b..cf1375268bf45 100644
--- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp
+++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp
@@ -585,7 +585,6 @@ void ChooseSourceProcess::setupSourcesGeometry() {
void ChooseSourceProcess::setupGeometryWithParent(
not_null parent) {
- _window->createWinId();
const auto parentScreen = [&] {
if (const auto screen = QGuiApplication::screenAt(
parent->geometry().center())) {
@@ -595,7 +594,12 @@ void ChooseSourceProcess::setupGeometryWithParent(
}();
const auto myScreen = _window->screen();
if (parentScreen && myScreen != parentScreen) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ _window->setScreen(parentScreen);
+#else // Qt >= 6.0.0
+ _window->createWinId();
_window->windowHandle()->setScreen(parentScreen);
+#endif // Qt < 6.0.0
}
_window->setFixedSize(_fixedSize);
_window->move(
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index b3f7e73c978f0..0b1290dc8d58d 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -71,6 +71,7 @@ ComposeIcons {
menuSpoilerOff: icon;
menuBelow: icon;
menuAbove: icon;
+ menuPrice: icon;
stripBubble: icon;
stripExpandPanel: icon;
@@ -610,6 +611,7 @@ defaultComposeIcons: ComposeIcons {
menuSpoilerOff: menuIconSpoilerOff;
menuBelow: menuIconBelow;
menuAbove: menuIconAbove;
+ menuPrice: menuIconEarn;
stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg },
@@ -989,8 +991,33 @@ historyUnreadReactions: TwoIconButton(historyToDown) {
}
historyUnreadThingsSkip: 4px;
+historyQuoteStyle: QuoteStyle(defaultQuoteStyle) {
+ padding: margins(10px, 2px, 4px, 2px);
+ verticalSkip: 4px;
+ outline: 3px;
+ outlineShift: 2px;
+ radius: 5px;
+}
+historyTextStyle: TextStyle(defaultTextStyle) {
+ blockquote: QuoteStyle(historyQuoteStyle) {
+ padding: margins(10px, 2px, 20px, 2px);
+ icon: icon{{ "chat/mini_quote", windowFg }};
+ iconPosition: point(4px, 4px);
+ expand: icon{{ "intro_country_dropdown", windowFg }};
+ expandPosition: point(6px, 4px);
+ collapse: icon{{ "intro_country_dropdown-flip_vertical", windowFg }};
+ collapsePosition: point(6px, 4px);
+ }
+ pre: QuoteStyle(historyQuoteStyle) {
+ header: 20px;
+ headerPosition: point(10px, 2px);
+ scrollable: true;
+ icon: icon{{ "chat/mini_copy", windowFg }};
+ iconPosition: point(4px, 2px);
+ }
+}
historyComposeField: InputField(defaultInputField) {
- font: normalFont;
+ style: historyTextStyle;
textMargins: margins(0px, 0px, 0px, 0px);
textAlign: align(left);
textFg: historyComposeAreaFg;
@@ -1381,3 +1408,18 @@ editTagField: InputField(defaultInputField) {
editTagLimit: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
+
+paidStarIcon: icon {{ "settings/premium/star", creditsBg1 }};
+paidStarIconTop: 7px;
+paidAmountAbout: FlatLabel(defaultFlatLabel) {
+ minWidth: 256px;
+ textFg: windowSubTextFg;
+}
+paidTagLabel: FlatLabel(defaultFlatLabel) {
+ textFg: radialFg;
+ palette: TextPalette(defaultTextPalette) {
+ linkFg: creditsBg1;
+ }
+ style: semiboldTextStyle;
+}
+paidTagPadding: margins(16px, 6px, 16px, 6px);
diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp b/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp
index 01bc33b8c524a..ba904b77c26a4 100644
--- a/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp
+++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp
@@ -30,15 +30,15 @@ ResolveWindow ResolveWindowDefault() {
return (Window::SessionController*)nullptr;
};
auto &app = Core::App();
+ const auto account = not_null(&session->account());
if (const auto a = check(app.activeWindow())) {
return a;
} else if (const auto b = check(app.activePrimaryWindow())) {
return b;
- } else if (const auto c = check(app.windowFor(&session->account()))) {
+ } else if (const auto c = check(app.windowFor(account))) {
return c;
- } else if (const auto d = check(
- app.ensureSeparateWindowForAccount(
- &session->account()))) {
+ } else if (const auto d = check(app.ensureSeparateWindowFor(
+ account))) {
return d;
}
return nullptr;
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp
index 779bfb4d51d87..5d8be61445050 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.cpp
+++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp
@@ -14,11 +14,13 @@ For license and copyright information please follow this link:
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "base/event_filter.h"
+#include "ui/chat/chat_style.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "core/shortcuts.h"
#include "core/application.h"
#include "core/core_settings.h"
+#include "core/ui_integration.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h"
@@ -40,6 +42,7 @@ For license and copyright information please follow this link:
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
+#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h"
#include
@@ -58,6 +61,7 @@ using EditLinkSelection = Ui::InputField::EditLinkSelection;
constexpr auto kParseLinksTimeout = crl::time(1000);
constexpr auto kTypesDuration = 4 * crl::time(1000);
+constexpr auto kCodeLanguageLimit = 32;
// For mention / custom emoji tags save and validate selfId,
// ignore tags for different users.
@@ -222,6 +226,51 @@ void EditLinkBox(
}, text->lifetime());
}
+void EditCodeLanguageBox(
+ not_null box,
+ QString now,
+ Fn save) {
+ Expects(save != nullptr);
+
+ box->setTitle(tr::lng_formatting_code_title());
+ box->addRow(object_ptr(
+ box,
+ tr::lng_formatting_code_language(),
+ st::settingsAddReplyLabel));
+ const auto field = box->addRow(object_ptr(
+ box,
+ st::settingsAddReplyField,
+ tr::lng_formatting_code_auto(),
+ now.trimmed()));
+ box->setFocusCallback([=] {
+ field->setFocusFast();
+ });
+ field->selectAll();
+ field->setMaxLength(kCodeLanguageLimit);
+
+ Ui::AddLengthLimitLabel(field, kCodeLanguageLimit);
+
+ const auto callback = [=] {
+ const auto name = field->getLastText().trimmed();
+ const auto check = QRegularExpression("^[a-zA-Z0-9\\+\\-]*$");
+ if (check.match(name).hasMatch()) {
+ auto weak = Ui::MakeWeak(box);
+ save(name);
+ if (const auto strong = weak.data()) {
+ strong->closeBox();
+ }
+ } else {
+ field->showError();
+ }
+ };
+ field->submits(
+ ) | rpl::start_with_next(callback, field->lifetime());
+ box->addButton(tr::lng_settings_save(), callback);
+ box->addButton(tr::lng_cancel(), [=] {
+ box->closeBox();
+ });
+}
+
TextWithEntities StripSupportHashtag(TextWithEntities text) {
static const auto expression = QRegularExpression(
u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q,
@@ -274,15 +323,24 @@ TextWithTags PrepareEditText(not_null item) {
bool EditTextChanged(
not_null item,
- const TextWithTags &updated) {
+ TextWithTags updated) {
const auto original = PrepareEditText(item);
+ auto originalWithEntities = TextWithEntities{
+ std::move(original.text),
+ TextUtilities::ConvertTextTagsToEntities(original.tags)
+ };
+ auto updatedWithEntities = TextWithEntities{
+ std::move(updated.text),
+ TextUtilities::ConvertTextTagsToEntities(updated.tags)
+ };
+ TextUtilities::PrepareForSending(originalWithEntities, 0);
+ TextUtilities::PrepareForSending(updatedWithEntities, 0);
+
// Tags can be different for the same entities, because for
// animated emoji each tag contains a different random number.
// So we compare entities instead of tags.
- return (original.text != updated.text)
- || (TextUtilities::ConvertTextTagsToEntities(original.tags)
- != TextUtilities::ConvertTextTagsToEntities(updated.tags));
+ return originalWithEntities != updatedWithEntities;
}
Fn save)> DefaultEditLanguageCallback(
+ std::shared_ptr show) {
+ return [=](QString now, Fn save) {
+ show->showBox(Box(EditCodeLanguageBox, now, save));
+ };
+}
+
void InitMessageFieldHandlers(
not_null session,
std::shared_ptr show,
@@ -329,12 +394,16 @@ void InitMessageFieldHandlers(
const style::InputField *fieldStyle) {
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji));
- const auto paused = [customEmojiPaused] {
+ field->setCustomTextContext([=](Fn repaint) {
+ return std::any(Core::MarkedTextContext{
+ .session = session,
+ .customEmojiRepaint = std::move(repaint),
+ });
+ }, [customEmojiPaused] {
return On(PowerSaving::kEmojiChat) || customEmojiPaused();
- };
- field->setCustomEmojiFactory(
- session->data().customEmojiManager().factory(),
- std::move(customEmojiPaused));
+ }, [customEmojiPaused] {
+ return On(PowerSaving::kChatSpoiler) || customEmojiPaused();
+ });
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
@@ -342,8 +411,18 @@ void InitMessageFieldHandlers(
if (show) {
field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle));
+ field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, fieldStyle != nullptr);
}
+ const auto style = field->lifetime().make_state(
+ session->colorIndicesValue());
+ field->setPreCache([=] {
+ return style->messageStyle(false, false).preCache.get();
+ });
+ field->setBlockquoteCache([=] {
+ const auto colorIndex = session->user()->colorIndex();
+ return style->coloredQuoteCache(false, colorIndex).get();
+ });
}
[[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {
@@ -569,12 +648,26 @@ void InitMessageFieldFade(
Ui::DestroyChild(b.data());
}, topFade->lifetime());
- topFade->show();
- bottomFade->showOn(
- field->scrollTop().value(
- ) | rpl::map([field, descent = field->st().font->descent](int scroll) {
- return (scroll + descent) < field->scrollTopMax();
- }) | rpl::distinct_until_changed());
+ const auto descent = field->st().style.font->descent;
+ rpl::merge(
+ field->changes(),
+ field->scrollTop().changes() | rpl::to_empty,
+ field->sizeValue() | rpl::to_empty
+ ) | rpl::start_with_next([=] {
+ // InputField::changes fires before the auto-resize is being applied,
+ // so for the scroll values to be accurate we enqueue the check.
+ InvokeQueued(field, [=] {
+ const auto topHidden = !field->scrollTop().current();
+ if (topFade->isHidden() != topHidden) {
+ topFade->setVisible(!topHidden);
+ }
+ const auto adjusted = field->scrollTop().current() + descent;
+ const auto bottomHidden = (adjusted >= field->scrollTopMax());
+ if (bottomFade->isHidden() != bottomHidden) {
+ bottomFade->setVisible(!bottomHidden);
+ }
+ });
+ }, topFade->lifetime());
}
InlineBotQuery ParseInlineBotQuery(
@@ -766,11 +859,7 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
const auto text = static_cast(event)->text();
if (!text.isEmpty() && text.size() < 3) {
const auto ch = text[0];
- if (false
- || ch == '\n'
- || ch == '\r'
- || ch.isSpace()
- || ch == QChar::LineSeparator) {
+ if (IsSpace(ch)) {
_timer.callOnce(0);
}
}
@@ -1097,7 +1186,9 @@ base::unique_qptr PremiumRequiredSendRestriction(
const auto margins = (st.textMargins + st.placeholderMargins);
const auto available = width - margins.left() - margins.right();
label->resizeToWidth(available);
- label->moveToLeft(margins.left(), margins.top(), width);
+ const auto height = label->height() + link->height();
+ const auto top = (raw->height() - height) / 2;
+ label->moveToLeft(margins.left(), top, width);
link->move(
(width - link->width()) / 2,
label->y() + label->height());
@@ -1117,8 +1208,8 @@ void SelectTextInFieldWithMargins(
auto textCursor = field->textCursor();
// Try to set equal margins for top and bottom sides.
const auto charsCountInLine = field->width()
- / field->st().font->width('W');
- const auto linesCount = (field->height() / field->st().font->height);
+ / field->st().style.font->width('W');
+ const auto linesCount = field->height() / field->st().style.font->height;
const auto selectedLines = (selection.to - selection.from)
/ charsCountInLine;
constexpr auto kMinDiff = ushort(3);
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h
index 1e67642a767a7..041b54d661eb4 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.h
+++ b/Telegram/SourceFiles/chat_helpers/message_field.h
@@ -35,13 +35,14 @@ class Show;
namespace Ui {
class PopupMenu;
+class Show;
} // namespace Ui
[[nodiscard]] QString PrepareMentionTag(not_null user);
[[nodiscard]] TextWithTags PrepareEditText(not_null item);
[[nodiscard]] bool EditTextChanged(
not_null item,
- const TextWithTags &updated);
+ TextWithTags updated);
Fn show,
not_null field,
const style::InputField *fieldStyle = nullptr);
+Fn save)> DefaultEditLanguageCallback(
+ std::shared_ptr show);
void InitMessageFieldHandlers(
not_null session,
std::shared_ptr show, // may be null
diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index d0470dfc7288f..79dfd0977bcfc 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -8,6 +8,7 @@ For license and copyright information please follow this link:
#include "core/application.h"
#include "data/data_abstract_structure.h"
+#include "data/data_forum.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
@@ -18,6 +19,7 @@ For license and copyright information please follow this link:
#include "base/battery_saving.h"
#include "base/event_filter.h"
#include "base/concurrent_timer.h"
+#include "base/options.h"
#include "base/qt_signal_producer.h"
#include "base/timer.h"
#include "base/unixtime.h"
@@ -91,6 +93,7 @@ For license and copyright information please follow this link:
#include "payments/payments_checkout_process.h"
#include "export/export_manager.h"
#include "webrtc/webrtc_environment.h"
+#include "window/window_separate_id.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "boxes/abstract_box.h"
@@ -118,7 +121,7 @@ constexpr auto kFileOpenTimeoutMs = crl::time(1000);
LaunchState GlobalLaunchState/* = LaunchState::Running*/;
void SetCrashAnnotationsGL() {
-#ifdef Q_OS_WIN
+#ifdef DESKTOP_APP_USE_ANGLE
CrashReports::SetAnnotation("OpenGL ANGLE", [] {
if (Core::App().settings().disableOpenGL()) {
return "Disabled";
@@ -131,17 +134,25 @@ void SetCrashAnnotationsGL() {
}
Unexpected("Ui::GL::CurrentANGLE value in SetupANGLE.");
}());
-#else // Q_OS_WIN
+#else // DESKTOP_APP_USE_ANGLE
CrashReports::SetAnnotation(
"OpenGL",
Core::App().settings().disableOpenGL() ? "Disabled" : "Enabled");
-#endif // Q_OS_WIN
+#endif // DESKTOP_APP_USE_ANGLE
}
+base::options::toggle OptionSkipUrlSchemeRegister({
+ .id = kOptionSkipUrlSchemeRegister,
+ .name = "Skip URL scheme register",
+ .description = "Don't re-register tg:// URL scheme on autoupdate.",
+});
+
} // namespace
Application *Application::Instance = nullptr;
+const char kOptionSkipUrlSchemeRegister[] = "skip-url-scheme-register";
+
struct Application::Private {
base::Timer quitTimer;
UiIntegration uiIntegration;
@@ -209,8 +220,7 @@ Application::~Application() {
setLastActiveWindow(nullptr);
_windowInSettings = _lastActivePrimaryWindow = nullptr;
_closingAsyncWindows.clear();
- _secondaryWindows.clear();
- _primaryWindows.clear();
+ _windows.clear();
_mediaView = nullptr;
_notifications->clearAllFast();
@@ -261,7 +271,7 @@ void Application::run() {
refreshGlobalProxy(); // Depends on app settings being read.
if (const auto old = Local::oldSettingsVersion(); old < AppVersion) {
- InvokeQueued(this, [] { RegisterUrlScheme(); });
+ autoRegisterUrlScheme();
Platform::NewVersionLaunched(old);
}
@@ -315,8 +325,8 @@ void Application::run() {
// Create mime database, so it won't be slow later.
QMimeDatabase().mimeTypeForName(u"text/plain"_q);
- _primaryWindows.emplace(nullptr, std::make_unique());
- setLastActiveWindow(_primaryWindows.front().second.get());
+ _windows.emplace(nullptr, std::make_unique());
+ setLastActiveWindow(_windows.front().second.get());
_windowInSettings = _lastActivePrimaryWindow = _lastActiveWindow;
_domain->activeChanges(
@@ -404,8 +414,14 @@ void Application::run() {
processCreatedWindow(_lastActivePrimaryWindow);
}
+void Application::autoRegisterUrlScheme() {
+ if (!OptionSkipUrlSchemeRegister.value()) {
+ InvokeQueued(this, [] { RegisterUrlScheme(); });
+ }
+}
+
void Application::showAccount(not_null account) {
- if (const auto separate = separateWindowForAccount(account)) {
+ if (const auto separate = separateWindowFor(account)) {
_lastActivePrimaryWindow = separate;
separate->activate();
} else if (const auto last = activePrimaryWindow()) {
@@ -413,13 +429,13 @@ void Application::showAccount(not_null account) {
}
}
-void Application::checkWindowAccount(not_null window) {
- const auto account = window->maybeAccount();
- for (auto &[key, existing] : _primaryWindows) {
- if (existing.get() == window && key != account) {
+void Application::checkWindowId(not_null window) {
+ const auto id = window->id();
+ for (auto &[existingId, existing] : _windows) {
+ if (existing.get() == window && existingId != id) {
auto found = std::move(existing);
- _primaryWindows.remove(key);
- _primaryWindows.emplace(account, std::move(found));
+ _windows.remove(existingId);
+ _windows.emplace(id, std::move(found));
break;
}
}
@@ -495,10 +511,7 @@ void Application::startSystemDarkModeViewer() {
void Application::enumerateWindows(Fn)> callback) const {
- for (const auto &window : ranges::views::values(_primaryWindows)) {
- callback(window.get());
- }
- for (const auto &window : ranges::views::values(_secondaryWindows)) {
+ for (const auto &window : ranges::views::values(_windows)) {
callback(window.get());
}
}
@@ -607,10 +620,7 @@ void Application::clearEmojiSourceImages() {
}
bool Application::isActiveForTrayMenu() const {
- return ranges::any_of(ranges::views::values(_primaryWindows), [=](
- const std::unique_ptr &controller) {
- return controller->widget()->isActiveForTrayMenu();
- }) || ranges::any_of(ranges::views::values(_secondaryWindows), [=](
+ return ranges::any_of(ranges::views::values(_windows), [=](
const std::unique_ptr &controller) {
return controller->widget()->isActiveForTrayMenu();
});
@@ -1287,44 +1297,36 @@ Window::Controller *Application::activePrimaryWindow() const {
return _lastActivePrimaryWindow;
}
-Window::Controller *Application::separateWindowForAccount(
- not_null account) const {
- for (const auto &[openedAccount, window] : _primaryWindows) {
- if (openedAccount == account.get()) {
+Window::Controller *Application::separateWindowFor(
+ Window::SeparateId id) const {
+ for (const auto &[existingId, window] : _windows) {
+ if (existingId == id) {
return window.get();
}
}
return nullptr;
}
-Window::Controller *Application::separateWindowForPeer(
- not_null peer) const {
- for (const auto &[history, window] : _secondaryWindows) {
- if (history->peer == peer) {
- return window.get();
- }
- }
- return nullptr;
-}
-
-Window::Controller *Application::ensureSeparateWindowForPeer(
- not_null peer,
+Window::Controller *Application::ensureSeparateWindowFor(
+ Window::SeparateId id,
MsgId showAtMsgId) {
const auto activate = [&](not_null window) {
window->activate();
return window;
};
-
- if (const auto existing = separateWindowForPeer(peer)) {
- existing->sessionController()->showPeerHistory(
- peer,
- Window::SectionShow::Way::ClearStack,
- showAtMsgId);
+ if (const auto existing = separateWindowFor(id)) {
+ if (id.thread && id.type == Window::SeparateType::Chat) {
+ existing->sessionController()->showThread(
+ id.thread,
+ showAtMsgId,
+ Window::SectionShow::Way::ClearStack);
+ }
return activate(existing);
}
- const auto result = _secondaryWindows.emplace(
- peer->owner().history(peer),
- std::make_unique(peer, showAtMsgId)
+
+ const auto result = _windows.emplace(
+ id,
+ std::make_unique(id, showAtMsgId)
).first->second.get();
processCreatedWindow(result);
result->firstShow();
@@ -1332,39 +1334,52 @@ Window::Controller *Application::ensureSeparateWindowForPeer(
return activate(result);
}
-Window::Controller *Application::ensureSeparateWindowForAccount(
- not_null account) {
- const auto activate = [&](not_null window) {
- window->activate();
- return window;
- };
-
- if (const auto existing = separateWindowForAccount(account)) {
- return activate(existing);
+Window::Controller *Application::windowFor(Window::SeparateId id) const {
+ if (const auto separate = separateWindowFor(id)) {
+ return separate;
+ } else if (id && id.primary()) {
+ return windowFor(not_null(id.account));
}
- const auto result = _primaryWindows.emplace(
- account,
- std::make_unique(account)
- ).first->second.get();
- processCreatedWindow(result);
- result->firstShow();
- result->finishFirstShow();
- return activate(result);
+ return activePrimaryWindow();
}
-Window::Controller *Application::windowFor(not_null peer) const {
- if (const auto separate = separateWindowForPeer(peer)) {
+Window::Controller *Application::windowForShowingHistory(
+ not_null peer) const {
+ if (const auto separate = separateWindowFor(peer)) {
return separate;
}
- return windowFor(&peer->account());
+ auto result = (Window::Controller*)nullptr;
+ enumerateWindows([&](not_null 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::windowFor(
- not_null account) const {
- if (const auto separate = separateWindowForAccount(account)) {
+Window::Controller *Application::windowForShowingForum(
+ not_null forum) const {
+ const auto id = Window::SeparateId(
+ Window::SeparateType::Forum,
+ forum->history());
+ if (const auto separate = separateWindowFor(id)) {
return separate;
}
- return activePrimaryWindow();
+ auto result = (Window::Controller*)nullptr;
+ enumerateWindows([&](not_null window) {
+ if (const auto controller = window->sessionController()) {
+ const auto current = controller->shownForum().current();
+ if (forum == current) {
+ result = window;
+ }
+ }
+ });
+ return result;
}
Window::Controller *Application::findWindow(
@@ -1373,14 +1388,9 @@ Window::Controller *Application::findWindow(
if (_lastActiveWindow && _lastActiveWindow->widget() == window) {
return _lastActiveWindow;
}
- for (const auto &[account, primary] : _primaryWindows) {
- if (primary->widget() == window) {
- return primary.get();
- }
- }
- for (const auto &[history, secondary] : _secondaryWindows) {
- if (secondary->widget() == window) {
- return secondary.get();
+ for (const auto &[id, controller] : _windows) {
+ if (controller->widget() == window) {
+ return controller.get();
}
}
return nullptr;
@@ -1392,10 +1402,11 @@ Window::Controller *Application::activeWindow() const {
bool Application::closeNonLastAsync(not_null window) {
const auto hasOther = [&] {
- for (const auto &[account, primary] : _primaryWindows) {
- if (!_closingAsyncWindows.contains(primary.get())
- && primary.get() != window
- && primary->maybeSession()) {
+ for (const auto &[id, controller] : _windows) {
+ if (id.primary()
+ && !_closingAsyncWindows.contains(controller.get())
+ && controller.get() != window
+ && controller->maybeSession()) {
return true;
}
}
@@ -1457,10 +1468,10 @@ void Application::closeWindow(not_null window) {
: nullptr;
const auto next = nextFromStack
? nextFromStack
- : (_primaryWindows.front().second.get() != window)
- ? _primaryWindows.front().second.get()
- : (_primaryWindows.back().second.get() != window)
- ? _primaryWindows.back().second.get()
+ : (_windows.front().second.get() != window)
+ ? _windows.front().second.get()
+ : (_windows.back().second.get() != window)
+ ? _windows.back().second.get()
: nullptr;
Assert(next != window);
@@ -1481,20 +1492,12 @@ void Application::closeWindow(not_null window) {
}
}
_closingAsyncWindows.remove(window);
- for (auto i = begin(_primaryWindows); i != end(_primaryWindows);) {
+ for (auto i = begin(_windows); i != end(_windows);) {
if (i->second.get() == window) {
Assert(_lastActiveWindow != window);
Assert(_lastActivePrimaryWindow != window);
Assert(_windowInSettings != window);
- i = _primaryWindows.erase(i);
- } else {
- ++i;
- }
- }
- for (auto i = begin(_secondaryWindows); i != end(_secondaryWindows);) {
- if (i->second.get() == window) {
- Assert(_lastActiveWindow != window);
- i = _secondaryWindows.erase(i);
+ i = _windows.erase(i);
} else {
++i;
}
@@ -1502,36 +1505,34 @@ void Application::closeWindow(not_null window) {
const auto account = domain().started()
? &domain().active()
: nullptr;
- if (account && !_primaryWindows.contains(account) && _lastActiveWindow) {
+ if (account
+ && !_windows.contains(Window::SeparateId(account))
+ && _lastActiveWindow) {
domain().activate(&_lastActiveWindow->account());
}
}
void Application::closeChatFromWindows(not_null peer) {
- if (const auto window = windowFor(peer)
- ; window && !window->isPrimary()) {
- closeWindow(window);
- }
- for (const auto &[history, window] : _secondaryWindows) {
- if (const auto session = window->sessionController()) {
- if (session->activeChatCurrent().peer() == peer) {
- session->showPeerHistory(
- window->singlePeer()->id,
- Window::SectionShow::Way::ClearStack);
- }
- }
- }
- if (const auto window = windowFor(&peer->account())) {
- if (const auto primary = window->sessionController()) {
- if (primary->activeChatCurrent().peer() == peer) {
- primary->clearSectionStack();
- }
- if (const auto forum = primary->shownForum().current()) {
- if (peer->forum() == forum) {
- primary->closeForum();
+ const auto closeOne = [&] {
+ for (const auto &[id, window] : _windows) {
+ if (id.thread && id.thread->peer() == peer) {
+ closeWindow(window.get());
+ return true;
+ } else if (const auto controller = window->sessionController()) {
+ if (controller->activeChatCurrent().peer() == peer) {
+ controller->showByInitialId();
+ }
+ if (const auto forum = controller->shownForum().current()) {
+ if (peer->forum() == forum) {
+ controller->closeForum();
+ }
}
}
}
+ return false;
+ };
+
+ while (closeOne()) {
}
}
@@ -1737,11 +1738,8 @@ void Application::quitPreventFinished() {
}
void Application::quitDelayed() {
- for (const auto &[account, window] : _primaryWindows) {
- window->widget()->hide();
- }
- for (const auto &[history, window] : _secondaryWindows) {
- window->widget()->hide();
+ for (const auto &[id, controller] : _windows) {
+ controller->widget()->hide();
}
if (!_private->quitTimer.isActive()) {
_private->quitTimer.setCallback([] { Sandbox::QuitWhenStarted(); });
diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h
index 08a29a35e7762..eca07e75193ff 100644
--- a/Telegram/SourceFiles/core/application.h
+++ b/Telegram/SourceFiles/core/application.h
@@ -7,9 +7,10 @@ For license and copyright information please follow this link:
*/
#pragma once
+#include "base/timer.h"
#include "mtproto/mtproto_auth_key.h"
#include "mtproto/mtproto_proxy_data.h"
-#include "base/timer.h"
+#include "window/window_separate_id.h"
class History;
@@ -29,11 +30,9 @@ namespace Window {
class Controller;
} // namespace Window
-namespace Window {
-namespace Notifications {
+namespace Window::Notifications {
class System;
-} // namespace Notifications
-} // namespace Window
+} // namespace Window::Notifications
namespace ChatHelpers {
class EmojiKeywords;
@@ -127,6 +126,8 @@ enum class QuitReason {
QtQuitEvent,
};
+extern const char kOptionSkipUrlSchemeRegister[];
+
class Application final : public QObject {
public:
struct ProxyChange {
@@ -170,19 +171,17 @@ class Application final : public QObject {
not_null widget) const;
[[nodiscard]] Window::Controller *activeWindow() const;
[[nodiscard]] Window::Controller *activePrimaryWindow() const;
- [[nodiscard]] Window::Controller *separateWindowForAccount(
- not_null account) const;
- [[nodiscard]] Window::Controller *separateWindowForPeer(
- not_null peer) const;
- Window::Controller *ensureSeparateWindowForPeer(
- not_null peer,
- MsgId showAtMsgId);
- Window::Controller *ensureSeparateWindowForAccount(
- not_null account);
+ [[nodiscard]] Window::Controller *separateWindowFor(
+ Window::SeparateId id) const;
+ Window::Controller *ensureSeparateWindowFor(
+ Window::SeparateId id,
+ MsgId showAtMsgId = 0);
[[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch.
+ Window::SeparateId id) const;
+ [[nodiscard]] Window::Controller *windowForShowingHistory(
not_null peer) const;
- [[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch.
- not_null account) const;
+ [[nodiscard]] Window::Controller *windowForShowingForum(
+ not_null forum) const;
[[nodiscard]] bool closeNonLastAsync(
not_null window);
void closeWindow(not_null window);
@@ -195,7 +194,7 @@ class Application final : public QObject {
void checkSystemDarkMode();
[[nodiscard]] bool isActiveForTrayMenu() const;
void closeChatFromWindows(not_null peer);
- void checkWindowAccount(not_null window);
+ void checkWindowId(not_null window);
void activate();
// Media view interface.
@@ -352,6 +351,7 @@ class Application final : public QObject {
friend bool IsAppLaunched();
friend Application &App();
+ void autoRegisterUrlScheme();
void clearEmojiSourceImages();
[[nodiscard]] auto prepareEmojiSourceImages()
-> std::shared_ptr;
@@ -423,12 +423,9 @@ class Application final : public QObject {
const std::unique_ptr _calls;
const std::unique_ptr _iv;
base::flat_map<
- Main::Account*,
- std::unique_ptr> _primaryWindows;
+ Window::SeparateId,
+ std::unique_ptr> _windows;
base::flat_set> _closingAsyncWindows;
- base::flat_map<
- not_null